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

feat: emui catalog overview #1865

Merged
merged 7 commits into from
Nov 29, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions enclave-manager/web/src/components/KeyboardCommands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,17 @@ export const FindCommand = (props: TextProps) => {
</Text>
);
};

export const OmniboxCommand = (props: TextProps) => {
let text = "^K";

if (navigator.userAgent.indexOf("Mac") > -1) {
text = "⌘K";
}

return (
<Text as={"span"} {...props}>
{text}
</Text>
);
};
86 changes: 79 additions & 7 deletions enclave-manager/web/src/components/KurtosisBreadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,28 @@ import {
import { ReactElement, useMemo } from "react";
import { BsCaretDownFill } from "react-icons/bs";
import { Link, Params, UIMatch, useMatches } from "react-router-dom";
import { EmuiAppState, useEmuiAppContext } from "../emui/EmuiAppContext";
import { EnclavesState, useEnclavesContext } from "../emui/enclaves/EnclavesContext";
import { isDefined } from "../utils";
import { RemoveFunctions } from "../utils/types";
import { BREADCRUMBS_HEIGHT, MAIN_APP_MAX_WIDTH_WITHOUT_PADDING } from "./theme/constants";

export type KurtosisBreadcrumbsHandle = {
crumb?: (state: RemoveFunctions<EmuiAppState>, params: Params<string>) => KurtosisBreadcrumb | KurtosisBreadcrumb[];
extraControls?: (state: RemoveFunctions<EmuiAppState>, params: Params<string>) => ReactElement | null;
type KurtosisBaseBreadcrumbsHandle = {
type: string;
};

export type KurtosisEnclavesBreadcrumbsHandle = KurtosisBaseBreadcrumbsHandle & {
type: "enclavesHandle";
crumb?: (state: RemoveFunctions<EnclavesState>, params: Params<string>) => KurtosisBreadcrumb | KurtosisBreadcrumb[];
extraControls?: (state: RemoveFunctions<EnclavesState>, params: Params<string>) => ReactElement | null;
};

export type KurtosisCatalogBreadcrumbsHandle = {
type: "catalogHandle";
crumb?: () => KurtosisBreadcrumb | KurtosisBreadcrumb[];
};

export type KurtosisBreadcrumbsHandle = KurtosisEnclavesBreadcrumbsHandle | KurtosisCatalogBreadcrumbsHandle;

type KurtosisBreadcrumbMenuItem = {
name: string;
destination: string;
Expand All @@ -39,11 +51,42 @@ export type KurtosisBreadcrumb = {
};

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

const matches = useMatches() as UIMatch<object, KurtosisBreadcrumbsHandle>[];

const handlers = new Set(matches.map((match) => match.handle?.type).filter(isDefined));
if (handlers.size === 0) {
throw Error(`Currently routes with no breadcrumb handles are not supported`);
}
if (handlers.size > 1) {
throw Error(`Routes with multiple breadcrumb handles are not supported.`);
}
const handleType = [...handlers][0];
const isEnclavesMatches = (
matches: UIMatch<object, KurtosisBreadcrumbsHandle>[],
onlyType: KurtosisBreadcrumbsHandle["type"],
): matches is UIMatch<object, KurtosisEnclavesBreadcrumbsHandle>[] => onlyType === "enclavesHandle";
const isCatalogMatches = (
matches: UIMatch<object, KurtosisBreadcrumbsHandle>[],
onlyType: KurtosisBreadcrumbsHandle["type"],
): matches is UIMatch<object, KurtosisCatalogBreadcrumbsHandle>[] => onlyType === "catalogHandle";
if (isEnclavesMatches(matches, handleType)) {
return <KurtosisEnclavesBreadcrumbs matches={matches} />;
}
if (isCatalogMatches(matches, handleType)) {
return <KurtosisCatalogBreadcrumbs matches={matches} />;
}

throw new Error(`Unable to handle breadcrumbs of type ${handleType}`);
};

type KurtosisEnclavesBreadcrumbsProps = {
matches: UIMatch<object, KurtosisEnclavesBreadcrumbsHandle>[];
};

const KurtosisEnclavesBreadcrumbs = ({ matches }: KurtosisEnclavesBreadcrumbsProps) => {
const { enclaves, filesAndArtifactsByEnclave, starlarkRunsByEnclave, servicesByEnclave, starlarkRunningInEnclaves } =
useEnclavesContext();

const matchCrumbs = useMemo(
() =>
matches.flatMap((match) => {
Expand Down Expand Up @@ -100,6 +143,35 @@ export const KurtosisBreadcrumbs = () => {
],
);

return <KurtosisBreadcrumbsImpl matchCrumbs={matchCrumbs} extraControls={extraControls} />;
};

type KurtosisCatalogBreadcrumbsProps = {
matches: UIMatch<object, KurtosisCatalogBreadcrumbsHandle>[];
};

const KurtosisCatalogBreadcrumbs = ({ matches }: KurtosisCatalogBreadcrumbsProps) => {
const matchCrumbs = useMemo(
() =>
matches.flatMap((match) => {
if (isDefined(match.handle?.crumb)) {
const r = match.handle.crumb();
return Array.isArray(r) ? r : [r];
}
return [];
}),
[matches],
);

return <KurtosisBreadcrumbsImpl matchCrumbs={matchCrumbs} />;
};

type KurtosisBreadcrumbsImplProps = {
matchCrumbs: KurtosisBreadcrumb[];
extraControls?: ReactElement[];
};

const KurtosisBreadcrumbsImpl = ({ matchCrumbs, extraControls }: KurtosisBreadcrumbsImplProps) => {
return (
<Flex h={BREADCRUMBS_HEIGHT}>
<Flex w={MAIN_APP_MAX_WIDTH_WITHOUT_PADDING} alignItems={"center"} justifyContent={"space-between"}>
Expand Down
5 changes: 3 additions & 2 deletions enclave-manager/web/src/components/KurtosisThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const theme = extendTheme({
},
colors: {
kurtosisGreen: {
50: "#00371E",
100: "#005e11",
200: "#008c19",
300: "#00bb22",
Expand Down Expand Up @@ -126,8 +127,8 @@ const theme = extendTheme({
};
},
solid: defineStyle((props) => ({
_hover: { bg: "gray.700" },
_active: { bg: "gray.700" },
_hover: { bg: "gray.600" },
_active: { bg: "gray.600" },
color: `${props.colorScheme}.400`,
bg: "gray.700",
})),
Expand Down
12 changes: 12 additions & 0 deletions enclave-manager/web/src/components/PageTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Heading, HeadingProps } from "@chakra-ui/react";
import { PropsWithChildren } from "react";

type PageTitleProps = PropsWithChildren<HeadingProps>;

export const PageTitle = ({ children, ...headingProps }: PageTitleProps) => {
return (
<Heading fontSize={"lg"} fontWeight={"medium"} pl={"8px"} {...headingProps}>
{children}
</Heading>
);
};
59 changes: 59 additions & 0 deletions enclave-manager/web/src/components/catalog/KurtosisPackageCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Flex, Icon, Image, Text } from "@chakra-ui/react";
import { IoStarSharp } from "react-icons/io5";
import { useKurtosisClient } from "../../client/enclaveManager/KurtosisClientContext";
import { KurtosisPackage } from "../../client/packageIndexer/api/kurtosis_package_indexer_pb";
import { isDefined } from "../../utils";
import { RunKurtosisPackageButton } from "./widgets/RunKurtosisPackageButton";
import { SaveKurtosisPackageButton } from "./widgets/SaveKurtosisPackageButton";

type KurtosisPackageCardProps = { kurtosisPackage: KurtosisPackage; onClick?: () => void };

export const KurtosisPackageCard = ({ kurtosisPackage }: KurtosisPackageCardProps) => {
const client = useKurtosisClient();

const name = isDefined(kurtosisPackage.repositoryMetadata)
? `${kurtosisPackage.repositoryMetadata.name} ${kurtosisPackage.repositoryMetadata.rootPath.split("/").join(" ")}`
: kurtosisPackage.name;

return (
<Flex
h={"168px"}
p={"0 24px"}
bg={"gray.900"}
borderColor={"whiteAlpha.300"}
borderWidth={"1px"}
borderStyle={"solid"}
borderRadius={"6px"}
flexDirection={"column"}
gap={"16px"}
justifyContent={"center"}
alignItems={"center"}
>
<Flex h={"80px"} gap={"16px"}>
<Image bg={"black"} src={`${client.getBaseApplicationUrl()}/logo.png`} borderRadius={"6px"} />
<Flex flexDirection={"column"} flex={"1"} justifyContent={"space-between"}>
<Text noOfLines={2} fontSize={"lg"} textTransform={"capitalize"}>
{name}
</Text>
<Flex justifyContent={"space-between"} fontSize={"xs"}>
<Text as={"span"} textTransform={"capitalize"}>
{kurtosisPackage.repositoryMetadata?.owner.replaceAll("-", " ") || "Unknown owner"}
</Text>
<Flex gap={"4px"} alignItems={"center"}>
{kurtosisPackage.stars > 0 && (
<>
<Icon color="gray.500" as={IoStarSharp} />
<Text as={"span"}>{kurtosisPackage.stars.toString()}</Text>
</>
)}
</Flex>
</Flex>
</Flex>
</Flex>
<Flex gap={"16px"} width={"100%"}>
<SaveKurtosisPackageButton kurtosisPackage={kurtosisPackage} flex={"1"} />
<RunKurtosisPackageButton kurtosisPackage={kurtosisPackage} flex={"1"} />
</Flex>
</Flex>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Grid, GridItem } from "@chakra-ui/react";
import { memo } from "react";
import { KurtosisPackage } from "../../client/packageIndexer/api/kurtosis_package_indexer_pb";
import { KurtosisPackageCard } from "./KurtosisPackageCard";

type KurtosisPackageCardGridProps = {
packages: KurtosisPackage[];
onPackageClicked?: (kurtosisPackage: KurtosisPackage) => void;
};

export const KurtosisPackageCardGrid = memo(({ packages, onPackageClicked }: KurtosisPackageCardGridProps) => {
return (
<Grid gridTemplateColumns={"1fr 1fr 1fr"} columnGap={"32px"} rowGap={"32px"}>
{packages.map((kurtosisPackage) => (
<GridItem
key={kurtosisPackage.url}
onClick={onPackageClicked ? () => onPackageClicked(kurtosisPackage) : undefined}
>
<KurtosisPackageCard kurtosisPackage={kurtosisPackage} />
</GridItem>
))}
</Grid>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Button, ButtonProps } from "@chakra-ui/react";
import { useState } from "react";
import { FiDownload } from "react-icons/fi";
import { KurtosisPackage } from "../../../client/packageIndexer/api/kurtosis_package_indexer_pb";
import { EnclavesContextProvider } from "../../../emui/enclaves/EnclavesContext";
import { ConfigureEnclaveModal } from "../../enclaves/modals/ConfigureEnclaveModal";

type RunKurtosisPackageButtonProps = ButtonProps & {
kurtosisPackage: KurtosisPackage;
};

export const RunKurtosisPackageButton = ({ kurtosisPackage, ...buttonProps }: RunKurtosisPackageButtonProps) => {
const [configuringEnclave, setConfiguringEnclave] = useState(false);

return (
<>
<Button
size={"xs"}
colorScheme={"kurtosisGreen"}
leftIcon={<FiDownload />}
onClick={() => setConfiguringEnclave(true)}
{...buttonProps}
>
Run
</Button>
{configuringEnclave && (
<EnclavesContextProvider skipInitialLoad>
<ConfigureEnclaveModal
isOpen={true}
onClose={() => setConfiguringEnclave(false)}
kurtosisPackage={kurtosisPackage}
/>
</EnclavesContextProvider>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Button, ButtonProps } from "@chakra-ui/react";
import { memo, MouseEventHandler, useCallback, useMemo } from "react";
import { MdBookmarkAdd } from "react-icons/md";
import { KurtosisPackage } from "../../../client/packageIndexer/api/kurtosis_package_indexer_pb";
import { useCatalogContext } from "../../../emui/catalog/CatalogContext";

type SaveKurtosisPackageButtonProps = ButtonProps & {
kurtosisPackage: KurtosisPackage;
};

export const SaveKurtosisPackageButton = ({ kurtosisPackage, ...buttonProps }: SaveKurtosisPackageButtonProps) => {
const { savedPackages, togglePackageSaved } = useCatalogContext();
const isPackageSaved = useMemo(
() => savedPackages.some((p) => p.name === kurtosisPackage.name),
[savedPackages, kurtosisPackage],
);

const handleClick = useCallback(() => togglePackageSaved(kurtosisPackage), [togglePackageSaved, kurtosisPackage]);

return <SaveKurtosisPackageButtonMemo isPackageSaved={isPackageSaved} onClick={handleClick} {...buttonProps} />;
};

type SaveKurtosisPackageButtonMemoProps = Omit<SaveKurtosisPackageButtonProps, "kurtosisPackage"> & {
isPackageSaved: boolean;
onClick: MouseEventHandler;
};

// this is memo'd to skip unecessary renders, which effectively doubles the performance of this component (as it is
// displayed a lot.
const SaveKurtosisPackageButtonMemo = memo(
({ isPackageSaved, onClick, ...buttonProps }: SaveKurtosisPackageButtonMemoProps) => {
return (
<Button
size={"xs"}
variant={"solid"}
colorScheme={isPackageSaved ? "kurtosisGreen" : "darkBlue"}
leftIcon={<MdBookmarkAdd />}
onClick={onClick}
bg={isPackageSaved ? "#18371E" : undefined}
{...buttonProps}
>
{isPackageSaved ? "Saved" : "Save"}
</Button>
);
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export const LogViewer = ({
isDisabled={logLines.length === 0}
isIconButton
aria-label={"Copy logs"}
color={"gray.100"}
/>
<DownloadButton
valueToDownload={getLogsValue}
Expand All @@ -193,6 +194,7 @@ export const LogViewer = ({
isDisabled={logLines.length === 0}
isIconButton
aria-label={"Download logs"}
color={"gray.100"}
/>
</ButtonGroup>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { SubmitHandler } from "react-hook-form";
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 { useEnclavesContext } from "../../../emui/enclaves/EnclavesContext";
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 @@ -51,7 +51,7 @@ export const ConfigureEnclaveModal = ({
existingEnclave,
}: ConfigureEnclaveModalProps) => {
const kurtosisClient = useKurtosisClient();
const { createEnclave, runStarlarkPackage } = useEmuiAppContext();
const { createEnclave, runStarlarkPackage } = useEnclavesContext();
const navigator = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export const PortsTable = ({ privatePorts, publicPorts, publicIp }: PortsTablePr
<DataTable
columns={columns}
data={getPortTableRows(privatePorts, publicPorts, publicIp)}
defaultSorting={[{ id: "number", desc: true }]}
defaultSorting={[{ id: "port", desc: true }]}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button, ButtonProps, Tooltip } from "@chakra-ui/react";
import { useState } from "react";
import { FiTrash2 } from "react-icons/fi";
import { useNavigate } from "react-router-dom";
import { useEmuiAppContext } from "../../../emui/EmuiAppContext";
import { useEnclavesContext } from "../../../emui/enclaves/EnclavesContext";
import { EnclaveFullInfo } from "../../../emui/enclaves/types";
import { KurtosisAlertModal } from "../../KurtosisAlertModal";

Expand All @@ -11,7 +11,7 @@ type DeleteEnclavesButtonProps = ButtonProps & {
};

export const DeleteEnclavesButton = ({ enclaves, ...buttonProps }: DeleteEnclavesButtonProps) => {
const { destroyEnclaves } = useEmuiAppContext();
const { destroyEnclaves } = useEnclavesContext();
const navigator = useNavigate();

const [showModal, setShowModal] = useState(false);
Expand Down
Loading