diff --git a/src/app/Header.tsx b/src/app/Header.tsx index ce0518c45..226c973f6 100644 --- a/src/app/Header.tsx +++ b/src/app/Header.tsx @@ -13,7 +13,7 @@ export function OverviewHeader() { {serverSettings ? ( diff --git a/src/app/artifacts/Fragments.tsx b/src/app/artifacts/Fragments.tsx index 91cf37f26..8e89067ab 100644 --- a/src/app/artifacts/Fragments.tsx +++ b/src/app/artifacts/Fragments.tsx @@ -7,10 +7,10 @@ export function InfoBox() { return (
-

This is a ZenML Cloud feature.

+

This is a ZenML Pro feature.

- Upgrade to ZenML Cloud to access the Artifact Control Plane and interact with your - artifacts in the Dashboard. + Upgrade to ZenML Pro to access the Artifact Control Plane and interact with your artifacts + in the Dashboard.

diff --git a/src/app/artifacts/page.tsx b/src/app/artifacts/page.tsx index 9e7f20d1b..0f7c901a4 100644 --- a/src/app/artifacts/page.tsx +++ b/src/app/artifacts/page.tsx @@ -10,13 +10,13 @@ export default function ModelsPage() {

Artifacts

- Cloud + Pro
diff --git a/src/app/models/Fragments.tsx b/src/app/models/Fragments.tsx index 4253cfd90..b4d9c03ef 100644 --- a/src/app/models/Fragments.tsx +++ b/src/app/models/Fragments.tsx @@ -7,9 +7,9 @@ export function InfoBox() { return (
-

This is a ZenML Cloud feature.

+

This is a ZenML Pro feature.

- Upgrade to ZenML Cloud to access the Model Control Plane and interact with your models in + Upgrade to ZenML Pro to access the Model Control Plane and interact with your models in the Dashboard.

diff --git a/src/app/models/page.tsx b/src/app/models/page.tsx index ca6f733e9..b5635800f 100644 --- a/src/app/models/page.tsx +++ b/src/app/models/page.tsx @@ -23,13 +23,13 @@ export default function ModelsPage() {

Models

- Cloud + Pro
diff --git a/src/app/stacks/ActionsDropdown.tsx b/src/app/stacks/ActionsDropdown.tsx new file mode 100644 index 000000000..e1ea39ed5 --- /dev/null +++ b/src/app/stacks/ActionsDropdown.tsx @@ -0,0 +1,79 @@ +import HorizontalDots from "@/assets/icons/dots-horizontal.svg?react"; +import Edit from "@/assets/icons/edit.svg?react"; +import Trash from "@/assets/icons/trash.svg?react"; +import { DialogItem } from "@/components/dialog/DialogItem"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger +} from "@zenml-io/react-component-library"; +import { useRef, useState } from "react"; +import { DeleteStackDialog, UpdateStackDialog } from "./DialogItems"; + +type Props = { name: string }; +export function StackActionsMenu({ name }: Props) { + const [hasOpenDialog, setHasOpenDialog] = useState(false); + const [dropdownOpen, setDropdownOpen] = useState(false); + + const dropdownTriggerRef = useRef(null); + const focusRef = useRef(null); + + function handleDialogItemSelect() { + focusRef.current = dropdownTriggerRef.current; + } + + function handleDialogItemOpenChange(open: boolean) { + setHasOpenDialog(open); + if (open === false) { + setDropdownOpen(false); + } + } + + return ( + + + + + + + + + ); +} diff --git a/src/app/stacks/DialogItems.tsx b/src/app/stacks/DialogItems.tsx new file mode 100644 index 000000000..d9dc95a5e --- /dev/null +++ b/src/app/stacks/DialogItems.tsx @@ -0,0 +1,88 @@ +import { DialogContent, DialogHeader, DialogTitle } from "@zenml-io/react-component-library"; +import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react"; +import { InfoBox as InfoboxPrimitive } from "@/components/Infobox"; +import { Codesnippet } from "@/components/CodeSnippet"; + +type Props = { + closeModal?: () => void; + name: string; +}; + +export const UpdateStackDialog = forwardRef< + ElementRef, + ComponentPropsWithoutRef & Props +>(({ closeModal, name, ...rest }, ref) => { + return ( + + + Update Stack + +
+ +
+

Update a stack

+ +
+
+
+ ); +}); + +UpdateStackDialog.displayName = "UpdateStackDialog"; + +export const DeleteStackDialog = forwardRef< + ElementRef, + ComponentPropsWithoutRef & Props +>(({ closeModal, name, ...rest }, ref) => { + return ( + + + Delete Stack + +
+ +
+

Delete a stack

+ +
+
+
+ ); +}); + +DeleteStackDialog.displayName = "DeleteStackDialog"; + +type InfoProps = { + action: "delete" | "update" | "describe"; +}; +export function Infobox({ action }: InfoProps) { + function getAction() { + switch (action) { + case "delete": + return "delete"; + case "update": + return "update"; + case "describe": + return "get details of"; + } + } + + return ( + +
+
+

+ We are working on the new Stacks experience. +

+

+ Meanwhile you can use the CLI to {getAction()} your stack. +

+
+
+
+ ); +} diff --git a/src/app/stacks/columns.tsx b/src/app/stacks/columns.tsx index a00f8a911..3ab57f5a2 100644 --- a/src/app/stacks/columns.tsx +++ b/src/app/stacks/columns.tsx @@ -5,6 +5,8 @@ import { Stack } from "@/types/stack"; import { User } from "@/types/user"; import { ColumnDef } from "@tanstack/react-table"; import { Avatar, AvatarFallback } from "@zenml-io/react-component-library"; +import { StackActionsMenu } from "./ActionsDropdown"; +import { StackSheet } from "@/components/stacks/Sheet"; export function getStackColumns(): ColumnDef[] { return [ @@ -21,7 +23,9 @@ export function getStackColumns(): ColumnDef[] {
-

{name}

+ +

{name}

+

{id.split("-")[0]}

@@ -51,6 +55,14 @@ export function getStackColumns(): ColumnDef[] { if (!author) return null; return ; } + }, + { + id: "actions", + header: "", + accessorKey: "name", + cell: ({ getValue }) => { + return ()} />; + } } ]; } diff --git a/src/app/stacks/create/new-infrastructure/Providers/AWS.tsx b/src/app/stacks/create/new-infrastructure/Providers/AWS.tsx index 7d3fa5052..e9e4c1b82 100644 --- a/src/app/stacks/create/new-infrastructure/Providers/AWS.tsx +++ b/src/app/stacks/create/new-infrastructure/Providers/AWS.tsx @@ -1,16 +1,11 @@ import { Tick } from "@/components/Tick"; import { Avatar, AvatarFallback, Spinner } from "@zenml-io/react-component-library"; import { ComponentListItem, ProviderComponents } from "."; -import { ComponentBadge } from "../ComponentBadge"; +import { ComponentBadge } from "../../../../../components/stack-components/ComponentBadge"; import { PermissionsCard } from "./PermissionsCard"; type Props = ProviderComponents; -export const awsPrizes = { - orchestratorCosts: "$0.45", - storageCosts: "$4.90" -}; - export function AWSComponents({ stackName, isLoading, diff --git a/src/app/stacks/create/new-infrastructure/Providers/GCP.tsx b/src/app/stacks/create/new-infrastructure/Providers/GCP.tsx index faf6b6c7d..fbaa01abc 100644 --- a/src/app/stacks/create/new-infrastructure/Providers/GCP.tsx +++ b/src/app/stacks/create/new-infrastructure/Providers/GCP.tsx @@ -1,16 +1,17 @@ +import ConfigIcon from "@/assets/icons/logs.svg?react"; +import { Codesnippet } from "@/components/CodeSnippet"; +import { InfoBox } from "@/components/Infobox"; import { Tick } from "@/components/Tick"; -import { Avatar, AvatarFallback, Spinner } from "@zenml-io/react-component-library"; +import { stackQueries } from "@/data/stacks"; +import { useQuery } from "@tanstack/react-query"; +import { Avatar, AvatarFallback, Skeleton, Spinner } from "@zenml-io/react-component-library"; import { ComponentListItem, ProviderComponents } from "."; -import { ComponentBadge } from "../ComponentBadge"; +import { ComponentBadge } from "../../../../../components/stack-components/ComponentBadge"; +import { useNewInfraFormContext } from "../NewInfraFormContext"; import { PermissionsCard } from "./PermissionsCard"; type Props = ProviderComponents; -export const gcpPrizes = { - orchestratorCosts: "$0.27", - storageCosts: "$4.60" -}; - export function GcpComponents({ stackName, isLoading, @@ -36,8 +37,8 @@ export function GcpComponents({ subtitle={components?.connector?.id || "Manage access to GCP resources"} badge={Service Connector} img={{ - src: "https://public-flavor-logos.s3.eu-central-1.amazonaws.com/service_connector/iam.webp", - alt: "Service connector logo" + src: "https://public-flavor-logos.s3.eu-central-1.amazonaws.com/service_connector/gcp-iam.webp", + alt: "Service Account logo" }} /> {displayPermissions && } @@ -81,6 +82,65 @@ export function GcpComponents({ }} />
+
+ Image Builder} + img={{ + src: "https://public-flavor-logos.s3.eu-central-1.amazonaws.com/image_builder/gcp.png", + alt: "Cloud Build logo" + }} + /> +
); } + +export function GCPWarning() { + return ( + + The Cloud Shell session will warn you that the ZenML GitHub repository is untrusted. We + recommend that you review the contents of the repository and then check the Trust repo + checkbox to proceed with the deployment, otherwise the Cloud Shell session will not be + authenticated to access your GCP projects. + + ); +} + +export function GCPCodesnippet() { + const { data } = useNewInfraFormContext(); + const deploymentConfig = useQuery({ + ...stackQueries.stackDeploymentConfig({ + provider: "gcp", + stack_name: data.stackName!, + location: data.location + }) + }); + if (deploymentConfig.isError) return null; + if (deploymentConfig.isPending) return ; + + return ( +
+
+

+ + Configuration +

+

+ You will be asked to provide the following configuration values during the deployment + process. +

+
+ +
+ ); +} diff --git a/src/app/stacks/create/new-infrastructure/Providers/index.tsx b/src/app/stacks/create/new-infrastructure/Providers/index.tsx index 3c9a08d37..c1b1fb7f1 100644 --- a/src/app/stacks/create/new-infrastructure/Providers/index.tsx +++ b/src/app/stacks/create/new-infrastructure/Providers/index.tsx @@ -5,8 +5,8 @@ import { StackDeploymentProvider } from "@/types/stack"; import { Box, Spinner } from "@zenml-io/react-component-library"; import { ReactNode } from "react"; import { useNewInfraFormContext } from "../NewInfraFormContext"; -import { AWSComponents, awsPrizes } from "./AWS"; -import { gcpPrizes } from "./GCP"; +import { AWSComponents } from "./AWS"; +import { GcpComponents } from "./GCP"; export type ProviderComponents = { stackName: string; @@ -18,6 +18,7 @@ export type ProviderComponents = { artifactStore?: Component; registry?: Component; orchestrator?: Component; + imageBuilder?: Component; }; }; @@ -32,8 +33,8 @@ export function CloudComponents({ componentProps, type }: Props) { switch (type) { case "aws": return ; - // case "gcp": - // return ; + case "gcp": + return ; } } @@ -72,14 +73,22 @@ export function ComponentListItem({ export function EstimateCosts() { const { data } = useNewInfraFormContext(); - gcpPrizes; - function getPrizes() { + + function PricingCalculatorLink() { + let link = "#"; switch (data.provider) { case "aws": - return awsPrizes; - // case "gcp": - // return gcpPrizes; + link = "https://calculator.aws/#/"; + break; + case "gcp": + link = "https://cloud.google.com/products/calculator"; } + + return ( + + official pricing calculator + + ); } return ( @@ -99,25 +108,11 @@ export function EstimateCosts() {

- Processing jobs:{" "} - - {getPrizes()?.orchestratorCosts} - {" "} - per hour -

-

- 200GB of general storage:{" "} - - {getPrizes()?.storageCosts} - {" "} - per month + A small training job would cost around:{" "} + $0.60

- {/*

- An average processing example would cost:{" "} - $0.3112 -

*/} -

- Use the official pricing Calculator for a detailed estimate +

+ Please use the for a detailed estimate

diff --git a/src/app/stacks/create/new-infrastructure/Steps/Configuration/Partials.tsx b/src/app/stacks/create/new-infrastructure/Steps/Configuration/Partials.tsx index f49fca93c..48b5b5573 100644 --- a/src/app/stacks/create/new-infrastructure/Steps/Configuration/Partials.tsx +++ b/src/app/stacks/create/new-infrastructure/Steps/Configuration/Partials.tsx @@ -16,7 +16,7 @@ export function Region() { Choose Your Location

- Select where you want to deploy your Cloud components for optimal performance and + Select where you want to deploy your cloud infrastructure for optimal performance and compliance.

diff --git a/src/app/stacks/create/new-infrastructure/Steps/Deploy/ButtonStep.tsx b/src/app/stacks/create/new-infrastructure/Steps/Deploy/ButtonStep.tsx index 47f3d4e20..8fbc7824a 100644 --- a/src/app/stacks/create/new-infrastructure/Steps/Deploy/ButtonStep.tsx +++ b/src/app/stacks/create/new-infrastructure/Steps/Deploy/ButtonStep.tsx @@ -1,10 +1,12 @@ import External from "@/assets/icons/link-external.svg?react"; import { InfoBox } from "@/components/Infobox"; import { CloudProviderIcon } from "@/components/ProviderIcon"; -import { useDeploymentUrl } from "@/data/stacks/stack-deployment-url"; -import { Button } from "@zenml-io/react-component-library"; +import { useQuery } from "@tanstack/react-query"; +import { Button, Skeleton } from "@zenml-io/react-component-library"; +import { stackQueries } from "../../../../../../data/stacks"; import { useNewInfraFormContext } from "../../NewInfraFormContext"; import { useNewInfraWizardContext } from "../../NewInfraWizardContext"; +import { GCPCodesnippet, GCPWarning } from "../../Providers/GCP"; export function DeployButtonPart() { const { data } = useNewInfraFormContext(); @@ -23,7 +25,9 @@ export function DeployButtonPart() { Deploy the stack from your browser by clicking the button below:

+ {data.provider === "gcp" && } + {data.provider === "gcp" && } ); } @@ -33,25 +37,36 @@ type DeploymentButtonProps = { }; export function DeploymentButton({ setTimestampBool }: DeploymentButtonProps) { const { data, setTimestamp } = useNewInfraFormContext(); + const { setIsLoading } = useNewInfraWizardContext(); - const { mutate } = useDeploymentUrl({ - onSuccess: async (data) => { - setTimestampBool && setTimestamp(new Date().toISOString().slice(0, -1)!); - setIsLoading(true); - window.open(data[0], "_blank"); - } + + const stackDeploymentConfig = useQuery({ + ...stackQueries.stackDeploymentConfig({ + provider: data.provider!, + location: data.location, + stack_name: data.stackName! + }) }); + if (stackDeploymentConfig.isError) { + return null; + } + + if (stackDeploymentConfig.isPending) { + return ; + } + function handleClick() { - mutate({ - stack_name: data.stackName!, - location: data.location, - provider: data.provider! - }); + setTimestampBool && setTimestamp(new Date().toISOString().slice(0, -1)!); + setIsLoading(true); } + return ( - ); } diff --git a/src/app/stacks/create/new-infrastructure/Steps/Deploy/ProvisioningStep.tsx b/src/app/stacks/create/new-infrastructure/Steps/Deploy/ProvisioningStep.tsx index d347e0451..861323618 100644 --- a/src/app/stacks/create/new-infrastructure/Steps/Deploy/ProvisioningStep.tsx +++ b/src/app/stacks/create/new-infrastructure/Steps/Deploy/ProvisioningStep.tsx @@ -10,6 +10,7 @@ import { useNewInfraFormContext } from "../../NewInfraFormContext"; import { useNewInfraWizardContext } from "../../NewInfraWizardContext"; import { CloudComponents } from "../../Providers"; import { DeploymentButton } from "./ButtonStep"; +import { GCPCodesnippet } from "../../Providers/GCP"; export function ProvisioningStep() { const { data, timestamp, setIsNextButtonDisabled } = useNewInfraFormContext(); @@ -50,19 +51,23 @@ export function ProvisioningStep() { function LoadingHeader() { const { data } = useNewInfraFormContext(); return ( - -
- -
-

Deploying the Stack...

-

- Follow the steps in your Cloud console to finish the setup. You can come back to check - once your components are deployed. -

+
+ + +
+ +
+

Deploying the Stack...

+

+ Follow the steps in your Cloud console to finish the setup. You can come back to check + once your components are deployed. +

+
-
- - + + + {data.provider === "gcp" && } + ); } @@ -127,3 +132,15 @@ function ItTakesLongerBox({ isReady }: { isReady: boolean }) { ); } + +function Warning() { + return ( + + Please, do not leave this screen until the stack and the components are fully + created. + + ); +} diff --git a/src/app/stacks/create/new-infrastructure/Steps/Provider.tsx b/src/app/stacks/create/new-infrastructure/Steps/Provider.tsx index 95d80d473..e258c2456 100644 --- a/src/app/stacks/create/new-infrastructure/Steps/Provider.tsx +++ b/src/app/stacks/create/new-infrastructure/Steps/Provider.tsx @@ -50,14 +50,8 @@ export function ProviderStep() { subtitle="ZenML stack with S3, ECR, and SageMaker integration" /> - + } title="GCP" subtitle="Create ZenML infrastructure using GCS, Artifact Registry, and Vertex AI" diff --git a/src/app/stacks/create/new-infrastructure/Steps/Success/SuccessStep.tsx b/src/app/stacks/create/new-infrastructure/Steps/Success/SuccessStep.tsx index 81df784bd..7c046ff1d 100644 --- a/src/app/stacks/create/new-infrastructure/Steps/Success/SuccessStep.tsx +++ b/src/app/stacks/create/new-infrastructure/Steps/Success/SuccessStep.tsx @@ -51,6 +51,10 @@ function SuccessList() { const registry = stack.stack.metadata?.components["container_registry"] as | StackComponent[] | undefined; + const imageBuilder = stack.stack.metadata?.components["image_builder"] as + | StackComponent[] + | undefined; + const components = { orchestrator: { name: orchestrators?.[0]?.name ?? "Orchestrator", @@ -67,6 +71,10 @@ function SuccessList() { connector: { name: stack.service_connector?.name as string, id: stack.service_connector?.id?.split("-")[0] ?? "" + }, + imageBuilder: { + name: imageBuilder?.[0]?.name ?? "Image Builder", + id: imageBuilder?.[0]?.id.split("-")[0] ?? "" } }; diff --git a/src/app/stacks/create/new-infrastructure/Steps/schemas.ts b/src/app/stacks/create/new-infrastructure/Steps/schemas.ts index 0efc52967..d47e68abf 100644 --- a/src/app/stacks/create/new-infrastructure/Steps/schemas.ts +++ b/src/app/stacks/create/new-infrastructure/Steps/schemas.ts @@ -1,7 +1,7 @@ import { z } from "zod"; export const providerSchema = z.object({ - provider: z.enum(["aws"]) + provider: z.enum(["aws", "gcp"]) }); export type ProviderForm = z.infer; diff --git a/src/app/stacks/create/new-infrastructure/Wizard.tsx b/src/app/stacks/create/new-infrastructure/Wizard.tsx index bc567294e..b5f15fb82 100644 --- a/src/app/stacks/create/new-infrastructure/Wizard.tsx +++ b/src/app/stacks/create/new-infrastructure/Wizard.tsx @@ -77,7 +77,7 @@ function NextButton() { function CancelButton() { return ( - ); diff --git a/src/app/stacks/create/new-infrastructure/page.tsx b/src/app/stacks/create/new-infrastructure/page.tsx index 8683aea9c..d067bec4b 100644 --- a/src/app/stacks/create/new-infrastructure/page.tsx +++ b/src/app/stacks/create/new-infrastructure/page.tsx @@ -16,7 +16,7 @@ export default function StackWithNewInfrastructurePage() { "Deploy Stack" ]} /> -
+
diff --git a/src/assets/icons/dots-horizontal.svg b/src/assets/icons/dots-horizontal.svg index 7af69f002..9cb4ac824 100644 --- a/src/assets/icons/dots-horizontal.svg +++ b/src/assets/icons/dots-horizontal.svg @@ -1,5 +1,5 @@ - + + /> diff --git a/src/assets/icons/edit.svg b/src/assets/icons/edit.svg new file mode 100644 index 000000000..ba8619dba --- /dev/null +++ b/src/assets/icons/edit.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/assets/icons/trash.svg b/src/assets/icons/trash.svg new file mode 100644 index 000000000..943be3b31 --- /dev/null +++ b/src/assets/icons/trash.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/components/breadcrumbs/Breadcrumbs.tsx b/src/components/breadcrumbs/Breadcrumbs.tsx index f6f2ad31f..91c622caf 100644 --- a/src/components/breadcrumbs/Breadcrumbs.tsx +++ b/src/components/breadcrumbs/Breadcrumbs.tsx @@ -71,7 +71,7 @@ export function Breadcrumbs() { {formatIdToTitleCase(value?.name as string)} @@ -80,7 +80,7 @@ export function Breadcrumbs() { {typeof value?.name === "string" ? ( diff --git a/src/components/breadcrumbs/SegmentsBreadcrumbs.tsx b/src/components/breadcrumbs/SegmentsBreadcrumbs.tsx index dec6d7141..0f20842af 100644 --- a/src/components/breadcrumbs/SegmentsBreadcrumbs.tsx +++ b/src/components/breadcrumbs/SegmentsBreadcrumbs.tsx @@ -6,21 +6,21 @@ export const matchSegmentWithRequest = ({ segment, data }: { segment: string; da const routeMap: { [key: string]: { [key: string]: { id?: string | null; name?: string } } } = { // Pipelines pipelines: { - pipelines: { id: data?.body?.pipeline?.id, name: "pipelines" } + pipelines: { id: data?.body?.pipeline?.id, name: "Pipelines" } }, pipeline_detail: { - pipelines: { id: data?.body?.pipeline?.id, name: "pipelines" }, + pipelines: { id: data?.body?.pipeline?.id, name: "Pipelines" }, pipeline_detail: { id: data?.name, name: data?.name } }, stacks: { - stacks: { name: "stacks" } + stacks: { name: "Stacks" } }, create_stack: { - stacks: { name: "stacks" }, + stacks: { name: "Stacks" }, create: { name: "New Stack" } }, runs: { - pipelines: { id: data?.body?.pipeline?.id, name: "pipelines" }, + pipelines: { id: data?.body?.pipeline?.id, name: "Pipelines" }, pipeline_detail: { id: data?.body?.pipeline?.name, name: data?.body?.pipeline?.name diff --git a/src/components/dialog/DialogItem.tsx b/src/components/dialog/DialogItem.tsx new file mode 100644 index 000000000..7c3fececc --- /dev/null +++ b/src/components/dialog/DialogItem.tsx @@ -0,0 +1,39 @@ +import { Dialog, DialogTrigger, DropdownMenuItem } from "@zenml-io/react-component-library"; +import { forwardRef, ReactElement, ReactNode } from "react"; + +type DialogItemProps = { + triggerChildren: ReactNode; + icon?: ReactElement; + children: ReactNode; + open?: boolean; + onSelect?: () => void; + onOpenChange?: (isOpen: boolean) => void; + // Add other props with their respective types if needed. +}; + +export const DialogItem = forwardRef, DialogItemProps>( + (props, forwardedRef) => { + const { triggerChildren, children, onSelect, onOpenChange, icon, open, ...itemProps } = props; + return ( + + + { + event.preventDefault(); + onSelect && onSelect(); + }} + > + {triggerChildren} + + + {children} + + ); + } +); + +DialogItem.displayName = "DialogItem"; diff --git a/src/components/sheet/SheetHeader.tsx b/src/components/sheet/SheetHeader.tsx new file mode 100644 index 000000000..0fc3c11ca --- /dev/null +++ b/src/components/sheet/SheetHeader.tsx @@ -0,0 +1,13 @@ +import DoubleChevronRight from "@/assets/icons/chevron-right-double.svg?react"; +import { SheetClose } from "@zenml-io/react-component-library"; + +export function SheetHeader() { + return ( +
+ + + Close + +
+ ); +} diff --git a/src/app/stacks/create/new-infrastructure/ComponentBadge.tsx b/src/components/stack-components/ComponentBadge.tsx similarity index 100% rename from src/app/stacks/create/new-infrastructure/ComponentBadge.tsx rename to src/components/stack-components/ComponentBadge.tsx diff --git a/src/components/stack-components/ComponentFallbackDialog.tsx b/src/components/stack-components/ComponentFallbackDialog.tsx new file mode 100644 index 000000000..d108596f9 --- /dev/null +++ b/src/components/stack-components/ComponentFallbackDialog.tsx @@ -0,0 +1,78 @@ +import { snakeCaseToDashCase, snakeCaseToLowerCase, snakeCaseToTitleCase } from "@/lib/strings"; +import { StackComponentType } from "@/types/components"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger +} from "@zenml-io/react-component-library"; +import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react"; +import { Codesnippet } from "../CodeSnippet"; +import { InfoBox } from "../Infobox"; + +type Props = { + name: string; + type: StackComponentType; +}; + +export const ComponentFallbackDialog = forwardRef< + ElementRef, + ComponentPropsWithoutRef & Props +>(({ name, children, type, ...rest }, ref) => { + return ( + + {children} + + + {snakeCaseToTitleCase(type || "")} + +
+ +
+

+ Describe your {snakeCaseToLowerCase(type)} +

+ +
+
+

+ Update your {snakeCaseToLowerCase(type)} +

+ +
+
+
+
+ ); +}); + +ComponentFallbackDialog.displayName = "ComponentFallbackDialog"; + +type InfoProps = { + type: StackComponentType; +}; +function Info({ type }: InfoProps) { + return ( + +
+
+

+ We are working on the new Stacks experience. +

+

+ Meanwhile you can use the CLI to manage your {snakeCaseToLowerCase(type)}. +

+
+
+
+ ); +} diff --git a/src/components/stacks/Sheet/index.tsx b/src/components/stacks/Sheet/index.tsx new file mode 100644 index 000000000..cc8db726f --- /dev/null +++ b/src/components/stacks/Sheet/index.tsx @@ -0,0 +1,146 @@ +import { SheetHeader } from "@/components/sheet/SheetHeader"; +import { flavorQueries } from "@/data/flavors"; +import { stackQueries } from "@/data/stacks"; +import { extractComponents } from "@/lib/components"; +import { snakeCaseToTitleCase } from "@/lib/strings"; +import { sanitizeUrl } from "@/lib/url"; +import { StackComponent, StackComponentType } from "@/types/components"; +import { useQuery } from "@tanstack/react-query"; +import { + Avatar, + AvatarFallback, + Box, + Sheet, + SheetContent, + SheetTrigger, + Skeleton +} from "@zenml-io/react-component-library"; +import { PropsWithChildren } from "react"; +import { CopyButton } from "../../CopyButton"; +import { ComponentBadge } from "../../stack-components/ComponentBadge"; +import { ComponentFallbackDialog } from "../../stack-components/ComponentFallbackDialog"; + +type Props = { + stackId: string; +}; + +export function StackSheet({ children, stackId }: PropsWithChildren) { + return ( + + {children} + + + + + + + ); +} + +function StackHeadline({ stackId }: Props) { + const stack = useQuery({ ...stackQueries.stackDetail(stackId) }); + + if (stack.isError) return null; + if (stack.isPending) + return ( +
+ +
+ ); + + return ( +
+ + {stack.data.name[0]} + +
+
+

{stack.data.id}

+ +
+ +
+

{stack.data.name}

+
+
+
+ ); +} + +function ComponentList({ stackId }: Props) { + const stack = useQuery({ ...stackQueries.stackDetail(stackId) }); + + if (stack.isError) return null; + if (stack.isPending) + return ( +
+ +
+ ); + + const components = extractComponents( + stack.data.metadata?.components as Record | undefined + ); + + return ( +
    + {components.map((component) => ( +
  • + +
  • + ))} +
+ ); +} + +type ComponentListItemProps = { + component: StackComponent; +}; +function ComponentListItem({ component }: ComponentListItemProps) { + return ( + +
+ +
+ + + +
+

{component.id.split("-")[0]}

+ +
+
+
+ + {snakeCaseToTitleCase(component.body?.type || "")} + +
+ ); +} + +type FlavorIconProps = { + flavor: string; + type: StackComponentType; +}; + +function FlavorIcon({ flavor, type }: FlavorIconProps) { + const flavorQuery = useQuery({ ...flavorQueries.flavorList({ name: flavor, type }) }); + + if (flavorQuery.isError) return null; + if (flavorQuery.isPending) return ; + + return ( + {`${flavor} + ); +} diff --git a/src/components/tour/Tour.tsx b/src/components/tour/Tour.tsx index 72668b58e..61ba8213d 100644 --- a/src/components/tour/Tour.tsx +++ b/src/components/tour/Tour.tsx @@ -98,7 +98,7 @@ const steps: Step[] = [ }, { content: - "Find out more about our advanced ZenML Cloud features like model and artifact management.", + "Find out more about our advanced ZenML Pro features like model and artifact management.", target: "#models-sidebar-link", title: "OSS is just the beginning", disableBeacon: true, diff --git a/src/contents/cloud-only.tsx b/src/contents/cloud-only.tsx index 641288d6f..0201339bf 100644 --- a/src/contents/cloud-only.tsx +++ b/src/contents/cloud-only.tsx @@ -2,12 +2,19 @@ import { Box, buttonVariants } from "@zenml-io/react-component-library"; const cloudOnlyFeatures = [ "Managed ZenML server on your VPC or hosted on our servers", - "Social SSO, RBAC, and User Management", - "CI/CD/CT, Artifact Control Plane and more!" + "Social SSO, RBAC, and User Management" ]; -export const modelFeatures = ["Model Control Plane Dashboard", ...cloudOnlyFeatures]; -export const artifactFeatures = ["Artifact Control Plane Dashboard", ...cloudOnlyFeatures]; +export const modelFeatures = [ + "Model Control Plane Dashboard", + ...cloudOnlyFeatures, + "CI/CD/CT, Artifact Control Plane and more!" +]; +export const artifactFeatures = [ + "Artifact Control Plane Dashboard", + ...cloudOnlyFeatures, + "CI/CD/CT, Model Control Plane and more!" +]; type CTASectionProps = { image: { @@ -21,7 +28,7 @@ export function CTASection({ features, image }: CTASectionProps) {

- Access Advanced Model Management Features with ZenML Cloud + Access Advanced Model Management Features with ZenML Pro

    {features.map((item, i) => ( diff --git a/src/data/api.ts b/src/data/api.ts index 5d6e15dbd..1bbe10d40 100644 --- a/src/data/api.ts +++ b/src/data/api.ts @@ -35,13 +35,16 @@ export const apiPaths = { }, stackDeployment: { info: "/stack-deployment/info", - url: "/stack-deployment/url", + config: "/stack-deployment/config", stack: "/stack-deployment/stack" }, stacks: { all: "/stacks", detail: (stackId: string) => `/stacks/${stackId}` }, + flavors: { + all: "/flavors" + }, steps: { detail: (stepId: string) => `/steps/${stepId}`, logs: (stepId: string) => `/steps/${stepId}/logs` diff --git a/src/data/flavors/flavors-list.ts b/src/data/flavors/flavors-list.ts new file mode 100644 index 000000000..0f13a96a5 --- /dev/null +++ b/src/data/flavors/flavors-list.ts @@ -0,0 +1,38 @@ +import { FetchError } from "@/lib/fetch-error"; +import { objectToSearchParams } from "@/lib/url"; +import { fetcher } from "../fetch"; +import { apiPaths, createApiPath } from "../api"; +import { FlavorListQueryParams, FlavorsPage } from "@/types/flavors"; +import { notFound } from "@/lib/not-found-error"; + +export async function fetchFlavors(queryParams: FlavorListQueryParams): Promise { + const url = + createApiPath(apiPaths.flavors.all) + + (queryParams ? `?${objectToSearchParams(queryParams)}` : ""); + const res = await fetcher(url, { + method: "GET", + headers: { + "Content-Type": "application/json" + } + }); + + if (res.status === 404) notFound(); + + if (!res.ok) { + const errorData: string = await res + .json() + .then((data) => { + if (Array.isArray(data.detail)) { + return data.detail[1]; + } + return data.detail; + }) + .catch(() => "Error while fetching flavors"); + throw new FetchError({ + status: res.status, + statusText: res.statusText, + message: errorData + }); + } + return res.json(); +} diff --git a/src/data/flavors/index.ts b/src/data/flavors/index.ts new file mode 100644 index 000000000..d47d1cfc1 --- /dev/null +++ b/src/data/flavors/index.ts @@ -0,0 +1,12 @@ +import { queryOptions } from "@tanstack/react-query"; +import { fetchFlavors } from "./flavors-list"; +import { FlavorListQueryParams } from "@/types/flavors"; + +export const flavorQueries = { + all: ["flavors"], + flavorList: (queryParams: FlavorListQueryParams) => + queryOptions({ + queryKey: [...flavorQueries.all, queryParams], + queryFn: async () => fetchFlavors(queryParams) + }) +}; diff --git a/src/data/stacks/index.ts b/src/data/stacks/index.ts index 00baa55b1..9bfeb0842 100644 --- a/src/data/stacks/index.ts +++ b/src/data/stacks/index.ts @@ -4,9 +4,11 @@ import { StackListQueryParams } from "@/types/stack"; import { queryOptions } from "@tanstack/react-query"; -import { fetchStacks } from "./stacklist-query"; +import { fetchStackDeploymentConfig } from "./stack-deployment-config"; import { fetchStackDeploymentInfo } from "./stack-deployment-info"; import { fetchStackDeploymentStack } from "./stack-deployment-stack"; +import { fetchStack } from "./stack-detail-query"; +import { fetchStacks } from "./stacklist-query"; export const stackQueries = { all: ["stacks"], @@ -16,6 +18,11 @@ export const stackQueries = { queryKey: [...stackQueries.all, queryParams], queryFn: async () => fetchStacks(queryParams) }), + stackDetail: (stackId: string) => + queryOptions({ + queryKey: [...stackQueries.all, stackId], + queryFn: async () => fetchStack({ stackId }) + }), stackDeploymentInfo: (queryParams: StackDeploymentInfoQueryParams) => queryOptions({ queryKey: [...stackQueries.stackDeployment, "info", queryParams], @@ -25,5 +32,10 @@ export const stackQueries = { queryOptions({ queryKey: [...stackQueries.stackDeployment, "stack", queryParams], queryFn: async () => fetchStackDeploymentStack(queryParams) + }), + stackDeploymentConfig: (queryParams: StackDeploymentStackQueryParams) => + queryOptions({ + queryKey: [...stackQueries.stackDeployment, "config", queryParams], + queryFn: async () => fetchStackDeploymentConfig(queryParams) }) }; diff --git a/src/data/stacks/stack-deployment-url.ts b/src/data/stacks/stack-deployment-config.ts similarity index 50% rename from src/data/stacks/stack-deployment-url.ts rename to src/data/stacks/stack-deployment-config.ts index e5f5c3244..41cb264fd 100644 --- a/src/data/stacks/stack-deployment-url.ts +++ b/src/data/stacks/stack-deployment-config.ts @@ -1,16 +1,15 @@ -import { StackDeploymentURLQueryParams, StackDeploymentURLResponse } from "@/types/stack"; +import { StackDeploymentConfigQueryParams, StackDeploymentConfig } from "@/types/stack"; import { FetchError } from "@/lib/fetch-error"; import { notFound } from "@/lib/not-found-error"; import { apiPaths, createApiPath } from "../api"; import { fetcher } from "../fetch"; import { objectToSearchParams } from "@/lib/url"; -import { UseMutationOptions, useMutation } from "@tanstack/react-query"; -export async function fetchStackDeploymentUrl( - queryParams: StackDeploymentURLQueryParams -): Promise { +export async function fetchStackDeploymentConfig( + queryParams: StackDeploymentConfigQueryParams +): Promise { const url = - createApiPath(apiPaths.stackDeployment.url) + + createApiPath(apiPaths.stackDeployment.config) + (queryParams ? `?${objectToSearchParams(queryParams)}` : ""); const res = await fetcher(url, { method: "GET", @@ -30,7 +29,7 @@ export async function fetchStackDeploymentUrl( } return data.detail; }) - .catch(() => "Error while fetching stack deployment url"); + .catch(() => "Error while fetching stack deployment config"); throw new FetchError({ status: res.status, statusText: res.statusText, @@ -39,17 +38,3 @@ export async function fetchStackDeploymentUrl( } return res.json(); } - -export function useDeploymentUrl( - options?: Omit< - UseMutationOptions, - "mutationFn" - > -) { - return useMutation({ - mutationFn: async (payload) => { - return fetchStackDeploymentUrl(payload); - }, - ...options - }); -} diff --git a/src/lib/components.ts b/src/lib/components.ts new file mode 100644 index 000000000..d649deb88 --- /dev/null +++ b/src/lib/components.ts @@ -0,0 +1,14 @@ +import { StackComponent } from "@/types/components"; + +export function extractComponents(json?: Record): StackComponent[] { + if (!json) return []; + const components: StackComponent[] = []; + + for (const key in json) { + if (Array.isArray(json[key])) { + components.push(...json[key]); + } + } + + return components; +} diff --git a/src/lib/strings.spec.ts b/src/lib/strings.spec.ts index 33f1f0b49..fa8fcb787 100644 --- a/src/lib/strings.spec.ts +++ b/src/lib/strings.spec.ts @@ -3,6 +3,8 @@ import { extractDockerImageKey, formatIdToTitleCase, renderAnyToString, + snakeCaseToDashCase, + snakeCaseToLowerCase, snakeCaseToTitleCase, transformToEllipsis } from "./strings"; @@ -104,3 +106,39 @@ describe("formatIdToTitleCase converts hyphenated-strings to Title Case", () => }); }); }); + +describe("format snake_case to lowercase correctly", () => { + const testCases = [ + { + input: "hello_world", + expected: "hello world", + description: "string in snakecase to lowercase" + }, + { input: "single", expected: "single", description: "string without separator to lowercase" }, + { input: "", expected: "", description: "empty string" } + ]; + + testCases.forEach(({ input, expected, description }) => { + test(description, () => { + expect(snakeCaseToLowerCase(input)).toBe(expected); + }); + }); +}); + +describe("format snake_case to dashcase correctly", () => { + const testCases = [ + { + input: "hello_world", + expected: "hello-world", + description: "string in snakecase to dashcase" + }, + { input: "single", expected: "single", description: "string without separator to dashcase" }, + { input: "", expected: "", description: "empty string" } + ]; + + testCases.forEach(({ input, expected, description }) => { + test(description, () => { + expect(snakeCaseToDashCase(input)).toBe(expected); + }); + }); +}); diff --git a/src/lib/strings.ts b/src/lib/strings.ts index 896746bd2..220298743 100644 --- a/src/lib/strings.ts +++ b/src/lib/strings.ts @@ -43,3 +43,14 @@ export const formatIdToTitleCase = (text: string): string => { .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(" "); }; + +export function snakeCaseToLowerCase(input: string): string { + return input + .split("_") + .map((word) => word.charAt(0).toLowerCase() + word.slice(1)) + .join(" "); +} + +export function snakeCaseToDashCase(input: string): string { + return input.split("_").join("-"); +} diff --git a/src/types/core.ts b/src/types/core.ts index e9bb6dedf..a1cb73fb6 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -1723,9 +1723,9 @@ export type paths = { */ get: operations["get_stack_deployment_info_api_v1_stack_deployment_info_get"]; }; - "/api/v1/stack-deployment/url": { + "/api/v1/stack-deployment/config": { /** - * Get Stack Deployment Url + * Get Stack Deployment Config * @description Return the URL to deploy the ZenML stack to the specified cloud provider. * * Args: @@ -1736,10 +1736,10 @@ export type paths = { * auth_context: The authentication context. * * Returns: - * The URL to deploy the ZenML stack to the specified cloud provider - * and a text description of the URL. + * The cloud provider console URL where the stack will be deployed and + * the configuration for the stack deployment. */ - get: operations["get_stack_deployment_url_api_v1_stack_deployment_url_get"]; + get: operations["get_stack_deployment_config_api_v1_stack_deployment_config_get"]; }; "/api/v1/stack-deployment/stack": { /** @@ -7859,6 +7859,18 @@ export type components = { | "orchestrator" | "step_operator" | "model_registry"; + /** + * StackDeploymentConfig + * @description Configuration about a stack deployment. + */ + StackDeploymentConfig: { + /** The cloud provider console URL where the stack will be deployed. */ + deployment_url: string; + /** A textual description for the cloud provider console URL. */ + deployment_url_text: string; + /** Configuration for the stack deployment that the user must manually configure into the cloud provider console. */ + configuration: string | null; + }; /** * StackDeploymentInfo * @description Information about a stack deployment. @@ -7881,6 +7893,11 @@ export type components = { * @description The instructions for post-deployment. */ post_deploy_instructions: string; + /** + * ZenML integrations required for the stack. + * @description The list of ZenML integrations that need to be installed for the stack to be usable. + */ + integrations: string[]; /** * The permissions granted to ZenML to access the cloud resources. * @description The permissions granted to ZenML to access the cloud resources, as a dictionary grouping permissions by resource. @@ -7899,10 +7916,9 @@ export type components = { /** * StackDeploymentProvider * @description All possible stack deployment providers. - * @constant * @enum {string} */ - StackDeploymentProvider: "aws"; + StackDeploymentProvider: "aws" | "gcp"; /** * StackRequest * @description Request model for stacks. @@ -15690,7 +15706,7 @@ export type operations = { }; }; /** - * Get Stack Deployment Url + * Get Stack Deployment Config * @description Return the URL to deploy the ZenML stack to the specified cloud provider. * * Args: @@ -15701,10 +15717,10 @@ export type operations = { * auth_context: The authentication context. * * Returns: - * The URL to deploy the ZenML stack to the specified cloud provider - * and a text description of the URL. + * The cloud provider console URL where the stack will be deployed and + * the configuration for the stack deployment. */ - get_stack_deployment_url_api_v1_stack_deployment_url_get: { + get_stack_deployment_config_api_v1_stack_deployment_config_get: { parameters: { query: { provider: components["schemas"]["StackDeploymentProvider"]; @@ -15716,7 +15732,7 @@ export type operations = { /** @description Successful Response */ 200: { content: { - "application/json": [string, string]; + "application/json": components["schemas"]["StackDeploymentConfig"]; }; }; /** @description Unauthorized */ diff --git a/src/types/flavors.ts b/src/types/flavors.ts new file mode 100644 index 000000000..363a27b3e --- /dev/null +++ b/src/types/flavors.ts @@ -0,0 +1,6 @@ +import { components, operations } from "./core"; + +export type FlavorsPage = components["schemas"]["Page_FlavorResponse_"]; +export type FlavorListQueryParams = NonNullable< + operations["list_flavors_api_v1_flavors_get"]["parameters"]["query"] +>; diff --git a/src/types/stack.ts b/src/types/stack.ts index d569f7c0c..751dddf25 100644 --- a/src/types/stack.ts +++ b/src/types/stack.ts @@ -16,10 +16,10 @@ export type StackDeploymentInfo = components["schemas"]["StackDeploymentInfo"]; export type StackDeploymentInfoQueryParams = NonNullable< operations["get_stack_deployment_info_api_v1_stack_deployment_info_get"]["parameters"]["query"] >; -export type StackDeploymentURLQueryParams = NonNullable< - operations["get_stack_deployment_url_api_v1_stack_deployment_url_get"]["parameters"]["query"] +export type StackDeploymentConfigQueryParams = NonNullable< + operations["get_stack_deployment_config_api_v1_stack_deployment_config_get"]["parameters"]["query"] >; -export type StackDeploymentURLResponse = [string, string]; +export type StackDeploymentConfig = components["schemas"]["StackDeploymentConfig"]; export type StackDeploymentStackQueryParams = NonNullable< operations["get_deployed_stack_api_v1_stack_deployment_stack_get"]["parameters"]["query"] >;