Skip to content

Commit 17c4041

Browse files
feat: azure 1-click (#641)
1 parent 224e405 commit 17c4041

File tree

9 files changed

+277
-33
lines changed

9 files changed

+277
-33
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import ConfigIcon from "@/assets/icons/logs.svg?react";
2+
import { Codesnippet } from "@/components/CodeSnippet";
3+
import { InfoBox } from "@/components/Infobox";
4+
import { Numberbox } from "@/components/NumberBox";
5+
import { ComponentBadge } from "@/components/stack-components/ComponentBadge";
6+
import { Tick } from "@/components/Tick";
7+
import { stackQueries } from "@/data/stacks";
8+
import { useQuery } from "@tanstack/react-query";
9+
import { Avatar, AvatarFallback, Skeleton, Spinner } from "@zenml-io/react-component-library";
10+
import { useNewInfraFormContext } from "../NewInfraFormContext";
11+
import { DeploymentButton } from "../Steps/Deploy/ButtonStep";
12+
import { ComponentListItem, ProviderComponents } from "./index";
13+
import { PermissionsCard } from "./PermissionsCard";
14+
15+
type Props = ProviderComponents;
16+
17+
export function AzureComponents({
18+
stackName,
19+
isLoading,
20+
isSuccess,
21+
components,
22+
displayPermissions = false
23+
}: Props) {
24+
return (
25+
<div className="divide-y divide-theme-border-moderate overflow-hidden rounded-md border border-theme-border-moderate">
26+
<div className="flex items-center gap-3 bg-theme-surface-secondary p-5 text-text-lg font-semibold">
27+
{isLoading && <Spinner className="h-5 w-5 shrink-0 border-[3px]" />}
28+
{isSuccess && <Tick className="h-5 w-5" tickClasses="w-3 h-3" />}
29+
<Avatar type="square" size="lg">
30+
<AvatarFallback size="lg">{stackName[0]}</AvatarFallback>
31+
</Avatar>
32+
{stackName}
33+
</div>
34+
<div className="space-y-1 py-3 pl-9 pr-5">
35+
<ComponentListItem
36+
title={components?.connector?.name || "Azure Service Principal"}
37+
isLoading={isLoading}
38+
isSuccess={isSuccess}
39+
subtitle={components?.connector?.id || "Manage access to Azure resources"}
40+
badge={<ComponentBadge type="annotator">Service Connector</ComponentBadge>}
41+
img={{
42+
src: "https://public-flavor-logos.s3.eu-central-1.amazonaws.com/service_connector/azure-service-principal.webp",
43+
alt: "Service Principal logo"
44+
}}
45+
/>
46+
{displayPermissions && <PermissionsCard />}
47+
</div>
48+
<div className="py-3 pl-9 pr-5">
49+
<ComponentListItem
50+
title={components?.artifactStore?.name || "Azure Blob Storage"}
51+
subtitle={components?.artifactStore?.id || "Artifact storage for ML pipelines"}
52+
badge={<ComponentBadge type="artifact_store">Artifact Store</ComponentBadge>}
53+
isLoading={isLoading}
54+
isSuccess={isSuccess}
55+
img={{
56+
src: "https://public-flavor-logos.s3.eu-central-1.amazonaws.com/artifact_store/azure.png",
57+
alt: "Blob Storage logo"
58+
}}
59+
/>
60+
</div>
61+
<div className="py-3 pl-9 pr-5">
62+
<ComponentListItem
63+
title={components?.registry?.name || "Azure Container Registry"}
64+
subtitle={components?.registry?.id || "Container image storage"}
65+
badge={<ComponentBadge type="container_registry">Container Registry</ComponentBadge>}
66+
isLoading={isLoading}
67+
isSuccess={isSuccess}
68+
img={{
69+
src: "https://public-flavor-logos.s3.eu-central-1.amazonaws.com/container_registry/azure.png",
70+
alt: "Azure Container Registry logo"
71+
}}
72+
/>
73+
</div>
74+
<div className="py-3 pl-9 pr-5">
75+
<ComponentListItem
76+
title={components?.orchestrator?.name || "Azure Skypilot Orchestrator"}
77+
isLoading={isLoading}
78+
isSuccess={isSuccess}
79+
subtitle={components?.orchestrator?.id || "ML Workflow orchestration"}
80+
badge={<ComponentBadge type="orchestrator">Orchestrator</ComponentBadge>}
81+
img={{
82+
src: "https://public-flavor-logos.s3.eu-central-1.amazonaws.com/orchestrator/azure-skypilot.png",
83+
alt: "Azure Skypilot logo"
84+
}}
85+
/>
86+
</div>
87+
</div>
88+
);
89+
}
90+
91+
export function AzureInstructions({ displayInfobox = false }: { displayInfobox?: boolean }) {
92+
return (
93+
<section className="space-y-5">
94+
<AzureHeader />
95+
<AzureDeploymentButton displayInfobox={displayInfobox} />
96+
<CodeSnippetStep />
97+
<CommandStep />
98+
</section>
99+
);
100+
}
101+
102+
function AzureDeploymentButton({ displayInfobox = false }: { displayInfobox: boolean }) {
103+
return (
104+
<div className="space-y-5">
105+
<div className="space-y-1">
106+
<div className="flex items-center gap-1">
107+
<Numberbox>1</Numberbox>
108+
<span className="text-text-lg font-semibold">Open the Azure Cloud Shell Console</span>
109+
</div>
110+
<p className="text-theme-text-secondary">
111+
Deploy the stack using the Azure Cloud Shell console.
112+
</p>
113+
</div>
114+
<DeploymentButton setTimestampBool>
115+
<span className="text-text-lg font-semibold">Open the Azure Cloud Shell</span>
116+
</DeploymentButton>
117+
{displayInfobox && (
118+
<InfoBox className="border-warning-300 bg-warning-50" intent="warning">
119+
After the Terraform deployment is complete, you can close the Cloud Shell session and
120+
return to the dashboard to view details about the associated ZenML stack automatically
121+
registered with ZenML.
122+
</InfoBox>
123+
)}
124+
</div>
125+
);
126+
}
127+
128+
function CodeSnippetStep() {
129+
return (
130+
<div className="space-y-5">
131+
<div className="space-y-1">
132+
<div className="flex items-center gap-1">
133+
<Numberbox>2</Numberbox>
134+
<span className="text-text-lg font-semibold">
135+
Create a file with the following configuration
136+
</span>
137+
</div>
138+
<p className="text-theme-text-secondary">
139+
Create a file named <code className="font-mono text-primary-400">main.tf</code> in the
140+
Cloud Shell and copy and paste the Terraform configuration below into it.
141+
</p>
142+
</div>
143+
<AzureCodesnippet />
144+
</div>
145+
);
146+
}
147+
148+
function CommandStep() {
149+
return (
150+
<div className="space-y-5">
151+
<div className="space-y-1">
152+
<div className="flex items-center gap-1">
153+
<Numberbox>3</Numberbox>
154+
<span className="text-text-lg font-semibold">Run the following commands</span>
155+
</div>
156+
</div>
157+
<div>
158+
<p className="mb-1 text-text-sm text-theme-text-secondary">
159+
Initialize the Terraform configuration.
160+
</p>
161+
<Codesnippet code="terraform init --upgrade" />
162+
</div>
163+
<div>
164+
<p className="mb-1 text-text-sm text-theme-text-secondary">
165+
Run terraform apply to deploy the ZenML stack to Azure.
166+
</p>
167+
<Codesnippet code="terraform apply" />
168+
</div>
169+
</div>
170+
);
171+
}
172+
173+
function AzureCodesnippet() {
174+
const { data } = useNewInfraFormContext();
175+
const deploymentConfig = useQuery({
176+
...stackQueries.stackDeploymentConfig({
177+
provider: "azure",
178+
stack_name: data.stackName!,
179+
location: data.location
180+
}),
181+
enabled: !!data.stackName
182+
});
183+
if (deploymentConfig.isError) return null;
184+
if (deploymentConfig.isPending) return <Skeleton className="h-[200px] w-full" />;
185+
186+
return (
187+
// This should be a unified component for gcp as well
188+
189+
<Codesnippet
190+
fullWidth
191+
highlightCode
192+
codeClasses="whitespace-pre-wrap word-break-all"
193+
wrap
194+
code={deploymentConfig.data.configuration || ""}
195+
/>
196+
);
197+
}
198+
199+
function AzureHeader() {
200+
return (
201+
<div className="space-y-1">
202+
<p className="flex items-center gap-1 text-text-lg font-semibold">
203+
<ConfigIcon className="h-5 w-5 fill-primary-400" />
204+
Configuration
205+
</p>
206+
<p className="text-theme-text-secondary">
207+
You will be asked to provide the following configuration values during the deployment
208+
process.
209+
</p>
210+
</div>
211+
);
212+
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import {
1111
import { useNewInfraFormContext } from "../NewInfraFormContext";
1212

1313
export function PermissionsCard() {
14+
const { data } = useNewInfraFormContext();
1415
return (
1516
<HoverCard>
1617
<HoverCardTrigger className="block">
1718
<InfoBox intent="warning">
18-
This will give ZenML permissions and create secret keys for secure ZenML -{">"} Provider
19-
communication. You can revoke these permissions at any time.
19+
This will give ZenML permissions and create secret keys for secure ZenML -{">"}{" "}
20+
<span className="capitalize">{data.provider || "Provider"}</span> communication. You can
21+
revoke these permissions at any time.
2022
</InfoBox>
2123
</HoverCardTrigger>
2224
<HoverCardContent sideOffset={0} className="w-auto p-5">

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ReactNode } from "react";
77
import { useNewInfraFormContext } from "../NewInfraFormContext";
88
import { AWSComponents } from "./AWS";
99
import { GcpComponents } from "./GCP";
10+
import { AzureComponents } from "./Azure";
1011

1112
export type ProviderComponents = {
1213
stackName: string;
@@ -35,6 +36,8 @@ export function CloudComponents({ componentProps, type }: Props) {
3536
return <AWSComponents {...componentProps} />;
3637
case "gcp":
3738
return <GcpComponents {...componentProps} />;
39+
case "azure":
40+
return <AzureComponents {...componentProps} />;
3841
}
3942
}
4043

@@ -82,6 +85,10 @@ export function EstimateCosts() {
8285
break;
8386
case "gcp":
8487
link = "https://cloud.google.com/products/calculator";
88+
break;
89+
case "azure":
90+
link = "https://azure.microsoft.com/en-us/pricing/calculator/";
91+
break;
8592
}
8693

8794
return (

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

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { InfoBox } from "@/components/Infobox";
33
import { CloudProviderIcon } from "@/components/ProviderIcon";
44
import { useQuery } from "@tanstack/react-query";
55
import { Button, Skeleton } from "@zenml-io/react-component-library";
6+
import { PropsWithChildren } from "react";
67
import { stackQueries } from "../../../../../../data/stacks";
78
import { useNewInfraFormContext } from "../../NewInfraFormContext";
9+
import { AzureInstructions } from "../../Providers/Azure";
810
import { GCPCodesnippet, GCPWarning } from "../../Providers/GCP";
911
import { setWizardData } from "../../persist";
1012

@@ -16,26 +18,32 @@ export function DeployButtonPart() {
1618
This will provision and register a basic ZenML stack with all the necessary resources and
1719
credentials required to run pipelines.
1820
</InfoBox>
19-
<div>
20-
<div className="flex flex-wrap items-center gap-1">
21-
<CloudProviderIcon provider={data.provider!} className="h-5 w-5" />
22-
<p className="text-text-lg font-semibold">Deploy the Stack</p>
21+
{data.provider !== "azure" && (
22+
<div>
23+
<div className="flex flex-wrap items-center gap-1">
24+
<CloudProviderIcon provider={data.provider!} className="h-5 w-5" />
25+
<p className="text-text-lg font-semibold">Deploy the Stack</p>
26+
</div>
27+
<p className="text-theme-text-secondary">
28+
Deploy the stack from your browser by clicking the button below:
29+
</p>
2330
</div>
24-
<p className="text-theme-text-secondary">
25-
Deploy the stack from your browser by clicking the button below:
26-
</p>
27-
</div>
31+
)}
2832
{data.provider === "gcp" && <GCPWarning />}
29-
<DeploymentButton setTimestampBool />
33+
{data.provider !== "azure" && <DeploymentButton setTimestampBool />}
3034
{data.provider === "gcp" && <GCPCodesnippet />}
35+
{data.provider === "azure" && <AzureInstructions displayInfobox />}
3136
</div>
3237
);
3338
}
3439

3540
type DeploymentButtonProps = {
3641
setTimestampBool?: boolean;
3742
};
38-
export function DeploymentButton({ setTimestampBool }: DeploymentButtonProps) {
43+
export function DeploymentButton({
44+
setTimestampBool,
45+
children
46+
}: PropsWithChildren<DeploymentButtonProps>) {
3947
const { data, setTimestamp, setIsLoading } = useNewInfraFormContext();
4048

4149
const stackDeploymentConfig = useQuery({
@@ -67,9 +75,20 @@ export function DeploymentButton({ setTimestampBool }: DeploymentButtonProps) {
6775
}
6876

6977
return (
70-
<Button asChild className="w-fit whitespace-nowrap" size="md" onClick={() => handleClick()}>
78+
<Button
79+
asChild
80+
className="w-fit gap-3 whitespace-nowrap"
81+
size="lg"
82+
onClick={() => handleClick()}
83+
>
7184
<a href={stackDeploymentConfig.data.deployment_url} target="_blank" rel="noopener noreferrer">
72-
Deploy in {data.provider?.toUpperCase()}{" "}
85+
{children ? (
86+
children
87+
) : (
88+
<div>
89+
Deploy in <span className="uppercase">{data.provider}</span>
90+
</div>
91+
)}
7392
<External className="h-5 w-5 shrink-0 fill-white" />
7493
</a>
7594
</Button>

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Box, Skeleton } from "@zenml-io/react-component-library";
99
import { useEffect, useState } from "react";
1010
import { useNewInfraFormContext } from "../../NewInfraFormContext";
1111
import { CloudComponents } from "../../Providers";
12+
import { AzureInstructions } from "../../Providers/Azure";
1213
import { GCPCodesnippet } from "../../Providers/GCP";
1314
import { clearWizardData } from "../../persist";
1415
import { DeploymentButton } from "./ButtonStep";
@@ -65,9 +66,16 @@ function LoadingHeader() {
6566
</p>
6667
</div>
6768
</div>
68-
<DeploymentButton />
69+
{data.provider === "azure" ? (
70+
<DeploymentButton>
71+
<span>Deploy in Azure</span>
72+
</DeploymentButton>
73+
) : (
74+
<DeploymentButton />
75+
)}
6976
</Box>
7077
{data.provider === "gcp" && <GCPCodesnippet />}
78+
{data.provider === "azure" && <AzureInstructions />}
7179
</section>
7280
);
7381
}

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,8 @@ export function ProviderStep() {
5757
subtitle="Create ZenML infrastructure using GCS, Artifact Registry, and Vertex AI"
5858
/>
5959
</CloudProviderRadioButton>
60-
<CloudProviderRadioButton
61-
disabled
62-
id="azure-provider"
63-
{...register("provider")}
64-
value="azure"
65-
>
60+
<CloudProviderRadioButton id="azure-provider" {...register("provider")} value="azure">
6661
<ProviderCard
67-
comingSoon
6862
icon={<CloudProviderIcon provider="azure" className="h-6 w-6 shrink-0" />}
6963
title="Azure"
7064
subtitle="Set up ZenML with Azure Storage, Container Registry, and ML services"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const entries = ["Infrastructure Type", "Cloud Provider", "Review Configuration"
99
export default function StackWithNewInfrastructurePage() {
1010
const { success } = parseWizardData();
1111
return (
12-
<WizardProvider maxSteps={entries.length + 1} initialStep={success ? 3 : 1}>
12+
<WizardProvider maxSteps={entries.length} initialStep={success ? 3 : 1}>
1313
<NewInfraFormProvider>
1414
<section className="layout-container flex flex-col gap-5 p-5 xl:flex-row">
1515
<LeftSideMenu entries={entries} />

src/components/NumberBox.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { PropsWithChildren } from "react";
2+
3+
export function Numberbox({ children }: PropsWithChildren) {
4+
return (
5+
<div className="flex h-7 w-7 items-center justify-center rounded-sm bg-primary-100 text-text-lg font-semibold text-theme-text-brand">
6+
{children}
7+
</div>
8+
);
9+
}

0 commit comments

Comments
 (0)