Skip to content

Commit

Permalink
feat: emui design updates (#2028)
Browse files Browse the repository at this point in the history
## Description:
This PR (apologies for the size) implements several changes received in
the new EMUI mocks:
* The enclave configuration is moved to a drawer
* The package catalog is used when choosing a package to run in a new
enclave
* Support for YAML configuration of packages
* Various components have been updated with the new designs (log
navigation, button colours, table layouts, warning modals, alerts)
* Unused 'feature not implemented' modal is removed

### Demo


https://github.com/kurtosis-tech/kurtosis/assets/4419574/799287e6-8f91-4326-8853-7bdc284d13db

Alert change demo


![image](https://github.com/kurtosis-tech/kurtosis/assets/4419574/0f1e64d1-44fa-4fca-8c0c-b49097297b7e)

## Is this change user facing?
YES

## References (if applicable):

https://www.figma.com/file/8WkttVmRVJfudO9i9n4RGD/Handoff-Screens?node-id=1866%3A92406&mode=dev
  • Loading branch information
Dartoxian committed Jan 8, 2024
1 parent 7f8db9b commit 0e480cf
Show file tree
Hide file tree
Showing 58 changed files with 1,437 additions and 762 deletions.
4 changes: 2 additions & 2 deletions enclave-manager/web/packages/app/src/emui/catalog/Catalog.tsx
Expand Up @@ -40,7 +40,7 @@ import { HiStar } from "react-icons/hi";
import { IoFilterSharp, IoPlay } from "react-icons/io5";
import { MdBookmarkAdded } from "react-icons/md";
import { useSearchParams } from "react-router-dom";
import { ConfigureEnclaveModal } from "../enclaves/components/modals/ConfigureEnclaveModal";
import { CreateOrConfigureEnclaveDrawer } from "../enclaves/components/configuration/drawer/CreateOrConfigureEnclaveDrawer";
import { EnclavesContextProvider } from "../enclaves/EnclavesContext";
import { useCatalogContext } from "./CatalogContext";

Expand Down Expand Up @@ -171,7 +171,7 @@ const CatalogImpl = ({ catalog }: CatalogImplProps) => {
)}
{configuringPackage && (
<EnclavesContextProvider skipInitialLoad>
<ConfigureEnclaveModal
<CreateOrConfigureEnclaveDrawer
isOpen={true}
onClose={() => setConfiguringPackage(undefined)}
kurtosisPackage={configuringPackage}
Expand Down
Expand Up @@ -19,7 +19,7 @@ import {
SaveKurtosisPackageButton,
TitledCard,
} from "kurtosis-ui-components";
import { ConfigureEnclaveModal } from "../../enclaves/components/modals/ConfigureEnclaveModal";
import { CreateOrConfigureEnclaveDrawer } from "../../enclaves/components/configuration/drawer/CreateOrConfigureEnclaveDrawer";
import { EnclavesContextProvider } from "../../enclaves/EnclavesContext";
import { useKurtosisPackage } from "../CatalogContext";

Expand Down Expand Up @@ -143,7 +143,7 @@ const PackageImpl = ({ kurtosisPackage }: PackageImplProps) => {
</Flex>
{showConfigurePackage && (
<EnclavesContextProvider skipInitialLoad>
<ConfigureEnclaveModal
<CreateOrConfigureEnclaveDrawer
isOpen={true}
onClose={() => setShowConfigurePackage(false)}
kurtosisPackage={kurtosisPackage}
Expand Down
@@ -1,8 +1,8 @@
import { Button, ButtonGroup, Flex } from "@chakra-ui/react";
import { AppPageLayout, KurtosisAlert, PageTitle } from "kurtosis-ui-components";
import { useEffect, useMemo, useState } from "react";
import { CreateEnclaveButton } from "./components/CreateEnclaveButton";
import { EnclavesTable } from "./components/tables/EnclavesTable";
import { CreateEnclaveButton } from "./components/widgets/CreateEnclaveButton";
import { DeleteEnclavesButton } from "./components/widgets/DeleteEnclavesButton";
import { useFullEnclaves } from "./EnclavesContext";
import { EnclaveFullInfo } from "./types";
Expand Down Expand Up @@ -43,7 +43,7 @@ export const EnclaveList = () => {
<CreateEnclaveButton />
</Flex>
</Flex>
<Flex direction="column" pt={"24px"} width={"100%"}>
<Flex direction="column" pt={"24px"} width={"100%"} flex={"1"}>
{enclaves.isOk && (
<EnclavesTable
enclavesData={enclaves.value}
Expand Down
Expand Up @@ -2,32 +2,24 @@ import { KurtosisPackage } from "kurtosis-cloud-indexer-sdk";
import { isDefined } from "kurtosis-ui-components";
import { useCallback, useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { ConfigureEnclaveModal } from "./modals/ConfigureEnclaveModal";
import { KURTOSIS_CREATE_ENCLAVE_URL_ARG } from "./modals/constants";
import { ManualCreateEnclaveModal } from "./modals/ManualCreateEnclaveModal";
import { KURTOSIS_CREATE_ENCLAVE_URL_ARG } from "./configuration/drawer/constants";
import { CreateOrConfigureEnclaveDrawer } from "./configuration/drawer/CreateOrConfigureEnclaveDrawer";
import { PreloadPackage } from "./PreloadPackage";

export const CreateEnclave = () => {
const navigate = useNavigate();
const location = useLocation();

const [configureEnclaveOpen, setConfigureEnclaveOpen] = useState(false);
const [kurtosisPackage, setKurtosisPackage] = useState<KurtosisPackage>();
const [manualCreateEnclaveOpen, setManualCreateEnclaveOpen] = useState(false);

useEffect(() => {
setManualCreateEnclaveOpen(location.hash === `#${KURTOSIS_CREATE_ENCLAVE_URL_ARG}`);
}, [location]);

const handleManualCreateEnclaveConfirmed = (kurtosisPackage: KurtosisPackage) => {
setKurtosisPackage(kurtosisPackage);
setManualCreateEnclaveOpen(false);
setConfigureEnclaveOpen(true);
};

const handleOnPackageLoaded = useCallback((kurtosisPackage: KurtosisPackage) => {
setKurtosisPackage(kurtosisPackage);
setConfigureEnclaveOpen(true);
setManualCreateEnclaveOpen(true);
}, []);

const handleCloseManualCreateEnclave = () => {
Expand All @@ -40,18 +32,11 @@ export const CreateEnclave = () => {
return (
<>
<PreloadPackage onPackageLoaded={handleOnPackageLoaded} />
<ManualCreateEnclaveModal
<CreateOrConfigureEnclaveDrawer
isOpen={manualCreateEnclaveOpen}
onClose={handleCloseManualCreateEnclave}
onConfirm={handleManualCreateEnclaveConfirmed}
kurtosisPackage={kurtosisPackage}
/>
{isDefined(kurtosisPackage) && (
<ConfigureEnclaveModal
isOpen={configureEnclaveOpen}
onClose={() => setConfigureEnclaveOpen(false)}
kurtosisPackage={kurtosisPackage}
/>
)}
</>
);
};
Expand Up @@ -4,7 +4,7 @@ import { isDefined } from "kurtosis-ui-components";
import { useState } from "react";
import { FiEdit2 } from "react-icons/fi";
import { EnclaveFullInfo } from "../types";
import { ConfigureEnclaveModal } from "./modals/ConfigureEnclaveModal";
import { CreateOrConfigureEnclaveDrawer } from "./configuration/drawer/CreateOrConfigureEnclaveDrawer";
import { PackageLoadingModal } from "./modals/PackageLoadingModal";

type EditEnclaveButtonProps = ButtonProps & {
Expand All @@ -13,11 +13,13 @@ type EditEnclaveButtonProps = ButtonProps & {

export const EditEnclaveButton = ({ enclave, ...buttonProps }: EditEnclaveButtonProps) => {
const [showPackageLoader, setShowPackageLoader] = useState(false);
const [showEnclaveConfiguration, setShowEnclaveConfiguration] = useState(false);
const [kurtosisPackage, setKurtosisPackage] = useState<KurtosisPackage>();

const handlePackageLoaded = (kurtosisPackage: KurtosisPackage) => {
setShowPackageLoader(false);
setKurtosisPackage(kurtosisPackage);
setShowEnclaveConfiguration(true);
};

if (!isDefined(enclave.starlarkRun)) {
Expand Down Expand Up @@ -57,14 +59,15 @@ export const EditEnclaveButton = ({ enclave, ...buttonProps }: EditEnclaveButton
{showPackageLoader && (
<PackageLoadingModal packageId={enclave.starlarkRun.value.packageId} onPackageLoaded={handlePackageLoaded} />
)}
{isDefined(kurtosisPackage) && (
<ConfigureEnclaveModal
isOpen={true}
onClose={() => setKurtosisPackage(undefined)}
kurtosisPackage={kurtosisPackage}
existingEnclave={enclave}
/>
)}
<CreateOrConfigureEnclaveDrawer
isOpen={showEnclaveConfiguration}
onClose={() => {
setKurtosisPackage(undefined);
setShowEnclaveConfiguration(false);
}}
kurtosisPackage={kurtosisPackage}
existingEnclave={enclave}
/>
</>
);
};
Expand Up @@ -5,7 +5,7 @@ import {
KurtosisBreadcrumbsImpl,
RemoveFunctions,
} from "kurtosis-ui-components";
import { ReactElement, useMemo } from "react";
import { Fragment, ReactElement, useMemo } from "react";
import { Params, UIMatch } from "react-router-dom";
import { EnclavesState, useEnclavesContext } from "../EnclavesContext";

Expand Down Expand Up @@ -67,7 +67,8 @@ export const KurtosisEnclavesBreadcrumbs = ({ matches }: KurtosisEnclavesBreadcr
)
: null,
)
.filter(isDefined),
.filter(isDefined)
.map((el, i) => <Fragment key={i}>{el}</Fragment>),
[
matches,
enclaves,
Expand Down
@@ -1,7 +1,7 @@
import { KurtosisPackage } from "kurtosis-cloud-indexer-sdk";
import { isDefined } from "kurtosis-ui-components";
import { useSearchParams } from "react-router-dom";
import { KURTOSIS_PACKAGE_ID_URL_ARG } from "./modals/constants";
import { KURTOSIS_PACKAGE_ID_URL_ARG } from "./configuration/drawer/constants";
import { PackageLoadingModal } from "./modals/PackageLoadingModal";

type PreloadEnclaveProps = {
Expand Down
@@ -1,9 +1,8 @@
import { ArgumentValueType, KurtosisPackage, PackageArg } from "kurtosis-cloud-indexer-sdk";
import { isDefined, isStringTrue } from "kurtosis-ui-components";
import { CSSProperties, forwardRef, PropsWithChildren, useImperativeHandle } from "react";
import { KurtosisPackage } from "kurtosis-cloud-indexer-sdk";
import { CSSProperties, forwardRef, PropsWithChildren, useEffect, useImperativeHandle, useState } from "react";
import { FormProvider, SubmitHandler, useForm, useFormContext } from "react-hook-form";
import YAML from "yaml";
import { ConfigureEnclaveForm } from "./types";
import { transformFormArgsToKurtosisArgs } from "./utils";

type EnclaveConfigurationFormProps = PropsWithChildren<{
onSubmit: SubmitHandler<ConfigureEnclaveForm>;
Expand All @@ -14,91 +13,50 @@ type EnclaveConfigurationFormProps = PropsWithChildren<{

export type EnclaveConfigurationFormImperativeAttributes = {
getValues: () => ConfigureEnclaveForm;
setValues: (key: keyof ConfigureEnclaveForm, value: any) => void;
isDirty: () => boolean;
};

export const EnclaveConfigurationForm = forwardRef<
EnclaveConfigurationFormImperativeAttributes,
EnclaveConfigurationFormProps
>(({ children, kurtosisPackage, onSubmit, initialValues, style }: EnclaveConfigurationFormProps, ref) => {
const methods = useForm<ConfigureEnclaveForm>({ values: initialValues });
const [isDirty, setIsDirty] = useState(false);
const methods = useForm<ConfigureEnclaveForm>({ defaultValues: initialValues });

useImperativeHandle(
ref,
() => ({
getValues: () => {
return methods.getValues();
},
setValues: (key: keyof ConfigureEnclaveForm, value: any) => {
methods.setValue(key, value);
},
isDirty: () => {
return isDirty;
},
}),
[methods],
[methods, isDirty],
);

const handleSubmit: SubmitHandler<ConfigureEnclaveForm> = (data: { args: { [x: string]: any } }) => {
const transformValue = (
valueType: ArgumentValueType | undefined,
value: any,
innerValuetype?: ArgumentValueType,
) => {
// The DICT type is stored as an array of {key, value} objects, before passing it up we should correct
// any instances of it to be Record<string, any> objects
const transformRecordsToObject = (records: { key: string; value: any }[], valueType?: ArgumentValueType) =>
records.reduce(
(acc, { key, value }) => ({
...acc,
[key]: valueType === ArgumentValueType.BOOL ? isStringTrue(value) : value,
}),
{},
);

switch (valueType) {
case ArgumentValueType.DICT:
if (!isDefined(value)) return {};
else return transformRecordsToObject(value, innerValuetype);
case ArgumentValueType.LIST:
return value.map((v: any) => transformValue(innerValuetype, v));
case ArgumentValueType.BOOL:
return isDefined(value) ? isStringTrue(value) : null;
case ArgumentValueType.INTEGER:
return isNaN(value) || isNaN(parseFloat(value)) ? null : parseFloat(value);
case ArgumentValueType.STRING:
return value;
case ArgumentValueType.JSON:
return YAML.parse(value);
default:
return value;
}
};

const newArgs: Record<string, any> = kurtosisPackage.args
.filter((arg) => arg.name !== "plan") // plan args needs to be filtered out as it's not an actual arg
.map((arg): [PackageArg, any] => [
arg,
transformValue(
arg.typeV2?.topLevelType,
data.args[arg.name],
arg.typeV2?.topLevelType === ArgumentValueType.LIST ? arg.typeV2?.innerType1 : arg.typeV2?.innerType2,
),
])
.filter(([arg, value]) => {
switch (arg.typeV2?.topLevelType) {
case ArgumentValueType.DICT:
return Object.keys(value).length > 0;
case ArgumentValueType.LIST:
return value.length > 0;
case ArgumentValueType.STRING:
return isDefined(value) && value.length > 0;
default:
return isDefined(value);
}
})
.reduce(
(acc, [arg, value]) => ({
...acc,
[arg.name]: value,
}),
{},
);
useEffect(() => {
const { unsubscribe } = methods.watch((value) => {
// We manually track modified fields because we dynamically register values (this means that the
// isDirty field on the react-hook-form state cannot be used). Relying on the react-hook-form state isDirty field
// will actually trigger a cyclic setState (as it's secretly a getter method).
setIsDirty(true);
});
return () => unsubscribe();
}, [methods]);

onSubmit({ enclaveName: "", restartServices: false, ...data, args: newArgs });
const handleSubmit: SubmitHandler<ConfigureEnclaveForm> = (data: { args: { [x: string]: any } }) => {
onSubmit({
enclaveName: "",
restartServices: false,
...data,
args: transformFormArgsToKurtosisArgs(data.args, kurtosisPackage),
});
};

return (
Expand All @@ -109,5 +67,6 @@ export const EnclaveConfigurationForm = forwardRef<
</FormProvider>
);
});
EnclaveConfigurationForm.displayName = "EnclaveConfigurationForm";

export const useEnclaveConfigurationFormContext = () => useFormContext<ConfigureEnclaveForm>();
@@ -1,18 +1,28 @@
import { Badge, Flex, FormControl, FormErrorMessage, FormHelperText, FormLabel } from "@chakra-ui/react";
import {
Badge,
Flex,
FormControl,
FormControlProps,
FormErrorMessage,
FormHelperText,
FormLabel,
} from "@chakra-ui/react";
import { isDefined, KurtosisMarkdown } from "kurtosis-ui-components";
import { PropsWithChildren } from "react";
import { FieldError, FieldPath } from "react-hook-form";
import { useEnclaveConfigurationFormContext } from "./EnclaveConfigurationForm";
import { ConfigureEnclaveForm } from "./types";

type KurtosisArguementFormControlProps = PropsWithChildren<{
name: FieldPath<ConfigureEnclaveForm>;
label: string;
type: string;
helperText?: string;
disabled?: boolean;
isRequired?: boolean;
}>;
type KurtosisArguementFormControlProps = PropsWithChildren<
FormControlProps & {
name: FieldPath<ConfigureEnclaveForm>;
label: string;
type: string;
helperText?: string;
disabled?: boolean;
isRequired?: boolean;
}
>;
export const KurtosisArgumentFormControl = ({
name,
label,
Expand All @@ -21,6 +31,7 @@ export const KurtosisArgumentFormControl = ({
disabled,
isRequired,
children,
...formControlProps
}: KurtosisArguementFormControlProps) => {
const {
formState: { errors },
Expand All @@ -31,9 +42,9 @@ export const KurtosisArgumentFormControl = ({
.reduce((e, part) => (isDefined(e) ? e[part] : undefined), errors as Record<string, any>) as FieldError | undefined;

return (
<FormControl isInvalid={isDefined(error)} isDisabled={disabled} isRequired={isRequired}>
<FormControl isInvalid={isDefined(error)} isDisabled={disabled} isRequired={isRequired} {...formControlProps}>
<Flex alignItems={"center"}>
<FormLabel>{label}</FormLabel>
<FormLabel fontWeight={"bold"}>{label}</FormLabel>
<Badge mb={2}>{type}</Badge>
</Flex>
{children}
Expand Down

0 comments on commit 0e480cf

Please sign in to comment.