Skip to content

Commit f9ae065

Browse files
feat: Persist Wizard to localstorage (#632)
1 parent ca613bd commit f9ae065

File tree

12 files changed

+169
-69
lines changed

12 files changed

+169
-69
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { InfoBox } from "@/components/Infobox";
2+
import { stackQueries } from "@/data/stacks";
3+
import { routes } from "@/router/routes";
4+
import { useQuery } from "@tanstack/react-query";
5+
import { Button, Skeleton } from "@zenml-io/react-component-library";
6+
import { Dispatch, SetStateAction, useEffect } from "react";
7+
import { Link } from "react-router-dom";
8+
import { clearWizardData, parseWizardData } from "./create/new-infrastructure/persist";
9+
10+
type Props = {
11+
setHasResumeableStack: Dispatch<SetStateAction<boolean>>;
12+
};
13+
14+
export function ResumeStackBanner({ setHasResumeableStack }: Props) {
15+
const { success, data } = parseWizardData();
16+
const stack = useQuery({
17+
...stackQueries.stackDeploymentStack({
18+
provider: data?.provider || "aws",
19+
stack_name: data?.stackName || "",
20+
date_start: data?.timestamp
21+
}),
22+
enabled: success,
23+
throwOnError: true
24+
});
25+
26+
useEffect(() => {
27+
if (stack.data) {
28+
clearWizardData();
29+
setHasResumeableStack(false);
30+
}
31+
}, [stack.data]);
32+
33+
if (!success) return null;
34+
35+
if (stack.isError) return null;
36+
if (stack.isPending) return <Skeleton className="h-[70px] w-full" />;
37+
38+
return (
39+
<InfoBox className="w-full">
40+
<section className="flex flex-wrap items-center justify-between gap-y-2">
41+
<div className="flex flex-wrap items-center gap-2">
42+
<p className="font-semibold">You have a Stack provision incomplete</p>
43+
<p className="text-text-sm">
44+
Return to the setup and finish the configuration on your cloud provider
45+
</p>
46+
</div>
47+
<div>
48+
<Button
49+
className="w-fit bg-theme-surface-primary"
50+
intent="primary"
51+
emphasis="subtle"
52+
asChild
53+
>
54+
<Link to={routes.stacks.create.newInfra}>
55+
<span>Review Stack</span>
56+
</Link>
57+
</Button>
58+
</div>
59+
</section>
60+
</InfoBox>
61+
);
62+
}

src/app/stacks/StackList.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
import { stackQueries } from "@/data/stacks";
2-
import Refresh from "@/assets/icons/refresh.svg?react";
31
import Plus from "@/assets/icons/plus.svg?react";
2+
import Refresh from "@/assets/icons/refresh.svg?react";
3+
import Pagination from "@/components/Pagination";
4+
import { SearchField } from "@/components/SearchField";
5+
import { stackQueries } from "@/data/stacks";
6+
import { routes } from "@/router/routes";
47
import { useQuery } from "@tanstack/react-query";
58
import { Button, DataTable, Skeleton } from "@zenml-io/react-component-library";
6-
import Pagination from "@/components/Pagination";
9+
import { useState } from "react";
10+
import { Link } from "react-router-dom";
711
import { getStackColumns } from "./columns";
12+
import { parseWizardData } from "./create/new-infrastructure/persist";
13+
import { ResumeStackBanner } from "./ResumeStackBanner";
814
import { useStacklistQueryParams } from "./service";
9-
import { SearchField } from "@/components/SearchField";
10-
import { Link } from "react-router-dom";
11-
import { routes } from "@/router/routes";
1215

1316
export function StackList() {
17+
const [hasResumeableStack, setResumeableStack] = useState(parseWizardData().success);
1418
const queryParams = useStacklistQueryParams();
1519
const { refetch, data } = useQuery({
1620
...stackQueries.stackList({ ...queryParams, sort_by: "desc:updated" }),
@@ -20,7 +24,7 @@ export function StackList() {
2024
return (
2125
<section className="p-5">
2226
<div className="flex flex-col gap-5">
23-
<div className="flex items-center justify-between">
27+
<div className="flex flex-wrap items-center justify-between gap-y-4">
2428
<SearchField searchParams={queryParams} />
2529
<div className="flex items-center justify-between gap-2">
2630
<Button intent="primary" emphasis="subtle" size="md" onClick={() => refetch()}>
@@ -36,6 +40,7 @@ export function StackList() {
3640
</div>
3741
</div>
3842
<div className="flex flex-col items-center gap-5">
43+
{hasResumeableStack && <ResumeStackBanner setHasResumeableStack={setResumeableStack} />}
3944
<div className="w-full">
4045
{data ? (
4146
<DataTable columns={getStackColumns()} data={data.items} />

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { StackDeploymentProvider } from "@/types/stack";
22
import { Dispatch, SetStateAction, createContext, useContext, useRef, useState } from "react";
3+
import { parseWizardData } from "./persist";
34

45
type Data = {
56
provider?: StackDeploymentProvider;
@@ -15,14 +16,27 @@ type NewInfraFormType = {
1516
formRef: React.RefObject<HTMLFormElement>;
1617
setTimestamp: Dispatch<SetStateAction<string>>;
1718
timestamp: string;
19+
isLoading: boolean;
20+
setIsLoading: Dispatch<SetStateAction<boolean>>;
1821
};
1922

2023
export const NewInfraFormContext = createContext<NewInfraFormType | null>(null);
2124

2225
export function NewInfraFormProvider({ children }: { children: React.ReactNode }) {
26+
const { success, data: parsedData } = parseWizardData();
27+
2328
const [isNextButtonDisabled, setIsNextButtonDisabled] = useState(false);
24-
const [data, setData] = useState<Data>({});
25-
const [timestamp, setTimestamp] = useState<string>("");
29+
const [isLoading, setIsLoading] = useState(success ? true : false);
30+
const [data, setData] = useState<Data>(
31+
success
32+
? {
33+
location: parsedData.location,
34+
provider: parsedData.provider,
35+
stackName: parsedData.stackName
36+
}
37+
: {}
38+
);
39+
const [timestamp, setTimestamp] = useState<string>(success ? parsedData.timestamp : "");
2640
const formRef = useRef<HTMLFormElement>(null);
2741

2842
return (
@@ -32,6 +46,8 @@ export function NewInfraFormProvider({ children }: { children: React.ReactNode }
3246
setIsNextButtonDisabled,
3347
data,
3448
setData,
49+
isLoading,
50+
setIsLoading,
3551
formRef,
3652
timestamp,
3753
setTimestamp

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@ import { Dispatch, SetStateAction, createContext, useContext, useState } from "r
33
type NewInfraWizardType = {
44
currentStep: number;
55
setCurrentStep: Dispatch<SetStateAction<number>>;
6-
isLoading: boolean;
7-
setIsLoading: Dispatch<SetStateAction<boolean>>;
86
};
97

108
export const NewInfraWizardContext = createContext<NewInfraWizardType | null>(null);
119

12-
export function NewInfraWizardProvider({ children }: { children: React.ReactNode }) {
13-
const [currentStep, setCurrentStep] = useState(1);
14-
const [isLoading, setIsLoading] = useState(false);
10+
export function NewInfraWizardProvider({
11+
children,
12+
initialStep = 1
13+
}: {
14+
children: React.ReactNode;
15+
initialStep?: number;
16+
}) {
17+
const [currentStep, setCurrentStep] = useState(initialStep);
1518

1619
return (
17-
<NewInfraWizardContext.Provider
18-
value={{ currentStep, setCurrentStep, isLoading, setIsLoading }}
19-
>
20+
<NewInfraWizardContext.Provider value={{ currentStep, setCurrentStep }}>
2021
{children}
2122
</NewInfraWizardContext.Provider>
2223
);

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { useQuery } from "@tanstack/react-query";
55
import { Button, Skeleton } from "@zenml-io/react-component-library";
66
import { stackQueries } from "../../../../../../data/stacks";
77
import { useNewInfraFormContext } from "../../NewInfraFormContext";
8-
import { useNewInfraWizardContext } from "../../NewInfraWizardContext";
98
import { GCPCodesnippet, GCPWarning } from "../../Providers/GCP";
9+
import { setWizardData } from "../../persist";
1010

1111
export function DeployButtonPart() {
1212
const { data } = useNewInfraFormContext();
@@ -36,9 +36,7 @@ type DeploymentButtonProps = {
3636
setTimestampBool?: boolean;
3737
};
3838
export function DeploymentButton({ setTimestampBool }: DeploymentButtonProps) {
39-
const { data, setTimestamp } = useNewInfraFormContext();
40-
41-
const { setIsLoading } = useNewInfraWizardContext();
39+
const { data, setTimestamp, setIsLoading } = useNewInfraFormContext();
4240

4341
const stackDeploymentConfig = useQuery({
4442
...stackQueries.stackDeploymentConfig({
@@ -57,7 +55,14 @@ export function DeploymentButton({ setTimestampBool }: DeploymentButtonProps) {
5755
}
5856

5957
function handleClick() {
60-
setTimestampBool && setTimestamp(new Date().toISOString().slice(0, -1)!);
58+
const timestamp = new Date().toISOString().slice(0, -1);
59+
setTimestampBool && setTimestamp(timestamp);
60+
setWizardData({
61+
location: data.location || "",
62+
provider: data.provider || "aws",
63+
stackName: data.stackName!,
64+
timestamp
65+
});
6166
setIsLoading(true);
6267
}
6368

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

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import { useEffect, useState } from "react";
99
import { useNewInfraFormContext } from "../../NewInfraFormContext";
1010
import { useNewInfraWizardContext } from "../../NewInfraWizardContext";
1111
import { CloudComponents } from "../../Providers";
12-
import { DeploymentButton } from "./ButtonStep";
1312
import { GCPCodesnippet } from "../../Providers/GCP";
13+
import { clearWizardData } from "../../persist";
14+
import { DeploymentButton } from "./ButtonStep";
1415

1516
export function ProvisioningStep() {
1617
const { data, timestamp, setIsNextButtonDisabled } = useNewInfraFormContext();
@@ -32,6 +33,7 @@ export function ProvisioningStep() {
3233

3334
useEffect(() => {
3435
if (stackData) {
36+
clearWizardData();
3537
setCurrentStep((prev) => prev + 1);
3638
setIsNextButtonDisabled(false);
3739
}
@@ -52,7 +54,6 @@ function LoadingHeader() {
5254
const { data } = useNewInfraFormContext();
5355
return (
5456
<section className="space-y-5 border-b border-theme-border-moderate pb-5">
55-
<Warning />
5657
<Box className="flex items-center justify-between gap-4 px-6 py-5">
5758
<div className="flex items-start gap-3">
5859
<CloudProviderIcon provider={data.provider!} className="h-6 w-6 shrink-0" />
@@ -132,15 +133,3 @@ function ItTakesLongerBox({ isReady }: { isReady: boolean }) {
132133
</InfoBox>
133134
);
134135
}
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/Deploy/index.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { useEffect } from "react";
22
import { useNewInfraFormContext } from "../../NewInfraFormContext";
3-
import { useNewInfraWizardContext } from "../../NewInfraWizardContext";
43
import { WizardStepWrapper } from "../../Wizard";
5-
import { ProvisioningStep } from "./ProvisioningStep";
64
import { DeployButtonPart } from "./ButtonStep";
5+
import { ProvisioningStep } from "./ProvisioningStep";
76

87
export function DeployStep() {
98
return (
@@ -14,8 +13,7 @@ export function DeployStep() {
1413
}
1514

1615
function DisplaySteps() {
17-
const { isLoading } = useNewInfraWizardContext();
18-
const { setIsNextButtonDisabled } = useNewInfraFormContext();
16+
const { setIsNextButtonDisabled, isLoading } = useNewInfraFormContext();
1917
useEffect(() => {
2018
setIsNextButtonDisabled(true);
2119
}, []);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { InputHTMLAttributes, ReactNode, forwardRef, useEffect } from "react";
55
import { useForm } from "react-hook-form";
66
import { useNewInfraFormContext } from "../NewInfraFormContext";
77
import { WizardStepWrapper } from "../Wizard";
8-
import { ProviderForm, providerSchema } from "./schemas";
8+
import { ProviderForm, providerFormSchema } from "./schemas";
99

1010
export function ProviderStep() {
1111
const { formRef, setIsNextButtonDisabled, setData, data } = useNewInfraFormContext();
@@ -15,7 +15,7 @@ export function ProviderStep() {
1515
handleSubmit,
1616
formState: { isValid }
1717
} = useForm<ProviderForm>({
18-
resolver: zodResolver(providerSchema),
18+
resolver: zodResolver(providerFormSchema),
1919
defaultValues: { provider: data.provider }
2020
});
2121

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { z } from "zod";
22

3-
export const providerSchema = z.object({
4-
provider: z.enum(["aws", "gcp"])
3+
export const providerSchema = z.enum(["aws", "gcp"]);
4+
5+
export const providerFormSchema = z.object({
6+
provider: providerSchema
57
});
68

7-
export type ProviderForm = z.infer<typeof providerSchema>;
9+
export type ProviderForm = z.infer<typeof providerFormSchema>;
810

911
export const configurationSchema = z.object({
1012
region: z.string().trim().min(1),

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

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { routes } from "@/router/routes";
22
import { Box, Button } from "@zenml-io/react-component-library";
33
import { ReactNode } from "react";
4-
import { Link, useNavigate, useSearchParams } from "react-router-dom";
4+
import { useNavigate, useSearchParams } from "react-router-dom";
55
import { useNewInfraFormContext } from "./NewInfraFormContext";
66
import { useNewInfraWizardContext } from "./NewInfraWizardContext";
77
import { ConfigurationStep } from "./Steps/Configuration";
88
import { DeployStep } from "./Steps/Deploy";
99
import { ProviderStep } from "./Steps/Provider";
1010
import { SuccessStep } from "./Steps/Success/SuccessStep";
11+
import { clearWizardData } from "./persist";
1112

1213
export function CreateNewInfraWizard() {
1314
const { currentStep } = useNewInfraWizardContext();
@@ -40,6 +41,7 @@ function NextButton() {
4041
});
4142

4243
if (currentStep === maxSteps) {
44+
clearWizardData();
4345
navigate(isFromOnboarding ? routes.onboarding : routes.stacks.overview);
4446
}
4547
}
@@ -56,29 +58,16 @@ function NextButton() {
5658
);
5759
}
5860

59-
// function PrevButton() {
60-
// const { setCurrentStep, currentStep } = useNewInfraWizardContext();
61-
62-
// function previousStep() {
63-
// setCurrentStep((prev) => {
64-
// if (prev > 1) {
65-
// return prev - 1;
66-
// }
67-
// return prev;
68-
// });
69-
// }
70-
71-
// return (
72-
// <Button disabled={currentStep === 1} onClick={previousStep} emphasis="subtle" size="md">
73-
// Prev
74-
// </Button>
75-
// );
76-
// }
77-
7861
function CancelButton() {
62+
const navigate = useNavigate();
63+
64+
function cancel() {
65+
clearWizardData();
66+
navigate(routes.stacks.create.index);
67+
}
7968
return (
80-
<Button asChild intent="secondary" size="md">
81-
<Link to={routes.stacks.create.index}>Cancel</Link>
69+
<Button onClick={() => cancel()} intent="secondary" size="md">
70+
Cancel
8271
</Button>
8372
);
8473
}
@@ -92,7 +81,6 @@ export function WizardStepWrapper({ children, title }: { children: ReactNode; ti
9281
<div className="p-5">{children}</div>
9382
<div className="flex items-center justify-end gap-2 border-t border-theme-border-moderate p-5">
9483
<CancelButton />
95-
{/* <PrevButton /> */}
9684
<NextButton />
9785
</div>
9886
</Box>

0 commit comments

Comments
 (0)