Skip to content

Commit e141d37

Browse files
committed
feat: Utilize Fabric Connect when possible
https://harperdb.atlassian.net/browse/STUDIO-583
1 parent 8830183 commit e141d37

File tree

5 files changed

+133
-52
lines changed

5 files changed

+133
-52
lines changed

src/config/getInstanceClient.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { apiClient } from '@/config/apiClient';
12
import { authStore, EntityIds, OverallAppSignIn } from '@/features/auth/store/authStore';
23
import { rejectReplicationFailures } from '@/lib/api/replication';
34
import axios from 'axios';
@@ -9,7 +10,7 @@ interface InstanceClient {
910
secure?: boolean;
1011
}
1112

12-
export function getInstanceClient({ id = OverallAppSignIn, operationsUrl, port, secure }: InstanceClient = {}) {
13+
export function getInstanceClient({ id = OverallAppSignIn, operationsUrl, port, secure, forceFabricConnect }: InstanceClient & { forceFabricConnect?: boolean} = {}) {
1314
let baseURL = operationsUrl || authStore.getOperationsUrl(id);
1415
if (baseURL) {
1516
if (port || secure !== undefined) {
@@ -23,10 +24,21 @@ export function getInstanceClient({ id = OverallAppSignIn, operationsUrl, port,
2324
baseURL = newURL.toString();
2425
}
2526
}
26-
const auth = authStore.checkForBasicAuth(id);
27+
28+
const fabricConnect = forceFabricConnect || authStore.checkForFabricConnect(id);
29+
if (fabricConnect) {
30+
if (id.startsWith('clu-')) {
31+
baseURL = apiClient.defaults.baseURL + `/Cluster/${id}/operation`;
32+
} else if (id.startsWith('ins-')) {
33+
baseURL = apiClient.defaults.baseURL + `/HDBInstance/${id}/operation`;
34+
}
35+
}
36+
37+
const basicAuth = authStore.checkForBasicAuth(id);
38+
2739
const client = axios.create({
28-
auth,
29-
withCredentials: !auth,
40+
auth: fabricConnect ? undefined : basicAuth,
41+
withCredentials: fabricConnect || !basicAuth,
3042
timeout: 15000,
3143
headers: {
3244
'Content-Type': 'application/json',

src/features/auth/store/authStore.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class AuthStore {
4545

4646
private readonly potentiallyAuthenticatedKey = 'Studio:PotentiallyAuthenticated';
4747
private readonly basicAuthKeyPrefix = 'Studio:BasicAuth:';
48+
private readonly fabricConnectKeyPrefix = 'Studio:FabricConnect:';
4849
private readonly potentiallyAuthenticated: Record<EntityIds, AuthenticatedConnectionKey>;
4950
private readonly checkedAuthentication: Record<EntityIds, boolean> = {};
5051
private readonly allConnections: Record<EntityIds, AuthenticatedConnection> = {};
@@ -113,6 +114,10 @@ class AuthStore {
113114
if (!id || !key) {
114115
return;
115116
}
117+
return this.setUserForIdAndKey(id, key, user);
118+
}
119+
120+
public setUserForIdAndKey(id: EntityIds, key: AuthenticatedConnectionKey, user: AuthenticatedConnection['user']): void {
116121
if (user) {
117122
this.flagKeyAsSignedIn(id, key);
118123
} else {
@@ -123,8 +128,7 @@ class AuthStore {
123128

124129
public updateUserForEntity(entity: EntityTypes, changes: Partial<AuthenticatedConnection['user']>): void {
125130
const id = this.calculateIdFromEntity(entity);
126-
const key = this.calculateKeyFromEntity(entity);
127-
if (!id || !key) {
131+
if (!id) {
128132
return;
129133
}
130134
const connection = this.getConnectionById(id);
@@ -154,10 +158,18 @@ class AuthStore {
154158
}
155159

156160
public flagForBasicAuth(id: EntityIds, credentials: null | { username: string; password: string; }) {
157-
if (credentials === null) {
161+
if (credentials !== null) {
162+
localStorage.setItem(this.basicAuthKeyPrefix + id, btoa(JSON.stringify(credentials)));
163+
} else {
158164
localStorage.removeItem(this.basicAuthKeyPrefix + id);
165+
}
166+
}
167+
168+
public flagForFabricConnect(id: EntityIds, toggled: boolean) {
169+
if (toggled) {
170+
localStorage.setItem(this.fabricConnectKeyPrefix + id, 'true');
159171
} else {
160-
localStorage.setItem(this.basicAuthKeyPrefix + id, btoa(JSON.stringify(credentials)));
172+
localStorage.removeItem(this.fabricConnectKeyPrefix + id);
161173
}
162174
}
163175

@@ -166,6 +178,10 @@ class AuthStore {
166178
return value ? JSON.parse(atob(value)) : undefined;
167179
}
168180

181+
public checkForFabricConnect(id: EntityIds): boolean {
182+
return localStorage.getItem(this.fabricConnectKeyPrefix + id) === 'true';
183+
}
184+
169185
public async signOutFromPotentiallyAuthenticatedInstances() {
170186
for (const entityId in this.potentiallyAuthenticated) {
171187
this.allConnections[entityId].user = null;

src/features/clusters/components/ClusterCard.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export function ClusterCard({ cluster }: { cluster: Cluster; }) {
5050

5151
const isActive = useMemo(() => cluster.status && activeClusterStatuses.includes(cluster.status), [cluster.status]);
5252
const isSelfManaged = clusterIsSelfManaged(cluster);
53+
const isFabricConnect = authStore.checkForFabricConnect(cluster.id);
54+
const isDirectConnect = !isFabricConnect && !!auth.user;
5355
const isTerminated = useMemo(
5456
() => cluster.status && deletedClusterStatuses.includes(cluster.status),
5557
[cluster.status],
@@ -150,7 +152,7 @@ export function ClusterCard({ cluster }: { cluster: Cluster; }) {
150152
Copy API URL
151153
</DropdownMenuItem>
152154
),
153-
isActive && view && !!operationsUrl && !auth.isLoading && auth.user && (
155+
isActive && view && !!operationsUrl && !auth.isLoading && isDirectConnect && (
154156
<DropdownMenuItem onClick={onSignOutClick} disabled={signingOut}>
155157
Direct Sign Out
156158
</DropdownMenuItem>

src/features/clusters/components/ClusterCardAction.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Button } from '@/components/ui/button';
22
import { defaultInstanceRoute } from '@/config/constants';
3+
import { authStore } from '@/features/auth/store/authStore';
34
import { useInstanceAuth } from '@/hooks/useAuth';
45
import { useOrganizationClusterPermissions } from '@/hooks/usePermissions';
56
import { Cluster } from '@/lib/api.patch';
@@ -10,6 +11,8 @@ import { ArrowRight } from 'lucide-react';
1011
export function ClusterCardAction({ cluster }: { cluster: Cluster }) {
1112
const auth = useInstanceAuth(cluster.id);
1213
const { view, update } = useOrganizationClusterPermissions(cluster.organizationId, cluster.id);
14+
const isFabricConnect = authStore.checkForFabricConnect(cluster.id);
15+
const isDirectConnect = !isFabricConnect && !!auth.user;
1316

1417
if (!view) {
1518
return undefined;
@@ -40,7 +43,7 @@ export function ClusterCardAction({ cluster }: { cluster: Cluster }) {
4043
return undefined;
4144
}
4245

43-
if (auth.user) {
46+
if (isDirectConnect) {
4447
return <Link to={`/${cluster.organizationId}/${cluster.id}${defaultInstanceRoute}`} className="text-sm text-nowrap" aria-label={`View ${cluster.name}`} title={`View ${cluster.name}`}>
4548
<span className="py-2 hover:border-b-2">
4649
Direct Connect <ArrowRight className="inline-block" />
Lines changed: 90 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,103 @@
1+
import { getInstanceClient } from '@/config/getInstanceClient';
12
import { getInstanceClientIdFromParams } from '@/config/useInstanceClient';
3+
import {
4+
AuthenticatedCloudConnection,
5+
AuthenticatedConnection,
6+
authStore,
7+
OverallAppSignIn,
8+
} from '@/features/auth/store/authStore';
29
import { clusterLayoutRoute } from '@/features/cluster/clusterLayoutRoute';
310
import { InstanceLayout } from '@/features/instance/InstanceLayout';
11+
import { getInstanceUserInfo } from '@/features/instance/operations/queries/getInstanceUserInfo';
412
import { getRegistrationInfoQueryOptions } from '@/features/instance/operations/queries/getRegistrationInfo';
13+
import { getOrganizationClusterInstancePermissions, getOrganizationClusterPermissions } from '@/hooks/usePermissions';
514
import { buildRedirectInSearch } from '@/lib/urls/buildRedirectInSearch';
615
import { dashboardLayout } from '@/router/dashboardRoute';
16+
import { QueryClient } from '@tanstack/react-query';
717
import { createRoute, redirect } from '@tanstack/react-router';
818

919
export function createInstanceLayoutRoute(mode: 'local' | 'cluster' | 'instance') {
10-
if (mode === 'local') {
11-
return createRoute({
12-
getParentRoute: () => dashboardLayout,
13-
id: '_instanceLayout',
14-
component: InstanceLayout,
15-
loader: async ({ context, params }) => {
16-
const operationsParams = getInstanceClientIdFromParams(params);
17-
return context.queryClient.ensureQueryData(getRegistrationInfoQueryOptions(operationsParams));
18-
},
19-
});
20+
switch (mode) {
21+
case 'local':
22+
return createRoute({
23+
getParentRoute: () => dashboardLayout,
24+
id: '_instanceLayout',
25+
component: InstanceLayout,
26+
loader: registrationInfoLoader,
27+
});
28+
case 'cluster':
29+
return createRoute({
30+
getParentRoute: () => clusterLayoutRoute,
31+
id: '_instanceLayout',
32+
component: InstanceLayout,
33+
beforeLoad: checkClusterInstanceAuthenticationBeforeLoad,
34+
loader: registrationInfoLoader,
35+
});
36+
case 'instance':
37+
return createRoute({
38+
getParentRoute: () => clusterLayoutRoute,
39+
path: 'instance/$instanceId',
40+
component: InstanceLayout,
41+
beforeLoad: checkClusterInstanceAuthenticationBeforeLoad,
42+
loader: registrationInfoLoader,
43+
});
2044
}
21-
if (mode === 'cluster') {
22-
return createRoute({
23-
getParentRoute: () => clusterLayoutRoute,
24-
id: '_instanceLayout',
25-
component: InstanceLayout,
26-
beforeLoad: async ({ context, params }) => {
27-
const auth = context.authentication[params.clusterId];
28-
if (!auth || (!auth.isLoading && !auth.user)) {
29-
const to = `/${params.organizationId}/${params.clusterId}/sign-in`;
30-
throw redirect({ to, search: buildRedirectInSearch() });
31-
}
32-
},
33-
loader: async ({ context, params }) => {
34-
const operationsParams = getInstanceClientIdFromParams(params);
35-
return context.queryClient.ensureQueryData(getRegistrationInfoQueryOptions(operationsParams));
36-
},
45+
}
46+
47+
async function checkClusterInstanceAuthenticationBeforeLoad({
48+
context,
49+
params,
50+
}: {
51+
context: { authentication: Record<string, AuthenticatedConnection> },
52+
params: { organizationId: string; clusterId: string, instanceId?: string }
53+
}) {
54+
// Are we already authenticated?
55+
const auth = context.authentication[params.instanceId || params.clusterId];
56+
if (auth?.isLoading || auth?.user) {
57+
return;
58+
}
59+
60+
// See if we can Fabric Connect.
61+
const overallAuth = context.authentication[OverallAppSignIn] as AuthenticatedCloudConnection;
62+
if (overallAuth?.isLoading) {
63+
return;
64+
}
65+
const { update } = params.instanceId
66+
? getOrganizationClusterInstancePermissions(overallAuth.user, params.organizationId, params.clusterId)
67+
: getOrganizationClusterPermissions(overallAuth.user, params.organizationId, params.clusterId);
68+
if (update) {
69+
const entityId = params.instanceId || params.clusterId;
70+
const instanceClient = getInstanceClient({
71+
id: entityId,
72+
forceFabricConnect: true,
3773
});
74+
try {
75+
const user = await getInstanceUserInfo({ instanceClient });
76+
authStore.setUserForIdAndKey(
77+
entityId,
78+
instanceClient.defaults.baseURL!,
79+
user,
80+
);
81+
authStore.flagForFabricConnect(entityId, true);
82+
return;
83+
} catch (err) {
84+
console.error('Fabric Connect not established', err);
85+
}
3886
}
39-
return createRoute({
40-
getParentRoute: () => clusterLayoutRoute,
41-
path: 'instance/$instanceId',
42-
component: InstanceLayout,
43-
beforeLoad: async ({ context, params }) => {
44-
const auth = context.authentication[params.instanceId];
45-
if (!auth || (!auth.isLoading && !auth.user)) {
46-
const to = `/${params.organizationId}/${params.clusterId}/instance/${params.instanceId}/sign-in`;
47-
throw redirect({ to, search: buildRedirectInSearch() });
48-
}
49-
},
50-
loader: async ({ context, params }) => {
51-
const operationsParams = getInstanceClientIdFromParams(params);
52-
return context.queryClient.ensureQueryData(getRegistrationInfoQueryOptions(operationsParams));
53-
},
54-
});
87+
88+
const to = `/${params.organizationId}/${params.clusterId}/`
89+
+ (params.instanceId ? `instance/${params.instanceId}` : '')
90+
+ `/sign-in`;
91+
throw redirect({ to, search: buildRedirectInSearch() });
92+
}
93+
94+
function registrationInfoLoader({
95+
context,
96+
params,
97+
}: {
98+
context: { queryClient: QueryClient },
99+
params: { organizationId?: string; clusterId?: string, instanceId?: string },
100+
}) {
101+
const operationsParams = getInstanceClientIdFromParams(params);
102+
return context.queryClient.ensureQueryData(getRegistrationInfoQueryOptions(operationsParams));
55103
}

0 commit comments

Comments
 (0)