Skip to content

Commit

Permalink
feat: emui catalog overview (#1865)
Browse files Browse the repository at this point in the history
## Description:
This PR implements the first pass of the emui catalog. Additionally:
* It has a minor tweak to log viewer button colours
* Refactors the KurtosisBreadcrumbs to no longer be dependent on being
on an `enclaves` route
* Fixes the application max width to be 1440px, not the 1320px I had
incorrectly set it to.

### Demo


https://github.com/kurtosis-tech/kurtosis/assets/4419574/dfc5a489-3c6f-44d0-b27f-ca4d7d61786f

## Is this change user facing?
YES

## References (if applicable):
* Figma
  • Loading branch information
Dartoxian authored Nov 29, 2023
1 parent f204888 commit 2f118d9
Show file tree
Hide file tree
Showing 36 changed files with 649 additions and 148 deletions.
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

0 comments on commit 2f118d9

Please sign in to comment.