Skip to content

Commit db5a33e

Browse files
committed
feat: Cluster progress interstitial
https://harperdb.atlassian.net/browse/STUDIO-527
1 parent 29c082c commit db5a33e

File tree

12 files changed

+99
-42
lines changed

12 files changed

+99
-42
lines changed

src/config/clusterStatuses.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const activeClusterStatuses = ['RUNNING'];
2+
export const deletedClusterStatuses = ['TERMINATING', 'TERMINATED', 'REMOVED'];

src/features/auth/ClusterInstanceSignIn.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export function ClusterInstanceSignIn() {
112112
}
113113

114114
if (cluster?.resetPassword) {
115-
return <Navigate to={instance ? '../../set-password' : '../set-password'} replace={true} />;
115+
return <Navigate to={`/${cluster.organizationId}/${cluster.id}/finish-setup`} replace={true} />;
116116
}
117117

118118
return (

src/features/cluster/ClusterSetPassword.tsx renamed to src/features/cluster/FinishSetup.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ import { getOperationsUrlForCluster } from '@/lib/urls/getOperationsUrlForCluste
2121
import { zodResolver } from '@hookform/resolvers/zod';
2222
import { useQuery } from '@tanstack/react-query';
2323
import { Navigate, useNavigate, useParams, useRouter, useSearch } from '@tanstack/react-router';
24+
import { ActivityIcon } from 'lucide-react';
2425
import { useCallback, useEffect, useMemo } from 'react';
2526
import { useForm } from 'react-hook-form';
2627
import { toast } from 'sonner';
2728
import { z } from 'zod';
2829

29-
export function ClusterSetPassword() {
30+
export function FinishSetup() {
3031
const { user } = useCloudAuth();
3132
const { clusterId }: { clusterId: string; } = useParams({ strict: false });
3233
const { data: cluster } = useQuery(
@@ -94,6 +95,10 @@ export function ClusterSetPassword() {
9495
<div className="items-center justify-center flex mt-32 py-4 min-h-[calc(100vh-theme(spacing.32))]">
9596
<div className="text-white w-xs">
9697
<h2 className="text-2xl font-light">Create Admin User</h2>
98+
<p className="text-muted-foreground">
99+
You are ready to create your first user in your new cluster. These credentials belong to you alone, and you
100+
can create more users and roles once you create this first one.
101+
</p>
97102
<Form {...methods}>
98103
<form onSubmit={handleSubmit(submitForm)} className="my-4">
99104
<FormField
@@ -145,11 +150,18 @@ export function ClusterSetPassword() {
145150
</FormItem>
146151
)}
147152
/>
153+
148154
<Button disabled={isPending} type="submit" variant="submit" className="w-full my-2 rounded-full">
149155
Create Admin User
150156
</Button>
151157
</form>
152158
</Form>
159+
160+
<p className="text-muted-foreground flex gap-2 align-middle">
161+
<ActivityIcon size={36} className="flex-none" />
162+
<span>These credentials will be used to sign into your cluster directly, providing you a secure connection
163+
from your browser to your cluster.</span>
164+
</p>
153165
</div>
154166
</div>
155167
</>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { useParams } from '@tanstack/react-router';
1717
import { ColumnDef } from '@tanstack/react-table';
1818
import { useMemo } from 'react';
1919

20-
export function ClusterIndex() {
20+
export function Instances() {
2121
const { clusterId }: { clusterId: string; } = useParams({ strict: false });
2222
const { data: cluster, isLoading: clusterIsLoading } = useQuery(
2323
getClusterInfoQueryOptions(clusterId, true),

src/features/cluster/Progress.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { SubNavMenu } from '@/components/SubNavMenu';
2+
import { TextLoadingSkeleton } from '@/components/TextLoadingSkeleton';
3+
import { activeClusterStatuses } from '@/config/clusterStatuses';
4+
import { getClusterInfoQueryOptions } from '@/features/cluster/queries/getClusterInfoQuery';
5+
import { ClusterCardAction } from '@/features/clusters/components/ClusterCardAction';
6+
import { ClusterProgress } from '@/features/clusters/components/ClusterProgress';
7+
import { useQuery } from '@tanstack/react-query';
8+
import { useParams } from '@tanstack/react-router';
9+
import { useMemo } from 'react';
10+
11+
export function Progress() {
12+
const { clusterId }: { organizationId: string; clusterId: string; } = useParams({ strict: false });
13+
const { data: cluster, isLoading: clusterIsLoading } = useQuery(
14+
getClusterInfoQueryOptions(clusterId, 2000),
15+
);
16+
const isActive = useMemo(() => cluster?.status && activeClusterStatuses.includes(cluster.status), [cluster?.status]);
17+
18+
return (
19+
<>
20+
<SubNavMenu />
21+
<div className="mt-32 px-4 pt-4 md:px-12 min-h-[calc(100vh-theme(spacing.32))] flex justify-center">
22+
{clusterIsLoading || !cluster
23+
? <TextLoadingSkeleton />
24+
: (<div className="center text-center max-w-2xl flex flex-col gap-4">
25+
<h1 className="text-xl">Here we go!</h1>
26+
<ClusterProgress cluster={cluster} forceProgressBarVisible={true} />
27+
{!isActive ? (<>
28+
<p>Your cluster is spinning up with the latest changes, including your own DNS records and private
29+
connections. Please wait while we get everything going.</p>
30+
<p className="text-muted-foreground">We will let you know when we are ready for you to connect! In the
31+
meantime, may I suggest a cup of water,
32+
tea or perhaps a hot bean juice? ☕</p>
33+
</>) : (<>
34+
<p>It's ready! Let's set up your secure, browser-to-cluster connection now.</p>
35+
<p className="text-muted-foreground">Did you know you connect straight to your cluster, providing a private secure connection?</p>
36+
</>)}
37+
{isActive && <ClusterCardAction cluster={cluster} />}
38+
</div>)
39+
}
40+
</div>
41+
</>
42+
);
43+
}

src/features/cluster/routes.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import { defaultInstanceRouteUpOne } from '@/config/constants';
22
import { ClusterInstanceSignIn } from '@/features/auth/ClusterInstanceSignIn';
33
import { clusterLayoutRoute } from '@/features/cluster/clusterLayoutRoute';
4-
import { ClusterSetPassword } from '@/features/cluster/ClusterSetPassword';
5-
import { ClusterIndex } from '@/features/cluster/index';
4+
import { FinishSetup } from '@/features/cluster/FinishSetup';
5+
import { Instances } from '@/features/cluster/Instances';
6+
import { Progress } from '@/features/cluster/Progress';
67
import { createRoute, redirect } from '@tanstack/react-router';
78

8-
const clusterIndexRoute = createRoute({
9+
const clusterInstancesRoute = createRoute({
910
getParentRoute: () => clusterLayoutRoute,
1011
path: 'instances',
11-
component: ClusterIndex,
12+
component: Instances,
13+
});
14+
15+
const clusterProgressRoute = createRoute({
16+
getParentRoute: () => clusterLayoutRoute,
17+
path: 'progress',
18+
component: Progress,
1219
});
1320

1421
const clusterSignInRoute = createRoute({
@@ -43,15 +50,16 @@ const instanceSignInRoute = createRoute({
4350
},
4451
});
4552

46-
const clusterSetPasswordRoute = createRoute({
53+
const clusterFinishSetupRoute = createRoute({
4754
getParentRoute: () => clusterLayoutRoute,
48-
path: 'set-password',
49-
component: ClusterSetPassword,
55+
path: 'finish-setup',
56+
component: FinishSetup,
5057
});
5158

5259
export const clusterRoutes = [
53-
clusterIndexRoute,
60+
clusterInstancesRoute,
61+
clusterProgressRoute,
62+
clusterFinishSetupRoute,
5463
clusterSignInRoute,
5564
instanceSignInRoute,
56-
clusterSetPasswordRoute,
5765
];

src/features/clusters/ClustersList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function ClustersList() {
3232
.sort(byClusterStatusThenName) || [], [filterByNameValue, orgInfo?.clusters]);
3333

3434
if (!clusters.length && create) {
35-
return <UpsertCluster embedded={true} />;
35+
return <UpsertCluster />;
3636
}
3737

3838
return (

src/features/clusters/components/ClusterCard.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
DropdownMenuSeparator,
1010
DropdownMenuTrigger,
1111
} from '@/components/ui/dropdownMenu';
12+
import { activeClusterStatuses, deletedClusterStatuses } from '@/config/clusterStatuses';
1213
import { useInstanceClient } from '@/config/useInstanceClient';
1314
import { getClusterInfo } from '@/features/cluster/queries/getClusterInfoQuery';
1415
import { ClusterCardAction } from '@/features/clusters/components/ClusterCardAction';
@@ -28,9 +29,6 @@ import { CopyIcon, Ellipsis } from 'lucide-react';
2829
import { useCallback, useMemo, useState } from 'react';
2930
import { toast } from 'sonner';
3031

31-
const activeClusterStatuses = ['RUNNING'];
32-
const deletedClusterStatuses = ['TERMINATING', 'TERMINATED', 'REMOVED'];
33-
3432
export function ClusterCard({ cluster }: { cluster: Cluster; }) {
3533
const queryClient = useQueryClient();
3634
const operationsUrl = useMemo(() => getOperationsUrlForCluster(cluster), [cluster]);
@@ -188,7 +186,7 @@ export function ClusterCard({ cluster }: { cluster: Cluster; }) {
188186
</CardTitle>
189187
</CardHeader>
190188
<CardContent className="flex items-center justify-between gap-2">
191-
<ClusterProgress cluster={cluster} showProgressBarWhenFullyReady={true} />
189+
<ClusterProgress cluster={cluster} />
192190
{isActive && view && <ClusterCardAction cluster={cluster} />}
193191
</CardContent>
194192

src/features/clusters/components/ClusterCardAction.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Button } from '@/components/ui/button';
12
import { defaultInstanceRoute } from '@/config/constants';
23
import { useInstanceAuth } from '@/hooks/useAuth';
34
import { useOrganizationClusterPermissions } from '@/hooks/usePermissions';
@@ -15,7 +16,7 @@ export function ClusterCardAction({ cluster }: { cluster: Cluster }) {
1516
}
1617

1718
if (!cluster.fqdn) {
18-
return <Link to={`${cluster.id}/instances`} className="text-sm text-nowrap" aria-label={`View ${cluster.name}`} title={`View ${cluster.name}`}>
19+
return <Link to={`/${cluster.organizationId}/${cluster.id}/instances`} className="text-sm text-nowrap" aria-label={`View ${cluster.name}`} title={`View ${cluster.name}`}>
1920
<span className="py-2 hover:border-b-2">
2021
Instances <ArrowRight className="inline-block" />
2122
</span>
@@ -24,10 +25,10 @@ export function ClusterCardAction({ cluster }: { cluster: Cluster }) {
2425

2526
if (isPendingResetPassword) {
2627
if (update) {
27-
return <Link to={`${cluster.id}/set-password`} className="text-sm text-nowrap" aria-label={`Set Password on ${cluster.name}`} title={`Set Password on ${cluster.name}`}>
28-
<span className="py-2 hover:border-b-2">
29-
Set Password <ArrowRight className="inline-block" />
30-
</span>
28+
return <Link to={`/${cluster.organizationId}/${cluster.id}/finish-setup`} className="text-sm text-nowrap" aria-label={`Set Password on ${cluster.name}`} title={`Set Password on ${cluster.name}`}>
29+
<Button variant="positive" className="py-2 hover:border-b-2 animate-bounce">
30+
Finish Setup <ArrowRight className="inline-block" />
31+
</Button>
3132
</Link>;
3233
}
3334
return <span className="py-2 text-nowrap">
@@ -39,13 +40,13 @@ export function ClusterCardAction({ cluster }: { cluster: Cluster }) {
3940
return undefined;
4041
}
4142
if (auth.user) {
42-
return <Link to={`${cluster.id}${defaultInstanceRoute}`} className="text-sm text-nowrap" aria-label={`View ${cluster.name}`} title={`View ${cluster.name}`}>
43+
return <Link to={`/${cluster.organizationId}/${cluster.id}${defaultInstanceRoute}`} className="text-sm text-nowrap" aria-label={`View ${cluster.name}`} title={`View ${cluster.name}`}>
4344
<span className="py-2 hover:border-b-2">
4445
View <ArrowRight className="inline-block" />
4546
</span>
4647
</Link>;
4748
}
48-
return <Link to={`${cluster.id}/sign-in`} className="text-sm text-nowrap" aria-label={`Sign In to ${cluster.name}`} title={`Sign In to ${cluster.name}`}>
49+
return <Link to={`/${cluster.organizationId}/${cluster.id}/sign-in`} className="text-sm text-nowrap" aria-label={`Sign In to ${cluster.name}`} title={`Sign In to ${cluster.name}`}>
4950
<span className="py-2 hover:border-b-2">
5051
Sign In <ArrowRight className="inline-block" />
5152
</span>

src/features/clusters/components/ClusterProgress.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { isBeingUpdated, isPendingUpdate, isRunning } from '@/components/ui/util
22
import { getClusterInfoQueryOptions } from '@/features/cluster/queries/getClusterInfoQuery';
33
import { Cluster } from '@/lib/api.patch';
44
import { countBy } from '@/lib/countBy';
5-
import { sleep } from '@/lib/sleep';
65
import { capitalizeWords } from '@/lib/string/capitalizeWords';
76
import { useQuery } from '@tanstack/react-query';
87
import { useEffect, useMemo, useState } from 'react';
@@ -13,11 +12,11 @@ import { useEffect, useMemo, useState } from 'react';
1312
* 2. Updating (Provisioning or Cloning)
1413
* 3. Running (Running or Updated)
1514
*/
16-
export function ClusterProgress({ cluster, showProgressBarWhenFullyReady }: {
15+
export function ClusterProgress({ cluster, forceProgressBarVisible }: {
1716
cluster: Pick<Cluster, 'status' | 'id'>,
18-
showProgressBarWhenFullyReady: boolean
17+
forceProgressBarVisible?: boolean
1918
}) {
20-
const [showProgress, setShowProgress] = useState(false);
19+
const [showProgress, setShowProgress] = useState(forceProgressBarVisible || false);
2120

2221
const { data: clusterById } = useQuery(
2322
getClusterInfoQueryOptions(showProgress && cluster.id, 2000),
@@ -26,13 +25,8 @@ export function ClusterProgress({ cluster, showProgressBarWhenFullyReady }: {
2625
useEffect(() => {
2726
if (isPendingUpdate(cluster.status) || isBeingUpdated(cluster.status)) {
2827
setShowProgress(true);
29-
} else if (showProgress && !showProgressBarWhenFullyReady) {
30-
const waitingForInstances = clusterById?.instances?.some(instance => isPendingUpdate(instance.status) || isBeingUpdated(instance.status));
31-
if (!waitingForInstances) {
32-
sleep(10_000).then(() => setShowProgress(false));
33-
}
3428
}
35-
}, [showProgress, cluster.status, clusterById?.instances, showProgressBarWhenFullyReady]);
29+
}, [showProgress, cluster.status, clusterById?.instances]);
3630

3731
const updating = useMemo(() => {
3832
const instances = clusterById?.instances ?? [];

0 commit comments

Comments
 (0)