From c1f439666ce228b2f6a6a50bfc497dbcd1939351 Mon Sep 17 00:00:00 2001 From: Felipe Mota Date: Mon, 9 Dec 2024 22:02:05 -0300 Subject: [PATCH] fix(admin): Improve import projects from prod GitOrigin-RevId: 73c3fd0eea6899150f16b1d1a8d6ba3c1830ead4 --- .../admin/AdminImportProjectsFromProd.tsx | 163 ++++++++++++++++++ .../components/pages/admin/AdminPage.tsx | 6 +- .../pages/admin/ImportProjectsFromProd.tsx | 96 ----------- platform/wab/src/wab/server/AppServer.ts | 5 + platform/wab/src/wab/server/db/DbMgr.ts | 53 ++++-- platform/wab/src/wab/server/routes/admin.ts | 15 ++ .../wab/src/wab/server/routes/projects.ts | 76 ++++++-- platform/wab/src/wab/shared/SharedApi.ts | 9 + 8 files changed, 298 insertions(+), 125 deletions(-) create mode 100644 platform/wab/src/wab/client/components/pages/admin/AdminImportProjectsFromProd.tsx delete mode 100644 platform/wab/src/wab/client/components/pages/admin/ImportProjectsFromProd.tsx diff --git a/platform/wab/src/wab/client/components/pages/admin/AdminImportProjectsFromProd.tsx b/platform/wab/src/wab/client/components/pages/admin/AdminImportProjectsFromProd.tsx new file mode 100644 index 00000000000..3733f3d2cf1 --- /dev/null +++ b/platform/wab/src/wab/client/components/pages/admin/AdminImportProjectsFromProd.tsx @@ -0,0 +1,163 @@ +import { NonAuthCtx, useNonAuthCtx } from "@/wab/client/app-ctx"; +import { Modal } from "@/wab/client/components/widgets/Modal"; +import { WorkspaceId } from "@/wab/shared/ApiSchema"; +import { Bundle } from "@/wab/shared/bundles"; +import { DevFlagsType } from "@/wab/shared/devflags"; +import { Button } from "antd"; +import React, { useState } from "react"; + +type ProjectInfo = { + projectId: string; + bundle: string; + name: string; +}; + +function useImportFromProdListener( + onListen: (devflags: DevFlagsType, info: ProjectInfo[]) => Promise +) { + React.useEffect(() => { + const listener = async (event: MessageEvent) => { + try { + const data = JSON.parse(event.data); + if (data.source === "import-project-from-prod") { + const { devflags, projectsInfo } = data; + await onListen(devflags, projectsInfo); + } + } catch {} + }; + window.addEventListener("message", listener); + return () => window.removeEventListener("message", listener); + }, [onListen]); +} + +async function setupHostlessWorkspace( + nonAuthCtx: NonAuthCtx, + hostLessWorkspaceId: WorkspaceId +) { + try { + // If it doesn't throw an error, it means the workspace exists + await nonAuthCtx.api.getWorkspace(hostLessWorkspaceId); + } catch (e) { + async function getTeamId() { + const { teams } = await nonAuthCtx.api.listTeams(); + const nonPersonalTeam = teams.find((t) => !t.personalTeamOwnerId); + if (nonPersonalTeam) { + return nonPersonalTeam.id; + } + const { team } = await nonAuthCtx.api.createTeam("Hostless Team"); + return team.id; + } + + const teamId = await getTeamId(); + + await nonAuthCtx.api.adminCreateWorkspace({ + id: hostLessWorkspaceId, + name: "Hostless Workspace", + description: "Workspace for hostless projects", + teamId: teamId, + }); + } +} + +async function resetProjects( + nonAuthCtx: NonAuthCtx, + projectsInfo: ProjectInfo[] +) { + console.log("## Deleting existing projects..."); + + const projectIds = projectsInfo.flatMap((info) => { + const parsedBundles = JSON.parse(info.bundle) as Array<[string, Bundle]>; + const dependenciesProjectIds = parsedBundles.flatMap(([_, bundle]) => + Object.values(bundle.map) + .filter((inst) => inst.__type === "ProjectDependency") + .map((inst) => inst.projectId) + ); + + return [info.projectId, ...dependenciesProjectIds]; + }); + + await Promise.all( + projectIds.map((projectId) => + nonAuthCtx.api.deleteProjectAndRevisions(projectId) + ) + ); + + console.log("## Uploading new projects..."); + // We have to do it sync, since we can end up trying to insert the same project twice, concurrently and that will fail. + for (const bundle of projectsInfo) { + await nonAuthCtx.api.importProject(bundle.bundle, { + keepProjectIdsAndNames: true, + projectName: bundle.name, + }); + } +} + +export function AdminImportProjectsFromProd() { + const nonAuthCtx = useNonAuthCtx(); + const [modalVisible, setModalVisible] = useState(false); + const ref = React.createRef(); + + const onListenProjectsInfo = React.useCallback( + async (devflags: DevFlagsType, info: ProjectInfo[]) => { + if (devflags.hostLessWorkspaceId) { + await setupHostlessWorkspace(nonAuthCtx, devflags.hostLessWorkspaceId); + } + + devflags.hideBlankStarter = false; + + await nonAuthCtx.api.setDevFlagOverrides( + JSON.stringify(devflags, null, 2) + ); + + await resetProjects(nonAuthCtx, info); + + console.log("## Posting message to window..."); + ref.current!.contentWindow?.postMessage( + JSON.stringify({ + source: "import-project-from-prod", + done: true, + }) + ); + + setModalVisible(false); + }, + [nonAuthCtx, ref] + ); + + useImportFromProdListener(onListenProjectsInfo); + + const showImportFromProd = window.location.hostname.includes("localhost"); + // Don't even show this on prod, just for extra safety + if (!showImportFromProd) { + return null; + } + + return ( +
+

Import devflags and plasmic projects from prod

+ setModalVisible(false)} + width={800} + > +