Skip to content

Commit

Permalink
feat: support package nodes in enclave builder ui (#2283)
Browse files Browse the repository at this point in the history
## Description
THIS PR IS A COPY OF THIS PR:
#2250

The frontend PR was initially made off the backend PR branch. I merged
the backend PR branch to main. Then merged the frontend PR into the
backend PR branch. Now merging into main again.

---------

Co-authored-by: Ben Gazzard <ben@dartoxia.com>
  • Loading branch information
tedim52 and Dartoxian committed Mar 13, 2024
1 parent 2292010 commit 7fa27ed
Show file tree
Hide file tree
Showing 41 changed files with 2,149 additions and 1,021 deletions.
2 changes: 1 addition & 1 deletion enclave-manager/web/packages/app/package.json
Expand Up @@ -9,7 +9,7 @@
"enclave-manager-sdk": "file:../../../api/typescript",
"html-react-parser": "^4.2.2",
"js-cookie": "^3.0.5",
"kurtosis-cloud-indexer-sdk": "^0.0.2",
"kurtosis-cloud-indexer-sdk": "^0.0.31",
"kurtosis-ui-components": "0.88.5",
"react-error-boundary": "^4.0.11",
"react-hook-form": "^7.47.0",
Expand Down
Expand Up @@ -5,6 +5,7 @@ import {
RunStarlarkPackageArgs,
RunStarlarkScriptArgs,
ServiceInfo,
StarlarkPackagePlanYamlArgs,
} from "enclave-manager-sdk/build/api_container_service_pb";
import {
CreateEnclaveArgs,
Expand All @@ -24,6 +25,7 @@ import {
InspectFilesArtifactContentsRequest,
RunStarlarkPackageRequest,
RunStarlarkScriptRequest,
StarlarkPackagePlanYamlArgs as StarlarkPackagePlanYamlArgsRequest,
} from "enclave-manager-sdk/build/kurtosis_enclave_manager_api_pb";
import { assertDefined, asyncResult, isDefined, RemoveFunctions } from "kurtosis-ui-components";
import { EnclaveFullInfo } from "../../emui/enclaves/types";
Expand Down Expand Up @@ -184,15 +186,15 @@ export abstract class KurtosisClient {
}

async createEnclave(
enclaveName: string,
apiContainerLogLevel: string,
enclaveName?: string,
apiContainerLogLevel?: string,
productionMode?: boolean,
apiContainerVersionTag?: string,
) {
return asyncResult(() => {
const request = new CreateEnclaveArgs({
enclaveName,
apiContainerLogLevel,
enclaveName: enclaveName || "",
apiContainerLogLevel: apiContainerLogLevel || "info",
mode: productionMode ? EnclaveMode.PRODUCTION : EnclaveMode.TEST,
apiContainerVersionTag: apiContainerVersionTag || "",
});
Expand Down Expand Up @@ -237,4 +239,22 @@ export abstract class KurtosisClient {
});
return this.client.runStarlarkScript(request, this.getHeaderOptions());
}

async getStarlarkPackagePlanYaml(
apicInfo: RemoveFunctions<EnclaveAPIContainerInfo>,
packageId: string,
args: Record<string, any>,
) {
return asyncResult(() => {
const request = new StarlarkPackagePlanYamlArgsRequest({
apicIpAddress: apicInfo.bridgeIpAddress,
apicPort: apicInfo.grpcPortInsideEnclave,
starlarkPackagePlanYamlArgs: new StarlarkPackagePlanYamlArgs({
packageId: packageId,
serializedParams: JSON.stringify(args),
}),
});
return this.client.getStarlarkPackagePlanYaml(request, this.getHeaderOptions());
});
}
}
@@ -0,0 +1,193 @@
import { SmallCloseIcon } from "@chakra-ui/icons";
import {
Button,
Flex,
Icon,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
Spinner,
Text,
} from "@chakra-ui/react";
import { KurtosisPackage } from "kurtosis-cloud-indexer-sdk";
import {
FindCommand,
isDefined,
KurtosisAlert,
KurtosisPackageCardHorizontal,
parsePackageUrl,
useKeyboardAction,
useSavedPackages,
} from "kurtosis-ui-components";
import { debounce } from "lodash";
import { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { FiSearch } from "react-icons/fi";
import { useCatalogContext } from "../../../catalog/CatalogContext";

type ExactMatchState =
| { type: "loading"; url: string }
| { type: "loaded"; package: KurtosisPackage }
| { type: "error"; error: string };

type PackageSelectorProps = {
onPackageSelected: (kurtosisPackage: KurtosisPackage) => void;
};
export const PackageSelector = ({ onPackageSelected }: PackageSelectorProps) => {
const searchRef = useRef<HTMLInputElement>(null);
const [searchTerm, setSearchTerm] = useState("");

const [exactMatch, setExactMatch] = useState<ExactMatchState>();
const { catalog, getSinglePackage } = useCatalogContext();

const checkSinglePackageDebounced = useMemo(
() =>
debounce(
async (packageName: string) => {
const singlePackageResult = await getSinglePackage(packageName);
if (singlePackageResult.isErr) {
setExactMatch({ type: "error", error: singlePackageResult.error });
return;
}
if (isDefined(singlePackageResult.value.package)) {
setExactMatch({ type: "loaded", package: singlePackageResult.value.package });
}
},
1000,
{ trailing: true, leading: false },
),
[getSinglePackage],
);

const startCheckSinglePackage = useCallback(
async (packageName: string) => {
let isKurtosisPackageName = false;
try {
parsePackageUrl(packageName);
isKurtosisPackageName = true;
} catch (error: any) {
// This packageName isn't a kurtosis package url
}
if (isKurtosisPackageName) {
setExactMatch({ type: "loading", url: packageName });
checkSinglePackageDebounced(packageName);
} else {
setExactMatch(undefined);
}
},
[checkSinglePackageDebounced],
);

const searchResults = useMemo(
() =>
catalog.map((catalog) =>
catalog.packages.filter(
(kurtosisPackage) => kurtosisPackage.name.toLowerCase().indexOf(searchTerm.toLowerCase()) >= 0,
),
),
[catalog, searchTerm],
);

const { savedPackages } = useSavedPackages();

const handleSearchTermChange = async (e: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
};

useKeyboardAction(useMemo(() => ({ find: () => searchRef.current?.focus() }), [searchRef]));

useEffect(() => {
startCheckSinglePackage(searchTerm);
}, [startCheckSinglePackage, searchTerm]);

if (searchResults.isErr) {
return <KurtosisAlert message={"Unable to load kurtosis packages"} details={searchResults.error} />;
}

return (
<>
<InputGroup variant={"solid"} width={"100%"} color={"gray.150"}>
<InputLeftElement>
<Icon as={FiSearch} />
</InputLeftElement>
<Input
ref={searchRef}
value={searchTerm}
bgColor={"gray.850"}
onChange={handleSearchTermChange}
placeholder={"Search"}
autoFocus
/>
<InputRightElement w={"unset"} mr={"8px"}>
{searchTerm.length > 0 ? (
<Button variant="ghost" size={"xs"} rightIcon={<SmallCloseIcon />} onClick={() => setSearchTerm("")}>
Clear
</Button>
) : (
<FindCommand whiteSpace={"nowrap"} pr={"10px"} />
)}
</InputRightElement>
</InputGroup>
{isDefined(exactMatch) && (
<Flex flexDirection={"column"} gap={"10px"}>
<Text fontWeight={"semibold"} pt={"16px"} pb={"6px"}>
Exact Match
</Text>
{exactMatch.type === "loading" && (
<Flex flexDirection={"column"} alignItems={"center"}>
<Spinner />
<Text>Looking for a Kurtosis Package at {exactMatch.url}</Text>
</Flex>
)}
{exactMatch.type === "loaded" && (
<KurtosisPackageCardHorizontal
kurtosisPackage={exactMatch.package}
onClick={() => onPackageSelected(exactMatch.package)}
/>
)}
{exactMatch.type === "error" && (
<KurtosisAlert message={"Error looking up package"} details={exactMatch.error} />
)}
</Flex>
)}
{(searchTerm.length > 0 || savedPackages.length === 0) && (
<Flex flexDirection={"column"} gap={"10px"}>
<Text fontWeight={"semibold"} pt={"16px"} pb={"6px"}>
{searchTerm.length === 0 ? "All Packages" : "Search Results"}
</Text>
{searchResults.value.map((kurtosisPackage) => (
<KurtosisPackageCardHorizontal
key={kurtosisPackage.name}
kurtosisPackage={kurtosisPackage}
onClick={() => onPackageSelected(kurtosisPackage)}
/>
))}
</Flex>
)}
{searchTerm.length === 0 && savedPackages.length > 0 && (
<Flex flexDirection={"column"} gap={"10px"}>
<Text fontWeight={"semibold"} pt={"16px"} pb={"6px"}>
Saved
</Text>
{savedPackages.map((kurtosisPackage) => (
<KurtosisPackageCardHorizontal
key={kurtosisPackage.name}
kurtosisPackage={kurtosisPackage}
onClick={() => onPackageSelected(kurtosisPackage)}
/>
))}
<Text fontWeight={"semibold"} pt={"16px"} pb={"6px"}>
All Packages
</Text>
{searchResults.value.map((kurtosisPackage) => (
<KurtosisPackageCardHorizontal
key={kurtosisPackage.name}
kurtosisPackage={kurtosisPackage}
onClick={() => onPackageSelected(kurtosisPackage)}
/>
))}
</Flex>
)}
</>
);
};

0 comments on commit 7fa27ed

Please sign in to comment.