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 (
+
+
+
+
+
+ {
+ if (focusRef.current) {
+ focusRef.current.focus();
+ focusRef.current = null;
+ event.preventDefault();
+ }
+ }}
+ className="z-10"
+ align="end"
+ sideOffset={1}
+ >
+ }
+ triggerChildren="Update"
+ >
+ handleDialogItemOpenChange(false)}
+ />
+
+ }
+ triggerChildren="Delete"
+ >
+ handleDialogItemOpenChange(false)}
+ />
+
+
+
+
+ );
+}
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
+
+
+
+ );
+});
+
+UpdateStackDialog.displayName = "UpdateStackDialog";
+
+export const DeleteStackDialog = forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef & Props
+>(({ closeModal, name, ...rest }, ref) => {
+ return (
+
+
+ Delete 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 (
- handleClick()}>
- Deploy in {data.provider?.toUpperCase()}
+ handleClick()}>
+
+ Deploy in {data.provider?.toUpperCase()}{" "}
+
+
);
}
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 (
-
+
Cancel
);
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.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.name}
+
+
+
{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 (
+
+ );
+}
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"]
>;