Skip to content

Commit

Permalink
fix: minor enclave config changes and ui build (#1737)
Browse files Browse the repository at this point in the history
## Description:
This PR includes an update to the emui build and a few small fixes:
* implement artifact download
* Tweak editor resizing
* Fix back click after create enclave
* Require enclave name matches regex
* prettier run

## Is this change user facing?
YES
  • Loading branch information
Dartoxian authored Nov 10, 2023
1 parent 2f24b73 commit e349ce8
Show file tree
Hide file tree
Showing 27 changed files with 123 additions and 53 deletions.
2 changes: 2 additions & 0 deletions enclave-manager/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@
"react-router-dom": "^6.17.0",
"react-scripts": "5.0.1",
"react-virtuoso": "^4.6.2",
"streamsaver": "^2.0.6",
"true-myth": "^7.1.0"
},
"devDependencies": {
"@types/luxon": "^3.3.3",
"@types/node": "^16.7.13",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/streamsaver": "^2.0.4",
"monaco-editor": "^0.44.0",
"prettier": "3.0.3",
"prettier-plugin-organize-imports": "^3.2.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,14 @@ export abstract class KurtosisClient {
async downloadFilesArtifact(enclave: RemoveFunctions<EnclaveInfo>, file: FilesArtifactNameAndUuid) {
const apicInfo = enclave.apiContainerInfo;
assertDefined(
apicInfo,
`Cannot download files artifact because the passed enclave '${enclave.name}' does not have apicInfo`,
apicInfo,
`Cannot download files artifact because the passed enclave '${enclave.name}' does not have apicInfo`,
);
// Not currently using asyncResult as the return type here is an asyncIterable
const request = new DownloadFilesArtifactRequest({
apicIpAddress: apicInfo.bridgeIpAddress,
apicPort: apicInfo.grpcPortInsideEnclave,
downloadFilesArtifactsArgs: new DownloadFilesArtifactArgs({ identifier: file.fileUuid})
downloadFilesArtifactsArgs: new DownloadFilesArtifactArgs({ identifier: file.fileUuid }),
});
return this.client.downloadFilesArtifact(request, this.getHeaderOptions());
}
Expand Down
13 changes: 8 additions & 5 deletions enclave-manager/web/src/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Box } from "@chakra-ui/react";
import { Editor, OnChange, OnMount } from "@monaco-editor/react";
import { editor } from "monaco-editor";
import { useEffect, useState } from "react";
import { useState } from "react";
import { isDefined } from "../utils";

type CodeEditorProps = {
Expand All @@ -16,8 +16,9 @@ export const CodeEditor = ({ text, onTextChange, showLineNumbers }: CodeEditorPr

const resizeEditorBasedOnContent = () => {
if (isDefined(editor)) {
const contentHeight = Math.min(750, editor.getContentHeight() || 10);
const contentHeight = editor.getContentHeight();
editor.layout({ width: 500, height: contentHeight });
// Unclear why layout must be called twice, but seems to be necessary
editor.layout();
}
};
Expand All @@ -33,17 +34,19 @@ export const CodeEditor = ({ text, onTextChange, showLineNumbers }: CodeEditorPr
monaco.editor.setTheme("kurtosis-theme");
};

useEffect(() => resizeEditorBasedOnContent(), [editor]);

const handleChange: OnChange = (value, ev) => {
if (isDefined(value) && onTextChange) {
onTextChange(value);
resizeEditorBasedOnContent();
}
};

// Triggering this on every render seems to keep the editor correctly sized
// it is unclear why this is the case.
resizeEditorBasedOnContent();

return (
<Box width={"100%"} minHeight={`${editor?.getLayoutInfo().height || 10}px`}>
<Box width={"100%"}>
<Editor
onMount={handleMount}
value={text}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button, Menu, MenuButton, MenuItem, MenuList } from "@chakra-ui/react";
import { FiPackage, FiPlus, FiSettings } from "react-icons/fi";
import { Button, Menu, MenuButton } from "@chakra-ui/react";
import { FiPlus } from "react-icons/fi";
import { useNavigate } from "react-router-dom";
import { KURTOSIS_CREATE_ENCLAVE_URL_ARG } from "../constants";

Expand All @@ -11,7 +11,8 @@ export const CreateEnclaveButton = () => {
<MenuButton
as={Button}
colorScheme={"kurtosisGreen"}
leftIcon={<FiPlus />} size={"md"}
leftIcon={<FiPlus />}
size={"md"}
onClick={() => navigate(`#${KURTOSIS_CREATE_ENCLAVE_URL_ARG}`)}
>
New Enclave
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const BooleanArgumentInput = ({ inputType, ...props }: BooleanArgumentInp
disabled: props.disabled,
required: props.isRequired,
value: true,
validate: props.validate,
})}
/>
);
Expand All @@ -27,6 +28,7 @@ export const BooleanArgumentInput = ({ inputType, ...props }: BooleanArgumentInp
{...register(props.name, {
disabled: props.disabled,
required: props.isRequired,
validate: props.validate,
})}
value={"true"}
>
Expand All @@ -36,6 +38,7 @@ export const BooleanArgumentInput = ({ inputType, ...props }: BooleanArgumentInp
{...register(props.name, {
disabled: props.disabled,
required: props.isRequired,
validate: props.validate,
})}
value={"false"}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ export const DictArgumentInput = ({ keyType, valueType, ...otherProps }: DictArg
<KurtosisArgumentTypeInput
type={keyType}
name={`${otherProps.name as `args.${string}.${number}.value`}.${i}.key`}
validate={otherProps.validate}
isRequired
/>
<KurtosisArgumentTypeInput
type={valueType}
name={`${otherProps.name as `args.${string}.${number}.value`}.${i}.value`}
validate={otherProps.validate}
isRequired
/>
<Button onClick={() => remove(i)} leftIcon={<FiDelete />} minW={"90px"}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Input } from "@chakra-ui/react";
import { isDefined } from "../../../../utils";
import { useEnclaveConfigurationFormContext } from "../EnclaveConfigurationForm";
import { KurtosisArgumentTypeInputProps } from "./KurtosisArgumentTypeInput";

Expand All @@ -14,6 +15,11 @@ export const IntegerArgumentInput = (props: Omit<KurtosisArgumentTypeInputProps,
if (isNaN(value)) {
return "This value should be an integer";
}

const propsValidation = props.validate ? props.validate(value) : undefined;
if (isDefined(propsValidation)) {
return propsValidation;
}
},
})}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Controller } from "react-hook-form";
import { stringifyError } from "../../../../utils";
import { isDefined, stringifyError } from "../../../../utils";
import { CodeEditor } from "../../../CodeEditor";
import { useEnclaveConfigurationFormContext } from "../EnclaveConfigurationForm";
import { KurtosisArgumentTypeInputProps } from "./KurtosisArgumentTypeInput";
Expand All @@ -20,6 +20,11 @@ export const JSONArgumentInput = (props: Omit<KurtosisArgumentTypeInputProps, "t
} catch (err: any) {
return `This is not valid JSON. ${stringifyError(err)}`;
}

const propsValidation = props.validate ? props.validate(value) : undefined;
if (isDefined(propsValidation)) {
return propsValidation;
}
},
}}
disabled={props.disabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type KurtosisArgumentTypeInputProps = {
subType2?: ArgumentValueType;
name: FieldPath<ConfigureEnclaveForm>;
isRequired?: boolean;
validate?: (value: any) => string | undefined;
disabled?: boolean;
};

Expand All @@ -24,11 +25,12 @@ export const KurtosisArgumentTypeInput = ({
subType2,
name,
isRequired,
validate,
disabled,
}: KurtosisArgumentTypeInputProps) => {
switch (type) {
case ArgumentValueType.INTEGER:
return <IntegerArgumentInput name={name} isRequired={isRequired} disabled={disabled} />;
return <IntegerArgumentInput name={name} isRequired={isRequired} disabled={disabled} validate={validate} />;
case ArgumentValueType.DICT:
assertDefined(subType1, `innerType1 was not defined on DICT argument ${name}`);
assertDefined(subType2, `innerType2 was not defined on DICT argument ${name}`);
Expand All @@ -38,18 +40,27 @@ export const KurtosisArgumentTypeInput = ({
isRequired={isRequired}
keyType={subType1}
valueType={subType2}
validate={validate}
disabled={disabled}
/>
);
case ArgumentValueType.LIST:
assertDefined(subType1, `innerType1 was not defined on DICT argument ${name}`);
return <ListArgumentInput name={name} isRequired={isRequired} valueType={subType1} disabled={disabled} />;
return (
<ListArgumentInput
name={name}
isRequired={isRequired}
valueType={subType1}
validate={validate}
disabled={disabled}
/>
);
case ArgumentValueType.BOOL:
return <BooleanArgumentInput name={name} isRequired={isRequired} />;
return <BooleanArgumentInput name={name} isRequired={isRequired} validate={validate} disabled={disabled} />;
case ArgumentValueType.STRING:
return <StringArgumentInput name={name} isRequired={isRequired} disabled={disabled} />;
return <StringArgumentInput name={name} isRequired={isRequired} validate={validate} disabled={disabled} />;
case ArgumentValueType.JSON:
default:
return <JSONArgumentInput name={name} isRequired={isRequired} disabled={disabled} />;
return <JSONArgumentInput name={name} isRequired={isRequired} validate={validate} disabled={disabled} />;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const ListArgumentInput = ({ valueType, ...otherProps }: ListArgumentInpu
type={valueType}
name={`${otherProps.name as `args.${string}.${number}`}.${i}.value`}
isRequired
validate={otherProps.validate}
/>
<Button onClick={() => remove(i)} leftIcon={<FiDelete />} minW={"90px"}>
Delete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@ import { KurtosisArgumentTypeInputProps } from "./KurtosisArgumentTypeInput";
export const StringArgumentInput = (props: Omit<KurtosisArgumentTypeInputProps, "type">) => {
const { register } = useEnclaveConfigurationFormContext();

return <Input {...register(props.name, { disabled: props.disabled, required: props.isRequired })} />;
return (
<Input
{...register(props.name, { disabled: props.disabled, required: props.isRequired, validate: props.validate })}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function argTypeToString(argType?: ArgumentValueType) {
case ArgumentValueType.LIST:
return "list";
case ArgumentValueType.STRING:
return "string";
return "text";
default:
return "json";
}
Expand All @@ -29,7 +29,7 @@ export function argToTypeString(arg: PackageArg) {
case ArgumentValueType.DICT:
return `${argTypeToString(arg.typeV2.innerType1)} -> ${argTypeToString(arg.typeV2.innerType2)}`;
case ArgumentValueType.LIST:
return `${argTypeToString(arg.typeV2.innerType1)}[]`;
return `${argTypeToString(arg.typeV2.innerType1)} list`;
default:
return "json";
}
Expand Down
4 changes: 1 addition & 3 deletions enclave-manager/web/src/components/enclaves/logs/LogLine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ export const LogLine = ({ timestamp, message, status }: LogLineProps) => {
color={"grey"}
minW={"200px"}
>
<>
{timestamp.toLocal().toFormat("yyyy-MM-dd HH:mm:ss.SSS ZZZZ")}
</>
<>{timestamp.toLocal().toFormat("yyyy-MM-dd HH:mm:ss.SSS ZZZZ")}</>
</Box>
)}
<Box
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { StringArgumentInput } from "../configuration/inputs/StringArgumentInput
import { KurtosisArgumentFormControl } from "../configuration/KurtosisArgumentFormControl";
import { KurtosisPackageArgumentInput } from "../configuration/KurtosisPackageArgumentInput";
import { ConfigureEnclaveForm } from "../configuration/types";
import { allowedEnclaveNamePattern, isEnclaveNameAllowed } from "../utils";
import { EnclaveSourceButton } from "../widgets/EnclaveSourceButton";

type ConfigureEnclaveModalProps = {
Expand Down Expand Up @@ -207,7 +208,15 @@ export const ConfigureEnclaveModal = ({
</Tooltip>
</Flex>
<KurtosisArgumentFormControl name={"enclaveName"} label={"Enclave name"} type={"string"}>
<StringArgumentInput name={"enclaveName"} disabled={isDefined(existingEnclave)} />
<StringArgumentInput
name={"enclaveName"}
disabled={isDefined(existingEnclave)}
validate={(value) => {
if (!isEnclaveNameAllowed(value)) {
return `The enclave name must match ${allowedEnclaveNamePattern}`;
}
}}
/>
</KurtosisArgumentFormControl>
{kurtosisPackage.args.map((arg, i) => (
<KurtosisPackageArgumentInput key={i} argument={arg} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useMemo } from "react";
import { EnclaveFullInfo } from "../../../emui/enclaves/types";
import { RemoveFunctions } from "../../../utils/types";
import { DataTable } from "../../DataTable";
import { DownloadFileButton } from "../widgets/DownloadFileButton";
import { DownloadFileArtifactButton } from "../widgets/DownloadFileArtifactButton";

const columnHelper = createColumnHelper<FilesArtifactNameAndUuid>();

Expand All @@ -25,12 +25,12 @@ export const FilesTable = ({ filesAndArtifacts, enclave }: FilesTableProps) => {
//<Link to={`/enclave/${enclave.shortenedUuid}/file/${row.original.fileUuid}`}>
//<Button size={"sm"} variant={"ghost"}>
getValue(),
//</Button>
//</Link>
//</Button>
//</Link>
}),
columnHelper.display({
id: "download",
cell: ({ row }) => <DownloadFileButton file={row.original} enclave={enclave} />,
cell: ({ row }) => <DownloadFileArtifactButton file={row.original} enclave={enclave} />,
}),
],
[enclave],
Expand Down
10 changes: 10 additions & 0 deletions enclave-manager/web/src/components/enclaves/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Port_TransportProtocol } from "enclave-manager-sdk/build/api_container_service_pb";
import { isDefined } from "../../utils";

export function transportProtocolToString(protocol: Port_TransportProtocol) {
switch (protocol) {
Expand All @@ -10,3 +11,12 @@ export function transportProtocolToString(protocol: Port_TransportProtocol) {
return "UDP";
}
}

export const allowedEnclaveNamePattern = /^[-A-Za-z0-9]{1,60}$/;

export function isEnclaveNameAllowed(name: any): boolean {
if (typeof name !== "string") {
return false;
}
return isDefined(name.match(allowedEnclaveNamePattern));
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,32 @@
import { useToast } from "@chakra-ui/react";
import { FilesArtifactNameAndUuid } from "enclave-manager-sdk/build/api_container_service_pb";
import { useState } from "react";
import streamsaver from "streamsaver";
import { useKurtosisClient } from "../../../client/enclaveManager/KurtosisClientContext";
import { EnclaveFullInfo } from "../../../emui/enclaves/types";
import { saveTextAsFile } from "../../../utils/download";
import { DownloadButton } from "../../DownloadButton";

type DownloadFileButtonProps = {
file: FilesArtifactNameAndUuid;
enclave: EnclaveFullInfo;
};

export const DownloadFileButton = ({ file, enclave }: DownloadFileButtonProps) => {
export const DownloadFileArtifactButton = ({ file, enclave }: DownloadFileButtonProps) => {
const kurtosisClient = useKurtosisClient();
const toast = useToast();
const [isLoading, setIsLoading] = useState(false);

const handleDownloadClick = async () => {
setIsLoading(true);
// todo: get tgz download instead
const maybeFile = await kurtosisClient.inspectFilesArtifactContents(enclave, file);
if (maybeFile.isErr) {
toast({
title: `Could not inspect ${file.fileName}: ${maybeFile.error}`,
colorScheme: "red",
});
setIsLoading(false);
return;
}
const fileParts = await kurtosisClient.downloadFilesArtifact(enclave, file);
const writableStream = streamsaver.createWriteStream(`${enclave.name}-${file.fileName}.tgz`);
const writer = writableStream.getWriter();

saveTextAsFile("some file", `${enclave.name}-${file.fileName}.tgz`);
for await (const part of fileParts) {
await writer.write(part.data);
}
await writer.close();
setIsLoading(false);
};

Expand Down
Loading

0 comments on commit e349ce8

Please sign in to comment.