Skip to content

Commit c13644b

Browse files
feat: loading modal (#639)
1 parent 6fb83d6 commit c13644b

File tree

9 files changed

+163
-30
lines changed

9 files changed

+163
-30
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import Dots from "@/assets/icons/dots-horizontal.svg?react";
2+
import Zenml from "@/assets/icons/zenml-icon.svg?react";
3+
import { CloudProviderIcon } from "@/components/ProviderIcon";
4+
import { Tick } from "@/components/Tick";
5+
import { StackDeploymentProvider } from "@/types/stack";
6+
import { Spinner } from "@zenml-io/react-component-library";
7+
import {
8+
Dialog,
9+
DialogContent,
10+
DialogTitle
11+
} from "@zenml-io/react-component-library/components/client";
12+
import { PropsWithChildren, useEffect, useState } from "react";
13+
import { useExistingInfraContext } from "../../ExistingInfraContext";
14+
15+
type Props = {
16+
open: boolean;
17+
loadingComponents: boolean;
18+
};
19+
20+
export function LoadingModal({ open, loadingComponents }: Props) {
21+
const { data } = useExistingInfraContext();
22+
if (!data.connectorConfig?.type) return null;
23+
return (
24+
<Dialog open={open}>
25+
<DialogContent
26+
onPointerDownOutside={(e) => e.preventDefault()}
27+
onEscapeKeyDown={(e) => e.preventDefault()}
28+
className="max-w-[600px]"
29+
>
30+
<div className="flex items-center justify-between border-b border-theme-border-moderate py-2 pl-5 pr-3">
31+
<DialogTitle className="text-text-lg">
32+
Connecting and loading your components...
33+
</DialogTitle>
34+
</div>
35+
<div className="flex h-[200px] items-center justify-center gap-5 bg-primary-50">
36+
<Zenml className="h-[60px] w-[60px]" />
37+
<div className="flex h-[60px] w-[60px] items-center justify-center">
38+
<Dots className="h-5 w-5 fill-theme-text-tertiary" />
39+
</div>
40+
<CloudProviderIcon
41+
className="h-[60px] w-[60px]"
42+
provider={data.connectorConfig.type as StackDeploymentProvider}
43+
/>
44+
</div>
45+
<div className="flex flex-col gap-1 px-7 pb-6 pt-5">
46+
<p className="text-theme-text-secondary">
47+
We're securely connecting to{" "}
48+
<span className="capitalize">{data.connectorConfig?.type}</span> and retrieving your
49+
custom components. This process typically takes about 30-60 seconds.
50+
</p>
51+
<Loaders loadingComponents={loadingComponents} />
52+
</div>
53+
</DialogContent>
54+
</Dialog>
55+
);
56+
}
57+
58+
type LoaderProps = {
59+
loadingComponents: boolean;
60+
};
61+
function Loaders({ loadingComponents }: LoaderProps) {
62+
const [conncetionLoading, setConnectionLoading] = useState(true);
63+
const [authLoading, setAuthLoading] = useState(true);
64+
65+
useEffect(() => {
66+
const connectionTimeout = setTimeout(
67+
() => {
68+
setConnectionLoading(false);
69+
},
70+
Math.floor(Math.random() * 1000) + 500
71+
);
72+
73+
const authTimeout = setTimeout(() => {
74+
setAuthLoading(false);
75+
}, 1500);
76+
77+
return () => {
78+
clearTimeout(connectionTimeout);
79+
clearTimeout(authTimeout);
80+
};
81+
}, []);
82+
83+
return (
84+
<ul className="mt-3 space-y-3 text-theme-text-secondary">
85+
<ListItem>
86+
<Indicator loading={conncetionLoading} />
87+
Establishing a secure connection
88+
</ListItem>
89+
<ListItem>
90+
<Indicator loading={authLoading} />
91+
Authenticating your account
92+
</ListItem>
93+
<ListItem>
94+
<Indicator loading={loadingComponents} />
95+
Fetching your existing components
96+
</ListItem>
97+
</ul>
98+
);
99+
}
100+
101+
type IndicatorProps = {
102+
loading: boolean;
103+
};
104+
function Indicator({ loading }: IndicatorProps) {
105+
if (loading) {
106+
return <Spinner className="h-5 w-5 border-2" />;
107+
}
108+
return <Tick className="h-5 w-5" tickClasses="w-3 h-3" />;
109+
}
110+
111+
function ListItem({ children }: PropsWithChildren) {
112+
return <li className="flex items-center gap-1">{children}</li>;
113+
}

src/app/stacks/create/existing-infrastructure/steps/connect/NewConnectorForm.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@ import { useFormContext } from "react-hook-form";
22
import { StackName } from "../../../components/StackName";
33
import { AuthenticationMethod } from "./AuthenticationMethod";
44
import { ConnectorConfig } from "./Configuration";
5+
import { LoadingModal } from "./LoadingModal";
56
import { useNewConnector } from "./useNewConnector";
67

78
export function NewConnector() {
8-
const { handleSubmit } = useFormContext();
9-
const { handleFormSubmit } = useNewConnector();
9+
const {
10+
handleSubmit,
11+
formState: { isSubmitting }
12+
} = useFormContext();
13+
const { handleFormSubmit, fullStackResources, loadingComponents } = useNewConnector();
1014

1115
return (
1216
<form onSubmit={handleSubmit(handleFormSubmit)} id="connect-form">
17+
<LoadingModal
18+
loadingComponents={loadingComponents}
19+
open={isSubmitting || fullStackResources.isPending}
20+
/>
1321
<AuthenticationMethod />
1422
<StackName />
1523
<ConnectorConfig />

src/app/stacks/create/existing-infrastructure/steps/connect/index.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,17 @@ export function ConnectCloudStep() {
3131

3232
function NextButton() {
3333
const {
34-
formState: { isValid }
34+
formState: { isValid, isSubmitting }
3535
} = useFormContext();
3636
const isMutating = !!useIsMutating();
3737
return (
3838
<Button
3939
className="justify-center gap-2"
40-
disabled={!isValid || isMutating}
40+
disabled={!isValid || isMutating || isSubmitting}
4141
form="connect-form"
4242
size="md"
4343
>
44-
{isMutating && (
45-
<div
46-
role="alert"
47-
aria-busy="true"
48-
className="full h-[20px] w-[20px] animate-spin rounded-rounded border-2 border-theme-text-negative border-b-theme-text-brand"
49-
></div>
50-
)}
51-
{isMutating ? "Loading..." : "Next"}
44+
{isMutating || isSubmitting ? "Loading..." : "Next"}
5245
</Button>
5346
);
5447
}

src/app/stacks/create/existing-infrastructure/steps/connect/useNewConnector.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { useWizardContext } from "@/context/WizardContext";
22
import { useConnectorFullstackResources } from "@/data/service-connectors/resources-info";
3+
import { sleep } from "@/lib/common";
34
import { useToast } from "@zenml-io/react-component-library";
45
import { produce } from "immer";
5-
import { useExistingInfraContext } from "../../ExistingInfraContext";
6+
import { useState } from "react";
67
import { FieldValues, SubmitHandler } from "react-hook-form";
8+
import { useExistingInfraContext } from "../../ExistingInfraContext";
79

810
export function useNewConnector() {
911
const { setCurrentStep } = useWizardContext();
12+
const [loadingComponents, setIsLoadingComponents] = useState(true);
1013
const { toast } = useToast();
1114
const { setData, data } = useExistingInfraContext();
1215
const fullStackResources = useConnectorFullstackResources({
@@ -16,6 +19,8 @@ export function useNewConnector() {
1619
draft.fullstackResources = data;
1720
})
1821
);
22+
setIsLoadingComponents(false);
23+
await sleep(200);
1924
setCurrentStep((prev) => prev + 1);
2025
},
2126
onError: (e) => {
@@ -34,7 +39,7 @@ export function useNewConnector() {
3439
rest[key] === "" ||
3540
rest[key] === null ||
3641
rest[key] === undefined ||
37-
(rest[key] instanceof Array && rest[key].length === 0)
42+
(Array.isArray(rest[key]) && rest[key].length === 0)
3843
) {
3944
delete rest[key];
4045
}
@@ -57,5 +62,5 @@ export function useNewConnector() {
5762
});
5863
};
5964

60-
return { handleFormSubmit };
65+
return { handleFormSubmit, fullStackResources, loadingComponents };
6166
}
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
1+
import AlertCircle from "@/assets/icons/alert-circle.svg?react";
12
import { useWizardContext } from "@/context/WizardContext";
3+
import { stackQueries } from "@/data/stacks";
4+
import { useCreateFullstack } from "@/data/stacks/full-stack-create";
5+
import { ComponentResourceInfo } from "@/types/service-connectors";
6+
import { useQueryClient } from "@tanstack/react-query";
7+
import { useToast } from "@zenml-io/react-component-library";
28
import { produce } from "immer";
39
import { useExistingInfraContext } from "../../ExistingInfraContext";
410
import { ContainerRegistryFormType } from "./schema";
5-
import { useQueryClient } from "@tanstack/react-query";
6-
import { ComponentResourceInfo } from "@/types/service-connectors";
7-
import { useCreateFullstack } from "@/data/stacks/full-stack-create";
8-
import { stackQueries } from "@/data/stacks";
911

1012
export function useContainerRegistries() {
1113
const { data, setData } = useExistingInfraContext();
1214
const queryClient = useQueryClient();
15+
const { toast } = useToast();
1316

1417
const { setCurrentStep } = useWizardContext();
1518
const { mutate } = useCreateFullstack({
19+
onError: (error) => {
20+
if (error instanceof Error) {
21+
toast({
22+
status: "error",
23+
emphasis: "subtle",
24+
icon: <AlertCircle className="h-5 w-5 shrink-0 fill-error-700" />,
25+
description: error.message,
26+
rounded: true
27+
});
28+
}
29+
},
1630
onSuccess: async (res) => {
1731
setData((prev) =>
1832
produce(prev, (draft) => {

src/app/stacks/create/existing-infrastructure/steps/orchestrator/Form.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,12 @@ export function OrchestratorForm() {
4949
)
5050
);
5151
}
52-
if (orchestrator.flavor !== kubernetesKey)
52+
if (
53+
orchestrator.flavor !== kubernetesKey &&
54+
orchestrator.accessible_by_service_connector.length > 0
55+
)
5356
setValue("resourceId", orchestrator.accessible_by_service_connector[0]);
57+
5458
onChange(e);
5559
}}
5660
{...restField}

src/components/form/DynamicField.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,11 @@ function DynamicArray({ name, schema, isOptional }: DynamicFieldProps) {
6464
</label>
6565
<ul className="space-y-5">
6666
{fields.map((field, index) => (
67-
<li className="flex w-full items-center gap-4" key={field.id}>
67+
<li className="flex w-full items-center gap-4 [&>div]:w-full" key={field.id}>
6868
<Controller
6969
defaultValue=""
7070
name={`${name}.${index}`}
71-
render={({ field }) => (
72-
<Input
73-
// wrapperClasses="w-full"
74-
className="w-full flex-1"
75-
inputSize="md"
76-
{...field}
77-
/>
78-
)}
71+
render={({ field }) => <Input className="w-full flex-1" inputSize="md" {...field} />}
7972
/>
8073
<Button
8174
className="h-7 w-7"

src/context/WizardContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function WizardProvider({
2828
export function useWizardContext() {
2929
const context = useContext(WizardContext);
3030
if (context === null) {
31-
throw new Error("useWizardContext must be used within an AuthProvider");
31+
throw new Error("useWizardContext must be used within a WizardProvider");
3232
}
3333
return context;
3434
}

src/lib/common.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function sleep(ms: number) {
2+
return new Promise((resolve) => setTimeout(resolve, ms));
3+
}

0 commit comments

Comments
 (0)