Skip to content

Commit eea71b4

Browse files
authored
sandbox(fix): open trial after signup (#895)
* open trial after signup
1 parent 717a32b commit eea71b4

7 files changed

Lines changed: 239 additions & 72 deletions

File tree

workspaces/sandbox/make/sandbox.mk

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ generate-env:
3535
start-rhdh-local: clone-rhdh-local generate-env
3636
rm -rf plugins/sandbox/dist-dynamic
3737
rm -rf red-hat-developer-hub-backstage-plugin-sandbox
38-
npx @janus-idp/cli@3.3.1 package package-dynamic-plugins --export-to .
38+
podman run -it --rm -w /home -v $(PWD):/home node:20.19.2 bash -c "yarn install && npx --yes @janus-idp/cli@3.3.1 package package-dynamic-plugins --export-to . && exit"
3939
cp -r red-hat-developer-hub-backstage-plugin-sandbox $(RHDH_LOCAL_DIR)/local-plugins/
4040
cp deploy/base/app-config.yaml $(RHDH_LOCAL_DIR)/configs/app-config/app-config.yaml
4141
cp deploy/base/dynamic-plugins.yaml $(RHDH_LOCAL_DIR)/configs/dynamic-plugins/dynamic-plugins.override.yaml

workspaces/sandbox/plugins/sandbox/src/components/Modals/__tests__/PhoneVerificationModal.test.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ describe('PhoneVerificationModal', () => {
6666
ansibleStatus: AnsibleStatus.UNKNOWN,
6767
verificationRequired: false,
6868
userData: undefined,
69-
fetchError: null,
70-
signupError: null,
7169
signupUser: jest.fn(),
7270
refetchAAP: jest.fn(),
7371
ansibleData: undefined,

workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogCard.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import { AnsibleStatus } from '../../utils/aap-utils';
3636
import { useApi } from '@backstage/core-plugin-api';
3737
import { aapApiRef, kubeApiRef } from '../../api';
3838
import { Product } from './productData';
39+
import { signupDataToStatus } from '../../utils/register-utils';
40+
import { productsURLMapping } from '../../hooks/useProductURLs';
3941

4042
type SandboxCatalogCardProps = {
4143
id: Product;
@@ -107,14 +109,12 @@ export const SandboxCatalogCard: React.FC<SandboxCatalogCardProps> = ({
107109
const theme = useTheme();
108110
const kubeApi = useApi(kubeApiRef);
109111
const aapApi = useApi(aapApiRef);
112+
let { userData, userFound, userReady, verificationRequired } =
113+
useSandboxContext();
110114
const {
111-
userData,
112115
ansibleData,
113116
ansibleStatus,
114117
signupUser,
115-
userFound,
116-
userReady,
117-
verificationRequired,
118118
refetchUserData,
119119
refetchAAP,
120120
} = useSandboxContext();
@@ -158,6 +158,7 @@ export const SandboxCatalogCard: React.FC<SandboxCatalogCardProps> = ({
158158

159159
const handleTryButtonClick = async (pdt: Product) => {
160160
// User is not yet signed up
161+
let urlToOpen = link;
161162
if (!userFound) {
162163
signupUser();
163164

@@ -171,9 +172,19 @@ export const SandboxCatalogCard: React.FC<SandboxCatalogCardProps> = ({
171172

172173
try {
173174
// Fetch the latest user data and check if user is found
174-
const isUserFound = await refetchUserData();
175-
if (isUserFound) {
176-
break;
175+
userData = await refetchUserData();
176+
if (userData) {
177+
userFound = true;
178+
const userStatus = signupDataToStatus(userData);
179+
verificationRequired = userStatus === 'verify';
180+
userReady = userStatus === 'ready';
181+
// if user is ready or verification is required we can stop fetching the data
182+
if (userReady || verificationRequired) {
183+
const productURLs = productsURLMapping(userData);
184+
// find the link to open if any
185+
urlToOpen = productURLs.find(pu => pu.id === id)?.url || '';
186+
break;
187+
}
177188
}
178189
} catch (error) {
179190
// eslint-disable-next-line no-console
@@ -193,6 +204,8 @@ export const SandboxCatalogCard: React.FC<SandboxCatalogCardProps> = ({
193204
await handleAAPInstance();
194205
refetchAAP();
195206
setAnsibleCredsModalOpen(true);
207+
} else if (userFound && userReady && urlToOpen) {
208+
window.open(urlToOpen, '_blank');
196209
}
197210
showGreenCorner();
198211
};
@@ -235,6 +248,7 @@ export const SandboxCatalogCard: React.FC<SandboxCatalogCardProps> = ({
235248
return (
236249
<>
237250
<Card
251+
data-testid="catalog-card"
238252
elevation={0}
239253
key={id}
240254
sx={{

workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/__tests__/SandboxCatalogBanner.test.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ describe('SandboxCatalogBanner', () => {
5656
userReady: false,
5757
verificationRequired: false,
5858
pendingApproval: false,
59-
fetchError: null,
60-
signupError: null,
6159
ansibleError: null,
6260
ansibleData: undefined,
6361
ansibleUIUser: undefined,
@@ -83,8 +81,6 @@ describe('SandboxCatalogBanner', () => {
8381
userReady: boolean;
8482
verificationRequired: boolean;
8583
pendingApproval: boolean;
86-
fetchError: string | null;
87-
signupError: string | null;
8884
ansibleError: string | null;
8985
ansibleStatus: AnsibleStatus;
9086
}> = {},
@@ -96,8 +92,6 @@ describe('SandboxCatalogBanner', () => {
9692
userReady: false,
9793
verificationRequired: false,
9894
pendingApproval: false,
99-
fetchError: null,
100-
signupError: null,
10195
ansibleError: null,
10296
ansibleStatus: AnsibleStatus.NEW,
10397
refetchUserData: jest.fn(),
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
import { fireEvent, render, screen } from '@testing-library/react';
19+
import { createTheme, ThemeProvider } from '@mui/material/styles';
20+
import { SandboxCatalogCard } from '../SandboxCatalogCard';
21+
import { Product } from '../productData';
22+
import { AnsibleStatus } from '../../../utils/aap-utils';
23+
import { useSandboxContext } from '../../../hooks/useSandboxContext';
24+
import useGreenCorners from '../../../hooks/useGreenCorners';
25+
26+
jest.useFakeTimers(); // control timers
27+
// Mock the hooks
28+
jest.mock('../../../hooks/useGreenCorners');
29+
jest.mock('../../../hooks/useSandboxContext');
30+
31+
const mockCreateAAP = jest.fn();
32+
const configMock = {
33+
createAAP: mockCreateAAP,
34+
};
35+
36+
jest.mock('@backstage/core-plugin-api', () => ({
37+
...jest.requireActual('@backstage/core-plugin-api'),
38+
useApi: jest.fn(() => {
39+
return configMock;
40+
}),
41+
}));
42+
43+
describe('SandboxCatalogCard', () => {
44+
const theme = createTheme();
45+
46+
const mockSetGreenCorners = jest.fn();
47+
const mockGreenCorners = [{ id: 'openshift-console', show: false }];
48+
const mockRefetchUserData = jest.fn();
49+
const mockShowGreenCorner = jest.fn();
50+
const mockUseSandboxContext = useSandboxContext as jest.MockedFunction<
51+
typeof useSandboxContext
52+
>;
53+
const mockSignupUser = jest.fn();
54+
const mockRefetchAAP = jest.fn();
55+
56+
beforeEach(() => {
57+
jest.clearAllMocks();
58+
// refetching the user data will return the actual user provisioned
59+
mockRefetchUserData.mockReturnValue({
60+
name: 'bob',
61+
consoleURL: 'https://sandboxcluster.test/',
62+
cheDashboardURL: 'https://devspaces.test/',
63+
proxyURL: 'https://api-sandboxcluster.test',
64+
rhodsMemberURL: 'https://rhods-dashboard.test/',
65+
apiEndpoint: 'https://api.test:6443',
66+
clusterName: 'sandboxcluster.test',
67+
defaultUserNamespace: 'bob-2-dev',
68+
compliantUsername: 'bob-2',
69+
username: 'bob',
70+
status: {
71+
ready: true,
72+
reason: 'Provisioned',
73+
verificationRequired: false,
74+
},
75+
} as any);
76+
77+
mockUseSandboxContext.mockReturnValue({
78+
refetchUserData: mockRefetchUserData,
79+
loading: false,
80+
userFound: false,
81+
userReady: false,
82+
ansibleStatus: AnsibleStatus.UNKNOWN,
83+
verificationRequired: false,
84+
userData: undefined,
85+
signupUser: mockSignupUser,
86+
refetchAAP: mockRefetchAAP,
87+
ansibleData: undefined,
88+
ansibleUIUser: undefined,
89+
} as any);
90+
(useGreenCorners as jest.Mock).mockReturnValue({
91+
greenCorners: mockGreenCorners,
92+
setGreenCorners: mockSetGreenCorners,
93+
});
94+
});
95+
96+
const defaultProps = {
97+
id: Product.OPENSHIFT_CONSOLE,
98+
title: 'Openshift',
99+
image: 'sometestimage.svg',
100+
description: [
101+
{ icon: <div>icon 1</div>, value: 'Description 1' },
102+
{ icon: <div>icon 2</div>, value: 'Description 2' },
103+
],
104+
link: 'https://openshiftconsole.url.com',
105+
greenCorner: false,
106+
showGreenCorner: mockShowGreenCorner,
107+
};
108+
109+
const renderCard = (props = {}) => {
110+
return render(
111+
<ThemeProvider theme={theme}>
112+
<SandboxCatalogCard {...defaultProps} {...props} />
113+
</ThemeProvider>,
114+
);
115+
};
116+
117+
it('renders the card', () => {
118+
renderCard();
119+
120+
const cards = screen.getAllByTestId('catalog-card');
121+
expect(cards).toHaveLength(1);
122+
expect(screen.getByText('Openshift')).toBeInTheDocument(); // title
123+
expect(screen.getByText('Description 1')).toBeInTheDocument(); // description
124+
expect(screen.getByText('Description 2')).toBeInTheDocument(); // description
125+
expect(screen.getByText('icon 1')).toBeInTheDocument(); // icon
126+
const img = screen.getByAltText('Openshift') as HTMLImageElement;
127+
expect(img.src).toContain('sometestimage.svg');
128+
expect(screen.getByText('Try it')).toBeInTheDocument();
129+
});
130+
131+
it('opens the link when user signs up', async () => {
132+
const mockOpen = jest.fn();
133+
window.open = mockOpen; // override window.open with mock
134+
renderCard();
135+
136+
// Find and click try it button/icon
137+
const tryItButton = screen.getByRole('button', { name: /Try it/i });
138+
fireEvent.click(tryItButton);
139+
140+
// advance timers to trigger all retries
141+
for (let i = 0; i < 5; i++) {
142+
jest.advanceTimersByTime(1000);
143+
await Promise.resolve(); // allow awaiting the timer to flush
144+
}
145+
146+
expect(mockSignupUser).toHaveBeenCalled(); // check it signs up the user
147+
expect(mockRefetchUserData).toHaveBeenCalled();
148+
expect(mockOpen).toHaveBeenCalledWith(
149+
'https://sandboxcluster.test/',
150+
'_blank',
151+
); // check it opens the url after signup
152+
expect(mockShowGreenCorner).toHaveBeenCalled();
153+
});
154+
155+
it('starts provisioning AAP when user signs up', async () => {
156+
// AAP product card
157+
renderCard({ id: Product.AAP });
158+
159+
// Find and click provision button/icon
160+
const tryItButton = screen.getByRole('button', { name: /Provision/i });
161+
fireEvent.click(tryItButton);
162+
163+
// advance timers to trigger all retries
164+
for (let i = 0; i < 5; i++) {
165+
jest.advanceTimersByTime(1000);
166+
await Promise.resolve(); // allow awaiting the timer to flush
167+
}
168+
169+
expect(mockSignupUser).toHaveBeenCalled(); // check it signs up the user
170+
expect(mockRefetchUserData).toHaveBeenCalled();
171+
expect(mockRefetchAAP).toHaveBeenCalled(); // check it calls the aap specific functionality
172+
expect(mockCreateAAP).toHaveBeenCalledWith('bob-2-dev'); // check it creates the app instance in the user namespace
173+
expect(mockShowGreenCorner).toHaveBeenCalled();
174+
});
175+
});

workspaces/sandbox/plugins/sandbox/src/hooks/useProductURLs.ts

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import { useState, useEffect } from 'react';
16+
import { useEffect, useState } from 'react';
1717
import { useSandboxContext } from './useSandboxContext';
1818
import { Product } from '../components/SandboxCatalog/productData';
19+
import { SignupData } from '../types';
1920

2021
interface ProductURL {
2122
id: Product;
@@ -43,40 +44,41 @@ const getAppsURL = (
4344
return `https://${appRouteName}${appsURL}`;
4445
};
4546

47+
export const productsURLMapping = (userData: SignupData | undefined) => {
48+
const isProvisioned = userData?.status?.ready || false;
49+
return [
50+
{
51+
id: Product.OPENSHIFT_CONSOLE,
52+
url: isProvisioned ? userData?.consoleURL || '' : '',
53+
},
54+
{
55+
id: Product.OPENSHIFT_AI,
56+
url: isProvisioned ? userData?.rhodsMemberURL || '' : '',
57+
},
58+
{
59+
id: Product.DEVSPACES,
60+
url: isProvisioned
61+
? userData?.cheDashboardURL ||
62+
getAppsURL(AppURL.DEVSPACES, userData?.consoleURL)
63+
: '',
64+
},
65+
{ id: Product.AAP, url: '' },
66+
{
67+
id: Product.OPENSHIFT_VIRT,
68+
url: isProvisioned
69+
? `${userData?.consoleURL}/k8s/ns/${userData?.defaultUserNamespace}/virtualization-overview`
70+
: '',
71+
},
72+
];
73+
};
74+
4675
const useProductURLs = (): ProductURL[] => {
4776
const { userData } = useSandboxContext();
4877
const [productURLs, setProductURLs] = useState<ProductURL[]>([]);
49-
const defaultUserNamespace =
50-
userData?.defaultUserNamespace ?? `${userData?.username}-dev`;
5178

5279
useEffect(() => {
53-
const isProvisioned = userData?.status?.ready || false;
54-
55-
setProductURLs([
56-
{
57-
id: Product.OPENSHIFT_CONSOLE,
58-
url: isProvisioned ? userData?.consoleURL || '' : '',
59-
},
60-
{
61-
id: Product.OPENSHIFT_AI,
62-
url: isProvisioned ? userData?.rhodsMemberURL || '' : '',
63-
},
64-
{
65-
id: Product.DEVSPACES,
66-
url: isProvisioned
67-
? userData?.cheDashboardURL ||
68-
getAppsURL(AppURL.DEVSPACES, userData?.consoleURL)
69-
: '',
70-
},
71-
{ id: Product.AAP, url: '' },
72-
{
73-
id: Product.OPENSHIFT_VIRT,
74-
url: isProvisioned
75-
? `${userData?.consoleURL}/k8s/ns/${defaultUserNamespace}/virtualization-overview`
76-
: '',
77-
},
78-
]);
79-
}, [userData, defaultUserNamespace]);
80+
setProductURLs(productsURLMapping(userData));
81+
}, [userData]);
8082

8183
return productURLs;
8284
};

0 commit comments

Comments
 (0)