Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: emui optimistic data loading #1771

Merged
merged 5 commits into from Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 6 additions & 4 deletions enclave-manager/web/package.json
Expand Up @@ -12,8 +12,11 @@
"@emotion/styled": "^11.11.0",
"@monaco-editor/react": "^4.6.0",
"@tanstack/react-table": "^8.10.7",
"ansi-to-html": "^0.7.2",
"enclave-manager-sdk": "file:../api/typescript",
"framer-motion": "^10.16.4",
"has-ansi": "^5.0.1",
"html-react-parser": "^4.2.2",
"lodash": "^4.17.21",
"luxon": "^3.4.3",
"react": "^18.2.0",
Expand All @@ -25,10 +28,7 @@
"react-scripts": "5.0.1",
"react-virtuoso": "^4.6.2",
"streamsaver": "^2.0.6",
"true-myth": "^7.1.0",
"ansi-to-html": "^0.7.2",
"has-ansi": "^5.0.1",
"html-react-parser": "^4.2.2"
"true-myth": "^7.1.0"
},
"devDependencies": {
"@types/luxon": "^3.3.3",
Expand All @@ -39,6 +39,7 @@
"monaco-editor": "^0.44.0",
"prettier": "3.0.3",
"prettier-plugin-organize-imports": "^3.2.3",
"serve": "^14.2.1",
"source-map-explorer": "^2.5.3",
"typescript": "^4.4.2"
},
Expand All @@ -48,6 +49,7 @@
"clean": "rm -rf build",
"cleanInstall": "rm -rf node_modules; yarn install",
"start": "react-scripts start",
"start:prod": "serve -s build",
"build": "react-scripts build",
"postbuild": "cp -r build/ ../../engine/server/webapp",
"prettier": "prettier . --check",
Expand Down
Expand Up @@ -36,6 +36,7 @@ export class KurtosisPackageIndexerClient {
baseUrl: "github.com",
owner: components[1],
name: components[2],
rootPath: components.filter((v, i) => i > 2 && v.length > 0).join("/") + "/",
},
}),
);
Expand Down
21 changes: 14 additions & 7 deletions enclave-manager/web/src/components/KurtosisBreadcrumbs.tsx
Expand Up @@ -2,19 +2,23 @@ import { ChevronRightIcon } from "@chakra-ui/icons";
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, Button, Flex } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { Link, Params, UIMatch, useMatches } from "react-router-dom";
import { EmuiAppState, useEmuiAppContext } from "../emui/EmuiAppContext";
import { isDefined } from "../utils";
import { RemoveFunctions } from "../utils/types";

export type KurtosisBreadcrumb = {
name: string;
destination: string;
};

export const KurtosisBreadcrumbs = () => {
const { enclaves, filesAndArtifactsByEnclave, starlarkRunsByEnclave, servicesByEnclave } = useEmuiAppContext();

const matches = useMatches() as UIMatch<
object,
{
crumb?: (
data: Record<string, object>,
state: RemoveFunctions<EmuiAppState>,
params: Params<string>,
) => KurtosisBreadcrumb | Promise<KurtosisBreadcrumb>;
}
Expand All @@ -24,21 +28,24 @@ export const KurtosisBreadcrumbs = () => {

useEffect(() => {
(async () => {
const allLoaderData = matches
.filter((match) => isDefined(match.data))
.reduce((acc, match) => ({ ...acc, [match.id]: match.data }), {});

setMatchCrumbs(
await Promise.all(
matches
.map((match) =>
isDefined(match.handle?.crumb) ? Promise.resolve(match.handle.crumb(allLoaderData, match.params)) : null,
isDefined(match.handle?.crumb)
? Promise.resolve(
match.handle.crumb(
{ enclaves, filesAndArtifactsByEnclave, starlarkRunsByEnclave, servicesByEnclave },
match.params,
),
)
: null,
)
.filter(isDefined),
),
);
})();
}, [matches]);
}, [matches, enclaves, filesAndArtifactsByEnclave, starlarkRunsByEnclave, servicesByEnclave]);

return (
<Flex h="40px" p={"4px 0"} alignItems={"center"}>
Expand Down
Expand Up @@ -20,6 +20,14 @@ export const EditEnclaveButton = ({ enclave }: EditEnclaveButtonProps) => {
setKurtosisPackage(kurtosisPackage);
};

if (!isDefined(enclave.starlarkRun)) {
return (
<Button isLoading={true} colorScheme={"blue"} leftIcon={<FiEdit2 />} size={"md"}>
Edit
</Button>
);
}

if (enclave.starlarkRun.isErr) {
return (
<Tooltip label={"Cannot find previous run config to edit"}>
Expand Down
Expand Up @@ -58,7 +58,7 @@ export const EnclaveConfigurationForm = forwardRef<
case ArgumentValueType.LIST:
return value.map((v: any) => transformValue(innerValuetype, v));
case ArgumentValueType.BOOL:
return isStringTrue(value);
return isDefined(value) ? isStringTrue(value) : null;
case ArgumentValueType.INTEGER:
return isNaN(value) || isNaN(parseFloat(value)) ? null : parseFloat(value);
case ArgumentValueType.STRING:
Expand Down
Expand Up @@ -7,7 +7,9 @@ type BooleanArgumentInputProps = Omit<KurtosisArgumentTypeInputProps, "type"> &
};

export const BooleanArgumentInput = ({ inputType, ...props }: BooleanArgumentInputProps) => {
const { register } = useEnclaveConfigurationFormContext();
const { register, getValues } = useEnclaveConfigurationFormContext();

const currentDefault = getValues(props.name);

if (inputType === "switch") {
return (
Expand All @@ -22,7 +24,7 @@ export const BooleanArgumentInput = ({ inputType, ...props }: BooleanArgumentInp
);
} else {
return (
<RadioGroup>
<RadioGroup defaultValue={currentDefault}>
<Stack direction={"row"}>
<Radio
{...register(props.name, {
Expand Down
Expand Up @@ -40,6 +40,7 @@ export const DictArgumentInput = ({ keyType, valueType, ...otherProps }: DictArg
<ButtonGroup isAttached>
<CopyButton
contentName={"value"}
size={"sm"}
valueToCopy={() =>
JSON.stringify(
getValues(otherProps.name).reduce(
Expand All @@ -49,7 +50,7 @@ export const DictArgumentInput = ({ keyType, valueType, ...otherProps }: DictArg
)
}
/>
<PasteButton onValuePasted={handleValuePaste} />
<PasteButton size="sm" onValuePasted={handleValuePaste} />
</ButtonGroup>
{fields.map((field, i) => (
<Flex key={i} gap={"10px"}>
Expand All @@ -63,7 +64,7 @@ export const DictArgumentInput = ({ keyType, valueType, ...otherProps }: DictArg
name={`${otherProps.name as `args.${string}`}.${i}.key`}
validate={otherProps.validate}
isRequired
size={"xs"}
size={"sm"}
width={"222px"}
/>
</KurtosisArgumentSubtypeFormControl>
Expand All @@ -77,17 +78,17 @@ export const DictArgumentInput = ({ keyType, valueType, ...otherProps }: DictArg
name={`${otherProps.name as `args.${string}`}.${i}.value`}
validate={otherProps.validate}
isRequired
size={"xs"}
size={"sm"}
width={"222px"}
/>
</KurtosisArgumentSubtypeFormControl>
<Button onClick={() => remove(i)} leftIcon={<FiDelete />} size={"xs"} colorScheme={"red"}>
<Button onClick={() => remove(i)} leftIcon={<FiDelete />} size={"sm"} colorScheme={"red"}>
Delete
</Button>
</Flex>
))}
<Flex>
<Button onClick={() => append({})} leftIcon={<FiPlus />} size={"xs"} colorScheme={"kurtosisGreen"}>
<Button onClick={() => append({})} leftIcon={<FiPlus />} size={"sm"} colorScheme={"kurtosisGreen"}>
Add
</Button>
</Flex>
Expand Down
Expand Up @@ -38,6 +38,7 @@ export const ListArgumentInput = ({ valueType, ...otherProps }: ListArgumentInpu
<Flex flexDirection={"column"} gap={"10px"}>
<ButtonGroup isAttached>
<CopyButton
size={"sm"}
contentName={"value"}
valueToCopy={() => JSON.stringify(getValues(otherProps.name).map(({ value }: { value: any }) => value))}
/>
Expand All @@ -56,16 +57,16 @@ export const ListArgumentInput = ({ valueType, ...otherProps }: ListArgumentInpu
isRequired
validate={otherProps.validate}
width={"411px"}
size={"xs"}
size={"sm"}
/>
</KurtosisArgumentSubtypeFormControl>
<Button onClick={() => remove(i)} leftIcon={<FiDelete />} size={"xs"} colorScheme={"red"}>
<Button onClick={() => remove(i)} leftIcon={<FiDelete />} size={"sm"} colorScheme={"red"}>
Delete
</Button>
</Flex>
))}
<Flex>
<Button onClick={() => append({ value: "" })} leftIcon={<FiPlus />} colorScheme={"kurtosisGreen"} size={"xs"}>
<Button onClick={() => append({ value: "" })} leftIcon={<FiPlus />} colorScheme={"kurtosisGreen"} size={"sm"}>
Add
</Button>
</Flex>
Expand Down
Expand Up @@ -15,9 +15,10 @@ import {
import { EnclaveMode } from "enclave-manager-sdk/build/engine_service_pb";
import { useMemo, useRef, useState } from "react";
import { SubmitHandler } from "react-hook-form";
import { useNavigate, useSubmit } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { useKurtosisClient } from "../../../client/enclaveManager/KurtosisClientContext";
import { ArgumentValueType, KurtosisPackage } from "../../../client/packageIndexer/api/kurtosis_package_indexer_pb";
import { useEmuiAppContext } from "../../../emui/EmuiAppContext";
import { EnclaveFullInfo } from "../../../emui/enclaves/types";
import { assertDefined, isDefined, stringifyError } from "../../../utils";
import { KURTOSIS_PACKAGE_ID_URL_ARG, KURTOSIS_PACKAGE_PARAMS_URL_ARG } from "../../constants";
Expand Down Expand Up @@ -49,14 +50,14 @@ export const ConfigureEnclaveModal = ({
existingEnclave,
}: ConfigureEnclaveModalProps) => {
const kurtosisClient = useKurtosisClient();
const { createEnclave, runStarlarkPackage } = useEmuiAppContext();
const navigator = useNavigate();
const submit = useSubmit();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string>();
const formRef = useRef<EnclaveConfigurationFormImperativeAttributes>(null);

const initialValues = useMemo(() => {
if (isDefined(existingEnclave)) {
if (isDefined(existingEnclave) && isDefined(existingEnclave.starlarkRun)) {
if (existingEnclave.starlarkRun.isErr) {
setError(
`Could not retrieve starlark run for previous configuration, got error: ${existingEnclave.starlarkRun.isErr}`,
Expand All @@ -73,7 +74,7 @@ export const ConfigureEnclaveModal = ({
): any => {
switch (argType) {
case ArgumentValueType.BOOL:
return !!value ? "true" : "false";
return !!value ? "true" : isDefined(value) ? "false" : "";
case ArgumentValueType.INTEGER:
return isDefined(value) ? `${value}` : "";
case ArgumentValueType.STRING:
Expand Down Expand Up @@ -158,7 +159,7 @@ export const ConfigureEnclaveModal = ({
let enclaveUUID = existingEnclave?.shortenedUuid;
if (!isDefined(existingEnclave)) {
setIsLoading(true);
const newEnclave = await kurtosisClient.createEnclave(formData.enclaveName, "info", formData.restartServices);
const newEnclave = await createEnclave(formData.enclaveName, "info", formData.restartServices);
setIsLoading(false);

if (newEnclave.isErr) {
Expand All @@ -177,14 +178,9 @@ export const ConfigureEnclaveModal = ({
setError(`Cannot trigger starlark run as apic info cannot be found`);
return;
}
submit(
{ config: formData, packageId: kurtosisPackage.name, apicInfo: apicInfo.toJson() },
{
method: "post",
action: `/enclave/${enclaveUUID}/logs`,
encType: "application/json",
},
);

const logsIterator = await runStarlarkPackage(apicInfo, kurtosisPackage.name, formData.args);
navigator(`/enclave/${enclaveUUID}/logs`, { state: { logs: logsIterator } });
onClose();
};

Expand Down
Expand Up @@ -6,6 +6,7 @@ import { DateTime } from "luxon";
import { useMemo } from "react";
import { Link } from "react-router-dom";
import { EnclaveFullInfo } from "../../../emui/enclaves/types";
import { isDefined } from "../../../utils";
import { DataTable } from "../../DataTable";
import { FormatDateTime } from "../../FormatDateTime";
import { EnclaveArtifactsSummary } from "../widgets/EnclaveArtifactsSummary";
Expand All @@ -18,9 +19,9 @@ type EnclaveTableRow = {
name: string;
status: EnclaveContainersStatus;
created: DateTime | null;
source: string | null;
services: ServiceInfo[] | null;
artifacts: FilesArtifactNameAndUuid[] | null;
source: "loading" | string | null;
services: "loading" | ServiceInfo[] | null;
artifacts: "loading" | FilesArtifactNameAndUuid[] | null;
};

const enclaveToRow = (enclave: EnclaveFullInfo): EnclaveTableRow => {
Expand All @@ -29,9 +30,21 @@ const enclaveToRow = (enclave: EnclaveFullInfo): EnclaveTableRow => {
name: enclave.name,
status: enclave.containersStatus,
created: enclave.creationTime ? DateTime.fromJSDate(enclave.creationTime.toDate()) : null,
source: enclave.starlarkRun.isOk ? enclave.starlarkRun.value.packageId : null,
services: enclave.services.isOk ? Object.values(enclave.services.value.serviceInfo) : null,
artifacts: enclave.filesAndArtifacts.isOk ? enclave.filesAndArtifacts.value.fileNamesAndUuids : null,
source: !isDefined(enclave.starlarkRun)
? "loading"
: enclave.starlarkRun.isOk
? enclave.starlarkRun.value.packageId
: null,
services: !isDefined(enclave.services)
? "loading"
: enclave.services.isOk
? Object.values(enclave.services.value.serviceInfo)
: null,
artifacts: !isDefined(enclave.filesAndArtifacts)
? "loading"
: enclave.filesAndArtifacts.isOk
? enclave.filesAndArtifacts.value.fileNamesAndUuids
: null,
};
};

Expand Down
@@ -1,7 +1,8 @@
import { Button } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { FiTrash2 } from "react-icons/fi";
import { useFetcher } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { useEmuiAppContext } from "../../../emui/EmuiAppContext";
import { EnclaveFullInfo } from "../../../emui/enclaves/types";
import { KurtosisAlertModal } from "../../KurtosisAlertModal";

Expand All @@ -10,7 +11,8 @@ type DeleteEnclavesButtonProps = {
};

export const DeleteEnclavesButton = ({ enclaves }: DeleteEnclavesButtonProps) => {
const fetcher = useFetcher();
const { destroyEnclave } = useEmuiAppContext();
const navigator = useNavigate();

const [showModal, setShowModal] = useState(false);
const [isLoading, setIsLoading] = useState(false);
Expand All @@ -28,13 +30,12 @@ export const DeleteEnclavesButton = ({ enclaves }: DeleteEnclavesButtonProps) =>

const handleDelete = async () => {
setIsLoading(true);
fetcher.submit(
{
intent: "delete",
enclaveUUIDs: enclaves.map(({ enclaveUuid }) => enclaveUuid),
},
{ method: "post", action: "/enclaves", encType: "application/json" },
);
for (const enclaveUUID of enclaves.map(({ enclaveUuid }) => enclaveUuid)) {
await destroyEnclave(enclaveUUID);
}
navigator("/enclaves");
setIsLoading(false);
setShowModal(false);
};

return (
Expand Down
@@ -1,16 +1,20 @@
import { Flex, Tag, TagProps, Tooltip } from "@chakra-ui/react";
import { Flex, Spinner, Tag, TagProps, Tooltip } from "@chakra-ui/react";
import { ServiceInfo, ServiceStatus } from "enclave-manager-sdk/build/api_container_service_pb";
import { isDefined } from "../../../utils";

type ServicesSummaryProps = {
services: ServiceInfo[] | null;
services: "loading" | ServiceInfo[] | null;
};

export const EnclaveServicesSummary = ({ services }: ServicesSummaryProps) => {
if (!isDefined(services)) {
return <Tag>Unknown</Tag>;
}

if (services === "loading") {
return <Spinner size={"xs"} />;
}

const runningServices = services.filter(({ serviceStatus }) => serviceStatus === ServiceStatus.RUNNING).length;
const stopppedServices = services.filter(({ serviceStatus }) => serviceStatus === ServiceStatus.STOPPED).length;
const unknownServices = services.filter(({ serviceStatus }) => serviceStatus === ServiceStatus.UNKNOWN).length;
Expand Down