Skip to content

Commit f9e9e3d

Browse files
feat: enable GCP one click deployment (#627)
1 parent bc57532 commit f9e9e3d

File tree

16 files changed

+221
-103
lines changed

16 files changed

+221
-103
lines changed

src/app/stacks/create/new-infrastructure/Providers/AWS.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ import { PermissionsCard } from "./PermissionsCard";
66

77
type Props = ProviderComponents;
88

9-
export const awsPrizes = {
10-
cost: "$0.60"
11-
};
12-
139
export function AWSComponents({
1410
stackName,
1511
isLoading,

src/app/stacks/create/new-infrastructure/Providers/GCP.tsx

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1+
import ConfigIcon from "@/assets/icons/logs.svg?react";
2+
import { Codesnippet } from "@/components/CodeSnippet";
3+
import { InfoBox } from "@/components/Infobox";
14
import { Tick } from "@/components/Tick";
2-
import { Avatar, AvatarFallback, Spinner } from "@zenml-io/react-component-library";
5+
import { stackQueries } from "@/data/stacks";
6+
import { useQuery } from "@tanstack/react-query";
7+
import { Avatar, AvatarFallback, Skeleton, Spinner } from "@zenml-io/react-component-library";
38
import { ComponentListItem, ProviderComponents } from ".";
49
import { ComponentBadge } from "../../../../../components/stack-components/ComponentBadge";
10+
import { useNewInfraFormContext } from "../NewInfraFormContext";
511
import { PermissionsCard } from "./PermissionsCard";
612

713
type Props = ProviderComponents;
814

9-
export const gcpPrizes = {
10-
orchestratorCosts: "$0.27",
11-
storageCosts: "$4.60"
12-
};
13-
1415
export function GcpComponents({
1516
stackName,
1617
isLoading,
@@ -36,8 +37,8 @@ export function GcpComponents({
3637
subtitle={components?.connector?.id || "Manage access to GCP resources"}
3738
badge={<ComponentBadge type="annotator">Service Connector</ComponentBadge>}
3839
img={{
39-
src: "https://public-flavor-logos.s3.eu-central-1.amazonaws.com/service_connector/iam.webp",
40-
alt: "Service connector logo"
40+
src: "https://public-flavor-logos.s3.eu-central-1.amazonaws.com/service_connector/gcp-iam.webp",
41+
alt: "Service Account logo"
4142
}}
4243
/>
4344
{displayPermissions && <PermissionsCard />}
@@ -81,6 +82,65 @@ export function GcpComponents({
8182
}}
8283
/>
8384
</div>
85+
<div className="py-3 pl-9 pr-5">
86+
<ComponentListItem
87+
title={components?.imageBuilder?.name || "Cloud Build"}
88+
isLoading={isLoading}
89+
isSuccess={isSuccess}
90+
subtitle={components?.imageBuilder?.id || "Build, test, and deploy images"}
91+
badge={<ComponentBadge type="image_builder">Image Builder</ComponentBadge>}
92+
img={{
93+
src: "https://public-flavor-logos.s3.eu-central-1.amazonaws.com/image_builder/gcp.png",
94+
alt: "Cloud Build logo"
95+
}}
96+
/>
97+
</div>
8498
</div>
8599
);
86100
}
101+
102+
export function GCPWarning() {
103+
return (
104+
<InfoBox className="border-warning-300 bg-warning-50" intent="warning">
105+
The Cloud Shell session will warn you that the ZenML GitHub repository is untrusted. We
106+
recommend that you review the contents of the repository and then check the Trust repo
107+
checkbox to proceed with the deployment, otherwise the Cloud Shell session will not be
108+
authenticated to access your GCP projects.
109+
</InfoBox>
110+
);
111+
}
112+
113+
export function GCPCodesnippet() {
114+
const { data } = useNewInfraFormContext();
115+
const deploymentConfig = useQuery({
116+
...stackQueries.stackDeploymentConfig({
117+
provider: "gcp",
118+
stack_name: data.stackName!,
119+
location: data.location
120+
})
121+
});
122+
if (deploymentConfig.isError) return null;
123+
if (deploymentConfig.isPending) return <Skeleton className="h-[200px] w-full" />;
124+
125+
return (
126+
<section className="space-y-5 border-t border-theme-border-moderate pt-5">
127+
<div className="space-y-1">
128+
<p className="flex items-center gap-1 text-text-lg font-semibold">
129+
<ConfigIcon className="h-5 w-5 fill-primary-400" />
130+
Configuration
131+
</p>
132+
<p className="text-theme-text-secondary">
133+
You will be asked to provide the following configuration values during the deployment
134+
process.
135+
</p>
136+
</div>
137+
<Codesnippet
138+
fullWidth
139+
highlightCode
140+
codeClasses="whitespace-pre-wrap word-break-all"
141+
wrap
142+
code={deploymentConfig.data.configuration || ""}
143+
/>
144+
</section>
145+
);
146+
}

src/app/stacks/create/new-infrastructure/Providers/index.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { StackDeploymentProvider } from "@/types/stack";
55
import { Box, Spinner } from "@zenml-io/react-component-library";
66
import { ReactNode } from "react";
77
import { useNewInfraFormContext } from "../NewInfraFormContext";
8-
import { AWSComponents, awsPrizes } from "./AWS";
9-
import { gcpPrizes } from "./GCP";
8+
import { AWSComponents } from "./AWS";
9+
import { GcpComponents } from "./GCP";
1010

1111
export type ProviderComponents = {
1212
stackName: string;
@@ -18,6 +18,7 @@ export type ProviderComponents = {
1818
artifactStore?: Component;
1919
registry?: Component;
2020
orchestrator?: Component;
21+
imageBuilder?: Component;
2122
};
2223
};
2324

@@ -32,8 +33,8 @@ export function CloudComponents({ componentProps, type }: Props) {
3233
switch (type) {
3334
case "aws":
3435
return <AWSComponents {...componentProps} />;
35-
// case "gcp":
36-
// return <GcpComponents {...componentProps} />;
36+
case "gcp":
37+
return <GcpComponents {...componentProps} />;
3738
}
3839
}
3940

@@ -72,14 +73,22 @@ export function ComponentListItem({
7273

7374
export function EstimateCosts() {
7475
const { data } = useNewInfraFormContext();
75-
gcpPrizes;
76-
function getPrize() {
76+
77+
function PricingCalculatorLink() {
78+
let link = "#";
7779
switch (data.provider) {
7880
case "aws":
79-
return awsPrizes;
80-
// case "gcp":
81-
// return gcpPrizes;
81+
link = "https://calculator.aws/#/";
82+
break;
83+
case "gcp":
84+
link = "https://cloud.google.com/products/calculator";
8285
}
86+
87+
return (
88+
<a href={link} target="_blank" rel="noopener noreferrer" className="link">
89+
official pricing calculator
90+
</a>
91+
);
8392
}
8493

8594
return (
@@ -100,10 +109,10 @@ export function EstimateCosts() {
100109
<div>
101110
<p>
102111
A small training job would cost around:{" "}
103-
<span className="font-semibold text-theme-text-success">{getPrize()?.cost}</span>
112+
<span className="font-semibold text-theme-text-success">$0.60</span>
104113
</p>
105114
<p className="text-text-xs text-theme-text-secondary">
106-
Please use the official pricing Calculator for a detailed estimate
115+
Please use the <PricingCalculatorLink /> for a detailed estimate
107116
</p>
108117
</div>
109118
</Box>

src/app/stacks/create/new-infrastructure/Steps/Configuration/Partials.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function Region() {
1616
Choose Your Location
1717
</p>
1818
<p className="text-theme-text-secondary">
19-
Select where you want to deploy your Cloud components for optimal performance and
19+
Select where you want to deploy your cloud infrastructure for optimal performance and
2020
compliance.
2121
</p>
2222
</div>
Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import External from "@/assets/icons/link-external.svg?react";
22
import { InfoBox } from "@/components/Infobox";
33
import { CloudProviderIcon } from "@/components/ProviderIcon";
4-
import { useDeploymentUrl } from "@/data/stacks/stack-deployment-url";
5-
import { Button } from "@zenml-io/react-component-library";
4+
import { useQuery } from "@tanstack/react-query";
5+
import { Button, Skeleton } from "@zenml-io/react-component-library";
6+
import { stackQueries } from "../../../../../../data/stacks";
67
import { useNewInfraFormContext } from "../../NewInfraFormContext";
78
import { useNewInfraWizardContext } from "../../NewInfraWizardContext";
9+
import { GCPCodesnippet, GCPWarning } from "../../Providers/GCP";
810

911
export function DeployButtonPart() {
1012
const { data } = useNewInfraFormContext();
@@ -23,7 +25,9 @@ export function DeployButtonPart() {
2325
Deploy the stack from your browser by clicking the button below:
2426
</p>
2527
</div>
28+
{data.provider === "gcp" && <GCPWarning />}
2629
<DeploymentButton setTimestampBool />
30+
{data.provider === "gcp" && <GCPCodesnippet />}
2731
</div>
2832
);
2933
}
@@ -33,25 +37,36 @@ type DeploymentButtonProps = {
3337
};
3438
export function DeploymentButton({ setTimestampBool }: DeploymentButtonProps) {
3539
const { data, setTimestamp } = useNewInfraFormContext();
40+
3641
const { setIsLoading } = useNewInfraWizardContext();
37-
const { mutate } = useDeploymentUrl({
38-
onSuccess: async (data) => {
39-
setTimestampBool && setTimestamp(new Date().toISOString().slice(0, -1)!);
40-
setIsLoading(true);
41-
window.open(data[0], "_blank");
42-
}
42+
43+
const stackDeploymentConfig = useQuery({
44+
...stackQueries.stackDeploymentConfig({
45+
provider: data.provider!,
46+
location: data.location,
47+
stack_name: data.stackName!
48+
})
4349
});
4450

51+
if (stackDeploymentConfig.isError) {
52+
return null;
53+
}
54+
55+
if (stackDeploymentConfig.isPending) {
56+
return <Skeleton className="h-[48px] w-[100px]" />;
57+
}
58+
4559
function handleClick() {
46-
mutate({
47-
stack_name: data.stackName!,
48-
location: data.location,
49-
provider: data.provider!
50-
});
60+
setTimestampBool && setTimestamp(new Date().toISOString().slice(0, -1)!);
61+
setIsLoading(true);
5162
}
63+
5264
return (
53-
<Button className="min-w-fit" size="md" onClick={() => handleClick()}>
54-
Deploy in {data.provider?.toUpperCase()} <External className="h-5 w-5 fill-white" />
65+
<Button asChild className="w-fit whitespace-nowrap" size="md" onClick={() => handleClick()}>
66+
<a href={stackDeploymentConfig.data.deployment_url} target="_blank" rel="noopener noreferrer">
67+
Deploy in {data.provider?.toUpperCase()}{" "}
68+
<External className="h-5 w-5 shrink-0 fill-white" />
69+
</a>
5570
</Button>
5671
);
5772
}

src/app/stacks/create/new-infrastructure/Steps/Deploy/ProvisioningStep.tsx

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useNewInfraFormContext } from "../../NewInfraFormContext";
1010
import { useNewInfraWizardContext } from "../../NewInfraWizardContext";
1111
import { CloudComponents } from "../../Providers";
1212
import { DeploymentButton } from "./ButtonStep";
13+
import { GCPCodesnippet } from "../../Providers/GCP";
1314

1415
export function ProvisioningStep() {
1516
const { data, timestamp, setIsNextButtonDisabled } = useNewInfraFormContext();
@@ -50,19 +51,23 @@ export function ProvisioningStep() {
5051
function LoadingHeader() {
5152
const { data } = useNewInfraFormContext();
5253
return (
53-
<Box className="flex items-center justify-between gap-4 px-6 py-5">
54-
<div className="flex items-start gap-3">
55-
<CloudProviderIcon provider={data.provider!} className="h-6 w-6 shrink-0" />
56-
<div>
57-
<p className="text-text-lg font-semibold">Deploying the Stack...</p>
58-
<p className="text-theme-text-secondary">
59-
Follow the steps in your Cloud console to finish the setup. You can come back to check
60-
once your components are deployed.
61-
</p>
54+
<section className="space-y-5 border-b border-theme-border-moderate pb-5">
55+
<Warning />
56+
<Box className="flex items-center justify-between gap-4 px-6 py-5">
57+
<div className="flex items-start gap-3">
58+
<CloudProviderIcon provider={data.provider!} className="h-6 w-6 shrink-0" />
59+
<div>
60+
<p className="text-text-lg font-semibold">Deploying the Stack...</p>
61+
<p className="text-theme-text-secondary">
62+
Follow the steps in your Cloud console to finish the setup. You can come back to check
63+
once your components are deployed.
64+
</p>
65+
</div>
6266
</div>
63-
</div>
64-
<DeploymentButton />
65-
</Box>
67+
<DeploymentButton />
68+
</Box>
69+
{data.provider === "gcp" && <GCPCodesnippet />}
70+
</section>
6671
);
6772
}
6873

@@ -127,3 +132,15 @@ function ItTakesLongerBox({ isReady }: { isReady: boolean }) {
127132
</InfoBox>
128133
);
129134
}
135+
136+
function Warning() {
137+
return (
138+
<InfoBox
139+
className="border-warning-300 bg-warning-50 text-text-sm text-warning-900"
140+
intent="warning"
141+
>
142+
<strong>Please, do not leave this screen</strong> until the stack and the components are fully
143+
created.
144+
</InfoBox>
145+
);
146+
}

src/app/stacks/create/new-infrastructure/Steps/Provider.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,8 @@ export function ProviderStep() {
5050
subtitle="ZenML stack with S3, ECR, and SageMaker integration"
5151
/>
5252
</CloudProviderRadioButton>
53-
<CloudProviderRadioButton
54-
disabled
55-
id="gcp-provider"
56-
{...register("provider")}
57-
value="gcp"
58-
>
53+
<CloudProviderRadioButton id="gcp-provider" {...register("provider")} value="gcp">
5954
<ProviderCard
60-
comingSoon
6155
icon={<CloudProviderIcon provider="gcp" className="h-6 w-6 shrink-0" />}
6256
title="GCP"
6357
subtitle="Create ZenML infrastructure using GCS, Artifact Registry, and Vertex AI"

src/app/stacks/create/new-infrastructure/Steps/Success/SuccessStep.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ function SuccessList() {
5151
const registry = stack.stack.metadata?.components["container_registry"] as
5252
| StackComponent[]
5353
| undefined;
54+
const imageBuilder = stack.stack.metadata?.components["image_builder"] as
55+
| StackComponent[]
56+
| undefined;
57+
5458
const components = {
5559
orchestrator: {
5660
name: orchestrators?.[0]?.name ?? "Orchestrator",
@@ -67,6 +71,10 @@ function SuccessList() {
6771
connector: {
6872
name: stack.service_connector?.name as string,
6973
id: stack.service_connector?.id?.split("-")[0] ?? ""
74+
},
75+
imageBuilder: {
76+
name: imageBuilder?.[0]?.name ?? "Image Builder",
77+
id: imageBuilder?.[0]?.id.split("-")[0] ?? ""
7078
}
7179
};
7280

src/app/stacks/create/new-infrastructure/Steps/schemas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { z } from "zod";
22

33
export const providerSchema = z.object({
4-
provider: z.enum(["aws"])
4+
provider: z.enum(["aws", "gcp"])
55
});
66

77
export type ProviderForm = z.infer<typeof providerSchema>;

src/app/stacks/create/new-infrastructure/Wizard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ function NextButton() {
7575

7676
function CancelButton() {
7777
return (
78-
<Button intent="secondary" size="md">
78+
<Button asChild intent="secondary" size="md">
7979
<Link to={routes.stacks.create.index}>Cancel</Link>
8080
</Button>
8181
);

0 commit comments

Comments
 (0)