From cca596b4a9e09a6527c1184b7da3fea21bad1df2 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 19 Aug 2024 21:21:39 +0200 Subject: [PATCH] fix: one click demo --- .../OneClickDemo/OneClickDemoController.ts | 32 +++++++++ .../OneClickDemo/CreateOneClickDemo.ts | 30 +++++++-- .../OneClickDemo/OneClickDemoApplication.ts | 23 +++++-- ...40819164614_create_oneclick_demos_table.js | 21 ++++++ .../server/src/system/models/OneclickDemo.ts | 17 +++++ packages/webapp/src/components/App.tsx | 4 +- .../OneClickDemo/OneClickDemoPage.module.scss | 32 +++++++++ .../OneClickDemo/OneClickDemoPage.tsx | 66 ++++++++++++++++--- .../webapp/src/hooks/query/authentication.tsx | 2 +- .../webapp/src/hooks/query/oneclick-demo.ts | 66 ++++++++++++++++++- 10 files changed, 270 insertions(+), 23 deletions(-) create mode 100644 packages/server/src/system/migrations/20240819164614_create_oneclick_demos_table.js create mode 100644 packages/server/src/system/models/OneclickDemo.ts create mode 100644 packages/webapp/src/containers/OneClickDemo/OneClickDemoPage.module.scss diff --git a/packages/server/src/api/controllers/OneClickDemo/OneClickDemoController.ts b/packages/server/src/api/controllers/OneClickDemo/OneClickDemoController.ts index b2812fbec..dd5e41145 100644 --- a/packages/server/src/api/controllers/OneClickDemo/OneClickDemoController.ts +++ b/packages/server/src/api/controllers/OneClickDemo/OneClickDemoController.ts @@ -3,6 +3,8 @@ import { Service, Inject } from 'typedi'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import BaseController from '@/api/controllers/BaseController'; import { OneClickDemoApplication } from '@/services/OneClickDemo/OneClickDemoApplication'; +import { reset } from 'colorette'; +import { body } from 'express-validator'; @Service() export class OneClickDemoController extends BaseController { @@ -16,6 +18,14 @@ export class OneClickDemoController extends BaseController { const router = Router(); router.post('/one_click', asyncMiddleware(this.oneClickDemo.bind(this))); + router.post( + '/one_click_signin', + [ + body('demo_id').exists(), + ], + this.validationResult, + asyncMiddleware(this.oneClickSignIn.bind(this)) + ); return router; } @@ -38,4 +48,26 @@ export class OneClickDemoController extends BaseController { next(error); } } + + /** + * + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + private async oneClickSignIn( + req: Request, + res: Response, + next: NextFunction + ) { + const { demoId } = this.matchedBodyData(req); + + try { + const data = await this.oneClickDemoApp.autoSignIn(demoId); + + return res.status(200).send(data); + } catch (error) { + next(error); + } + } } diff --git a/packages/server/src/services/OneClickDemo/CreateOneClickDemo.ts b/packages/server/src/services/OneClickDemo/CreateOneClickDemo.ts index a29ed2560..62396cb6e 100644 --- a/packages/server/src/services/OneClickDemo/CreateOneClickDemo.ts +++ b/packages/server/src/services/OneClickDemo/CreateOneClickDemo.ts @@ -1,8 +1,11 @@ import { Inject, Service } from 'typedi'; import { faker } from '@faker-js/faker'; +import uniqid from 'uniqid'; import AuthenticationApplication from '../Authentication/AuthApplication'; import OrganizationService from '../Organization/OrganizationService'; import { IAuthSignInPOJO } from '@/interfaces'; +import { OneClickDemo } from '@/system/models/OneclickDemo'; +import { SystemUser } from '@/system/models'; @Service() export class CreateOneClickDemo { @@ -16,17 +19,21 @@ export class CreateOneClickDemo { * Creates one-click demo account. * @returns {Promise} */ - async createOneClickDemo(): Promise { + public async createOneClickDemo(): Promise { const firstName = faker.person.firstName(); const lastName = faker.person.lastName(); const email = faker.internet.email(); const password = '123123123'; + const demoId = uniqid(); await this.authApp.signUp({ firstName, lastName, email, password }); - // const signedIn = await this.authApp.signIn(email, password); const tenantId = signedIn.tenant.id; + const userId = signedIn.user.id; + + // Creates a new one-click demo. + await OneClickDemo.query().insert({ key: demoId, tenantId, userId }); const buildJob = await this.organizationService.buildRunJob( tenantId, @@ -40,18 +47,33 @@ export class CreateOneClickDemo { }, signedIn.user ); - return { email, signedIn, buildJob }; + return { email, demoId, signedIn, buildJob }; } /** * Sign-in automicatlly using the demo id one creating an account finish. * @param {string} oneClickDemoId - */ - async autoSignIn(oneClickDemoId: string) {} + async autoSignIn(oneClickDemoId: string) { + const foundOneclickDemo = await OneClickDemo.query() + .findOne('key', oneClickDemoId) + .throwIfNotFound(); + + const userId = foundOneclickDemo.userId; + const user = await SystemUser.query().findById(userId); + + const email = user.email; + const password = '123123123'; + + const signedIn = await this.authApp.signIn(email, password); + + return signedIn; + } } interface ICreateOneClickDemoPOJO { email: string; + demoId: string; signedIn: IAuthSignInPOJO; buildJob: any; } diff --git a/packages/server/src/services/OneClickDemo/OneClickDemoApplication.ts b/packages/server/src/services/OneClickDemo/OneClickDemoApplication.ts index 9465827f3..f14704f85 100644 --- a/packages/server/src/services/OneClickDemo/OneClickDemoApplication.ts +++ b/packages/server/src/services/OneClickDemo/OneClickDemoApplication.ts @@ -1,14 +1,25 @@ -import { Inject, Service } from "typedi"; -import { CreateOneClickDemo } from "./CreateOneClickDemo"; - +import { Inject, Service } from 'typedi'; +import { CreateOneClickDemo } from './CreateOneClickDemo'; @Service() export class OneClickDemoApplication { - @Inject() private createOneClickDemoService: CreateOneClickDemo; - + + /** + * + * @returns + */ public createOneClick() { return this.createOneClickDemoService.createOneClickDemo(); } -} \ No newline at end of file + + /** + * + * @param oneClickDemoId + * @returns + */ + public autoSignIn(oneClickDemoId: string) { + return this.createOneClickDemoService.autoSignIn(oneClickDemoId); + } +} diff --git a/packages/server/src/system/migrations/20240819164614_create_oneclick_demos_table.js b/packages/server/src/system/migrations/20240819164614_create_oneclick_demos_table.js new file mode 100644 index 000000000..0586350c2 --- /dev/null +++ b/packages/server/src/system/migrations/20240819164614_create_oneclick_demos_table.js @@ -0,0 +1,21 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema.createTable('oneclick_demos', (table) => { + table.increments('id'); + table.string('key'); + table.integer('tenant_id').unsigned(); + table.integer('user_id').unsigned(); + table.timestamps(); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.dropTableIfExists('oneclick_demos'); +}; diff --git a/packages/server/src/system/models/OneclickDemo.ts b/packages/server/src/system/models/OneclickDemo.ts new file mode 100644 index 000000000..9900a0285 --- /dev/null +++ b/packages/server/src/system/models/OneclickDemo.ts @@ -0,0 +1,17 @@ +import SystemModel from '@/system/models/SystemModel'; + +export class OneClickDemo extends SystemModel { + /** + * Table name + */ + static get tableName() { + return 'oneclick_demos'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return ['createdAt']; + } +} diff --git a/packages/webapp/src/components/App.tsx b/packages/webapp/src/components/App.tsx index ba148ba5b..338fefc02 100644 --- a/packages/webapp/src/components/App.tsx +++ b/packages/webapp/src/components/App.tsx @@ -40,7 +40,9 @@ function AppInsider({ history }) { - + + + diff --git a/packages/webapp/src/containers/OneClickDemo/OneClickDemoPage.module.scss b/packages/webapp/src/containers/OneClickDemo/OneClickDemoPage.module.scss new file mode 100644 index 000000000..850ca2253 --- /dev/null +++ b/packages/webapp/src/containers/OneClickDemo/OneClickDemoPage.module.scss @@ -0,0 +1,32 @@ + + +.root { + text-align: center; + display: flex; + height: 100vh; + width: 100%; +} + +.inner{ + margin: auto; + max-width: 450px; +} + +.progressBar{ + height: 5px; + + :global .bp4-progress-meter{ + background-color: rgba(159, 171, 188, 0.8) + } +} + +.oneClickBtn { + width: 400px; + margin-top: 3rem; +} + +.waitingText{ + font-size: 15px; + line-height: 1.54; + color: #5F6B7C; +} \ No newline at end of file diff --git a/packages/webapp/src/containers/OneClickDemo/OneClickDemoPage.tsx b/packages/webapp/src/containers/OneClickDemo/OneClickDemoPage.tsx index 620452f7d..fd996c99e 100644 --- a/packages/webapp/src/containers/OneClickDemo/OneClickDemoPage.tsx +++ b/packages/webapp/src/containers/OneClickDemo/OneClickDemoPage.tsx @@ -1,39 +1,85 @@ // @ts-nocheck +import { Button, Intent, ProgressBar, Spinner, Text } from '@blueprintjs/core'; import { useEffect, useState } from 'react'; -import { Box } from '@/components'; -import { useCreateOneClickDemo } from '@/hooks/query/oneclick-demo'; -import { Button } from '@blueprintjs/core'; +import { + useCreateOneClickDemo, + useOneClickDemoSignin, +} from '@/hooks/query/oneclick-demo'; +import { Box, Icon, Stack } from '@/components'; import { useJob } from '@/hooks/query'; +import style from './OneClickDemoPage.module.scss'; export default function OneClickDemoPage() { - const { mutateAsync: createOneClickDemo } = useCreateOneClickDemo(); + const { + mutateAsync: createOneClickDemo, + isLoading: isCreateOneClickLoading, + } = useCreateOneClickDemo(); + const { mutateAsync: oneClickDemoSignIn } = useOneClickDemoSignin(); const [buildJobId, setBuildJobId] = useState(''); + const [demoId, setDemoId] = useState(''); + + // Job done state. + const [isJobDone, setIsJobDone] = useState(false); const { data: { running, queued, failed, completed }, isFetching: isJobFetching, } = useJob(buildJobId, { refetchInterval: 2000, - enabled: !!buildJobId, + enabled: !isJobDone && !!buildJobId, + onSuccess: (res) => { + if (res.completed) { + oneClickDemoSignIn({ demoId }).then((res) => { + debugger; + }); + } + }, }); useEffect(() => { if (completed) { + setIsJobDone(true); } - }, [completed]); + }, [completed, setIsJobDone]); const handleCreateAccountBtnClick = () => { createOneClickDemo({}) .then((res) => { - setBuildJobId(res?.data?.data?.build_job?.job_id) + setBuildJobId(res?.data?.data?.build_job?.job_id); + setDemoId(res?.data?.data?.demo_id); }) .catch(() => {}); }; + const isLoading = running; return ( - - {running && (

Building...

)} - + + + + + + {isLoading && ( + + + + We're preparing temporary environment for trial, It typically + take few seconds. Do not close or refresh the page. + + + )} + + + {!isLoading && ( + + )} + ); } diff --git a/packages/webapp/src/hooks/query/authentication.tsx b/packages/webapp/src/hooks/query/authentication.tsx index 96ee23426..4b54bbac9 100644 --- a/packages/webapp/src/hooks/query/authentication.tsx +++ b/packages/webapp/src/hooks/query/authentication.tsx @@ -16,7 +16,7 @@ import { /** * Saves the response data to cookies. */ -function setAuthLoginCookies(data) { +export function setAuthLoginCookies(data) { setCookie('token', data.token); setCookie('authenticated_user_id', data.user.id); setCookie('organization_id', data.tenant.organization_id); diff --git a/packages/webapp/src/hooks/query/oneclick-demo.ts b/packages/webapp/src/hooks/query/oneclick-demo.ts index 537729214..27a2544f0 100644 --- a/packages/webapp/src/hooks/query/oneclick-demo.ts +++ b/packages/webapp/src/hooks/query/oneclick-demo.ts @@ -6,9 +6,22 @@ import { useQueryClient, } from 'react-query'; import useApiRequest from '../useRequest'; +import { + useSetAuthToken, + useSetAuthUserId, + useSetLocale, + useSetOrganizationId, + useSetTenantId, +} from '../state'; +import { setAuthLoginCookies } from './authentication'; +import { batch } from 'react-redux'; interface CreateOneClickDemoValues {} -interface CreateOneClickDemoRes {} +interface CreateOneClickDemoRes { + email: string; + signedIn: any; + buildJob: any; +} /** * @@ -33,3 +46,54 @@ export function useCreateOneClickDemo( }, ); } + +interface OneClickSigninDemoValues { + demoId: string; +} +interface OneClickSigninDemoRes {} + +/** + * + * @param {UseMutationOptions} props + * @returns {UseMutationResult} + */ +export function useOneClickDemoSignin( + props?: UseMutationOptions< + OneClickSigninDemoRes, + Error, + OneClickSigninDemoValues + >, +): UseMutationResult { + const queryClient = useQueryClient(); + const apiRequest = useApiRequest(); + + const setAuthToken = useSetAuthToken(); + const setOrganizationId = useSetOrganizationId(); + const setUserId = useSetAuthUserId(); + const setTenantId = useSetTenantId(); + const setLocale = useSetLocale(); + + return useMutation( + ({ demoId }) => + apiRequest.post(`/demo/one_click_signin`, { demo_id: demoId }), + { + onSuccess: (res, id) => { + // Set authentication cookies. + setAuthLoginCookies(res.data); + + batch(() => { + // Sets the auth metadata to global state. + setAuthToken(res.data.token); + setOrganizationId(res.data.tenant.organization_id); + setUserId(res.data.user.id); + setTenantId(res.data.tenant.id); + + if (res.data?.tenant?.metadata?.language) { + setLocale(res.data?.tenant?.metadata?.language); + } + }); + }, + ...props, + }, + ); +}