From f20622cfe7676a3664107de9bb5cdbc1f609e6d1 Mon Sep 17 00:00:00 2001 From: Jan Potoms <2109932+Janpot@users.noreply.github.com> Date: Wed, 31 Aug 2022 15:45:32 +0200 Subject: [PATCH 1/3] Simplify deploy flow --- packages/toolpad-app/pages/api/rpc.ts | 4 ++ packages/toolpad-app/src/server/data.ts | 26 ++++--- .../src/toolpad/AppEditor/AppEditorShell.tsx | 72 ++++++++++--------- packages/toolpad-app/src/utils/prisma.ts | 13 ++++ 4 files changed, 68 insertions(+), 47 deletions(-) create mode 100644 packages/toolpad-app/src/utils/prisma.ts diff --git a/packages/toolpad-app/pages/api/rpc.ts b/packages/toolpad-app/pages/api/rpc.ts index 3a651d858f0..42f7f2a3791 100644 --- a/packages/toolpad-app/pages/api/rpc.ts +++ b/packages/toolpad-app/pages/api/rpc.ts @@ -18,6 +18,7 @@ import { findActiveDeployment, findLastRelease, deleteApp, + deploy, } from '../../src/server/data'; import { hasOwnProperty } from '../../src/utils/collections'; @@ -155,6 +156,9 @@ const rpcServer = { createDeployment: createMethod((params) => { return createDeployment(...params); }), + deploy: createMethod((params) => { + return deploy(...params); + }), saveDom: createMethod((params) => { return saveDom(...params); }), diff --git a/packages/toolpad-app/src/server/data.ts b/packages/toolpad-app/src/server/data.ts index c3b0f63eae5..afb139098ab 100644 --- a/packages/toolpad-app/src/server/data.ts +++ b/packages/toolpad-app/src/server/data.ts @@ -14,20 +14,7 @@ import { omit } from '../utils/immutability'; import { asArray } from '../utils/collections'; import { decryptSecret, encryptSecret } from './secrets'; import applyTransform from './applyTransform'; - -// See https://github.com/prisma/prisma/issues/5042#issuecomment-1104679760 -function excludeFields( - fields: T, - excluded: K, -): Record, boolean> { - const result = {} as Record, boolean>; - for (const key of Object.keys(fields)) { - if (!excluded.includes(key as any)) { - result[key as Exclude] = true; - } - } - return result; -} +import { excludeFields } from '../utils/prisma'; const SELECT_RELEASE_META = excludeFields(Prisma.ReleaseScalarFieldEnum, ['snapshot']); const SELECT_APP_META = excludeFields(Prisma.AppScalarFieldEnum, ['dom']); @@ -304,9 +291,20 @@ export async function createDeployment(appId: string, version: number) { connect: { release_app_constraint: { appId, version } }, }, }, + include: { + release: { + select: SELECT_RELEASE_META, + }, + }, }); } +export async function deploy(appId: string, releaseInput: CreateReleaseParams) { + const release = await createRelease(appId, releaseInput); + const deployment = await createDeployment(appId, release.version); + return deployment; +} + export async function findActiveDeployment(appId: string) { return prisma.deployment.findFirst({ where: { appId }, diff --git a/packages/toolpad-app/src/toolpad/AppEditor/AppEditorShell.tsx b/packages/toolpad-app/src/toolpad/AppEditor/AppEditorShell.tsx index 3053b5e4291..e97a5a082ae 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/AppEditorShell.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/AppEditorShell.tsx @@ -8,7 +8,6 @@ import { DialogActions, DialogContent, DialogTitle, - IconButton, Stack, TextField, Tooltip, @@ -17,14 +16,16 @@ import { import CloudDoneIcon from '@mui/icons-material/CloudDone'; import * as React from 'react'; import { useForm } from 'react-hook-form'; -import { Outlet, useNavigate } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import RocketLaunchIcon from '@mui/icons-material/RocketLaunch'; +import invariant from 'invariant'; import DialogForm from '../../components/DialogForm'; import { useDomLoader } from '../DomLoader'; import ToolpadAppShell from '../ToolpadAppShell'; import PagePanel from './PagePanel'; import client from '../../api'; +import useBoolean from '../../utils/useBoolean'; interface CreateReleaseDialogProps { appId: string; @@ -33,8 +34,6 @@ interface CreateReleaseDialogProps { } function CreateReleaseDialog({ appId, open, onClose }: CreateReleaseDialogProps) { - const navigate = useNavigate(); - const lastRelease = client.useQuery('findLastRelease', [appId]); const { handleSubmit, register, formState, reset } = useForm({ @@ -43,32 +42,32 @@ function CreateReleaseDialog({ appId, open, onClose }: CreateReleaseDialogProps) }, }); - const createReleaseMutation = client.useMutation('createRelease'); + React.useEffect(() => { + if (open) { + reset(); + } + }, [reset, open]); + + const deployMutation = client.useMutation('deploy'); const doSubmit = handleSubmit(async (releaseParams) => { - const newRelease = await createReleaseMutation.mutateAsync([appId, releaseParams]); - reset(); - navigate(`/app/${appId}/releases/${newRelease.version}`); + await deployMutation.mutateAsync([appId, releaseParams]); + const url = new URL(`/deploy/${appId}/pages`, window.location.href); + const deploymentWindow = window.open(url, '_blank'); + invariant(deploymentWindow, 'window failed to open'); + deploymentWindow.focus(); + onClose(); }); return ( - Create new release + Deploy application {lastRelease.isSuccess ? ( - You are about to create a snapshot of your application under a unique url. You will - be able to verify whether everything is working correctly before deploying this - release to production. - - - The new version to be created is " - {lastRelease.data ? lastRelease.data.version + 1 : 1}". - - - Please summarize the changes you have made to the application since the last - release: + You are about to deploy your application to production. Please summarize the changes + you have made to the application since the last release: ) : null} - {createReleaseMutation.isError ? ( - {(createReleaseMutation.error as Error).message} + {deployMutation.isError ? ( + {(deployMutation.error as Error).message} ) : null} @@ -93,10 +92,10 @@ function CreateReleaseDialog({ appId, open, onClose }: CreateReleaseDialogProps) - Create + Deploy @@ -122,7 +121,12 @@ export interface ToolpadAppShellProps { export default function AppEditorShell({ appId, ...props }: ToolpadAppShellProps) { const domLoader = useDomLoader(); - const [createReleaseDialogOpen, setCreateReleaseDialogOpen] = React.useState(false); + const { + value: createReleaseDialogOpen, + setTrue: handleCreateReleasDialogOpen, + setFalse: handleCreateReleasDialogClose, + } = useBoolean(false); + const [isSaveStateVisible, setIsSaveStateVisible] = React.useState(false); const hasUnsavedChanges = domLoader.unsavedChanges > 0; @@ -169,15 +173,9 @@ export default function AppEditorShell({ appId, ...props }: ToolpadAppShellProps ) : null} - setCreateReleaseDialogOpen(true)} - > - - + } {...props} @@ -218,7 +224,7 @@ export default function AppEditorShell({ appId, ...props }: ToolpadAppShellProps setCreateReleaseDialogOpen(false)} + onClose={handleCreateReleasDialogClose} /> diff --git a/packages/toolpad-app/src/utils/prisma.ts b/packages/toolpad-app/src/utils/prisma.ts new file mode 100644 index 00000000000..d967ca43605 --- /dev/null +++ b/packages/toolpad-app/src/utils/prisma.ts @@ -0,0 +1,13 @@ +// See https://github.com/prisma/prisma/issues/5042#issuecomment-1104679760 +export function excludeFields( + fields: T, + excluded: K, +): Record, boolean> { + const result = {} as Record, boolean>; + for (const key of Object.keys(fields)) { + if (!excluded.includes(key as any)) { + result[key as Exclude] = true; + } + } + return result; +} From 044ed3e6e3f664e2bb1f72c7b55a36f6f2bc82cd Mon Sep 17 00:00:00 2001 From: Jan Potoms <2109932+Janpot@users.noreply.github.com> Date: Wed, 31 Aug 2022 18:27:07 +0200 Subject: [PATCH 2/3] Deployments --- packages/toolpad-app/pages/api/rpc.ts | 4 + packages/toolpad-app/src/server/data.ts | 99 +++++++----- .../toolpad-app/src/toolpad/Deployments.tsx | 148 ++++++++++++++++++ packages/toolpad-app/src/toolpad/Toolpad.tsx | 2 + .../src/toolpad/ToolpadAppShell.tsx | 3 + 5 files changed, 214 insertions(+), 42 deletions(-) create mode 100644 packages/toolpad-app/src/toolpad/Deployments.tsx diff --git a/packages/toolpad-app/pages/api/rpc.ts b/packages/toolpad-app/pages/api/rpc.ts index 42f7f2a3791..a6622a1e5e4 100644 --- a/packages/toolpad-app/pages/api/rpc.ts +++ b/packages/toolpad-app/pages/api/rpc.ts @@ -19,6 +19,7 @@ import { findLastRelease, deleteApp, deploy, + getDeployments, } from '../../src/server/data'; import { hasOwnProperty } from '../../src/utils/collections'; @@ -118,6 +119,9 @@ const rpcServer = { getActiveDeployments: createMethod((params) => { return getActiveDeployments(...params); }), + getDeployments: createMethod((params) => { + return getDeployments(...params); + }), getApp: createMethod((params) => { return getApp(...params); }), diff --git a/packages/toolpad-app/src/server/data.ts b/packages/toolpad-app/src/server/data.ts index afb139098ab..2a144bd3334 100644 --- a/packages/toolpad-app/src/server/data.ts +++ b/packages/toolpad-app/src/server/data.ts @@ -1,12 +1,6 @@ import { NodeId, BindableAttrValue } from '@mui/toolpad-core'; import * as _ from 'lodash-es'; -import { - App, - DomNodeAttributeType, - PrismaClient, - Release, - Prisma, -} from '../../prisma/generated/client'; +import * as prisma from '../../prisma/generated/client'; import { ServerDataSource, ApiResult, VersionOrPreview } from '../types'; import serverDataSources from '../toolpadDataSources/server'; import * as appDom from '../appDom'; @@ -16,28 +10,28 @@ import { decryptSecret, encryptSecret } from './secrets'; import applyTransform from './applyTransform'; import { excludeFields } from '../utils/prisma'; -const SELECT_RELEASE_META = excludeFields(Prisma.ReleaseScalarFieldEnum, ['snapshot']); -const SELECT_APP_META = excludeFields(Prisma.AppScalarFieldEnum, ['dom']); +const SELECT_RELEASE_META = excludeFields(prisma.Prisma.ReleaseScalarFieldEnum, ['snapshot']); +const SELECT_APP_META = excludeFields(prisma.Prisma.AppScalarFieldEnum, ['dom']); -export type AppMeta = Omit; +export type AppMeta = Omit; -function getPrismaClient(): PrismaClient { +function getPrismaClient(): prisma.PrismaClient { if (process.env.NODE_ENV === 'production') { - return new PrismaClient(); + return new prisma.PrismaClient(); } // avoid Next.js dev server from creating too many prisma clients // See https://github.com/prisma/prisma/issues/1983 if (!(globalThis as any).prisma) { - (globalThis as any).prisma = new PrismaClient(); + (globalThis as any).prisma = new prisma.PrismaClient(); } return (globalThis as any).prisma; } -const prisma = getPrismaClient(); +const prismaClient = getPrismaClient(); -function deserializeValue(dbValue: string, type: DomNodeAttributeType): unknown { +function deserializeValue(dbValue: string, type: prisma.DomNodeAttributeType): unknown { const serialized = type === 'secret' ? decryptSecret(dbValue) : dbValue; return serialized.length <= 0 ? undefined : JSON.parse(serialized); } @@ -77,7 +71,7 @@ function decryptSecrets(dom: appDom.AppDom): appDom.AppDom { } export async function saveDom(appId: string, app: appDom.AppDom): Promise { - await prisma.app.update({ + await prismaClient.app.update({ where: { id: appId, }, @@ -87,7 +81,7 @@ export async function saveDom(appId: string, app: appDom.AppDom): Promise } async function loadPreviewDomLegacy(appId: string): Promise { - const dbNodes = await prisma.domNode.findMany({ + const dbNodes = await prismaClient.domNode.findMany({ where: { appId }, include: { attributes: true }, }); @@ -133,7 +127,7 @@ async function loadPreviewDomLegacy(appId: string): Promise { } async function loadPreviewDom(appId: string): Promise { - const { dom } = await prisma.app.findUniqueOrThrow({ + const { dom } = await prismaClient.app.findUniqueOrThrow({ where: { id: appId }, }); @@ -145,7 +139,7 @@ async function loadPreviewDom(appId: string): Promise { } export async function getApps(): Promise { - return prisma.app.findMany({ + return prismaClient.app.findMany({ orderBy: { editedAt: 'desc', }, @@ -154,14 +148,14 @@ export async function getApps(): Promise { } export async function getActiveDeployments() { - return prisma.deployment.findMany({ + return prismaClient.deployment.findMany({ distinct: ['appId'], orderBy: { createdAt: 'desc' }, }); } export async function getApp(id: string): Promise { - return prisma.app.findUnique({ where: { id }, select: SELECT_APP_META }); + return prismaClient.app.findUnique({ where: { id }, select: SELECT_APP_META }); } function createDefaultDom(): appDom.AppDom { @@ -185,9 +179,9 @@ export interface CreateAppOptions { dom?: appDom.AppDom | null; } -export async function createApp(name: string, opts: CreateAppOptions = {}): Promise { - return prisma.$transaction(async () => { - const app = await prisma.app.create({ +export async function createApp(name: string, opts: CreateAppOptions = {}): Promise { + return prismaClient.$transaction(async () => { + const app = await prismaClient.app.create({ data: { name }, }); @@ -200,7 +194,7 @@ export async function createApp(name: string, opts: CreateAppOptions = {}): Prom } export async function updateApp(appId: string, name: string): Promise { - await prisma.app.update({ + await prismaClient.app.update({ where: { id: appId, }, @@ -213,7 +207,7 @@ export async function updateApp(appId: string, name: string): Promise { } export async function deleteApp(id: string): Promise { - await prisma.app.delete({ + await prismaClient.app.delete({ where: { id }, select: { // Only return the id to reduce amount of data returned from the db @@ -222,19 +216,21 @@ export async function deleteApp(id: string): Promise { }); } -interface CreateReleaseParams { +export interface CreateReleaseParams { description: string; } +export type ReleaseMeta = Pick; + async function findLastReleaseInternal(appId: string) { - return prisma.release.findFirst({ + return prismaClient.release.findFirst({ where: { appId }, orderBy: { version: 'desc' }, }); } -export async function findLastRelease(appId: string) { - return prisma.release.findFirst({ +export async function findLastRelease(appId: string): Promise { + return prismaClient.release.findFirst({ where: { appId }, orderBy: { version: 'desc' }, select: SELECT_RELEASE_META, @@ -244,14 +240,14 @@ export async function findLastRelease(appId: string) { export async function createRelease( appId: string, { description }: CreateReleaseParams, -): Promise> { +): Promise { const currentDom = await loadPreviewDom(appId); const snapshot = Buffer.from(JSON.stringify(currentDom), 'utf-8'); const lastRelease = await findLastReleaseInternal(appId); const versionNumber = lastRelease ? lastRelease.version + 1 : 1; - const release = await prisma.release.create({ + const release = await prismaClient.release.create({ select: SELECT_RELEASE_META, data: { appId, @@ -264,8 +260,8 @@ export async function createRelease( return release; } -export async function getReleases(appId: string) { - return prisma.release.findMany({ +export async function getReleases(appId: string): Promise { + return prismaClient.release.findMany({ where: { appId }, select: SELECT_RELEASE_META, orderBy: { @@ -274,15 +270,31 @@ export async function getReleases(appId: string) { }); } -export async function getRelease(appId: string, version: number) { - return prisma.release.findUnique({ +export async function getRelease(appId: string, version: number): Promise { + return prismaClient.release.findUnique({ where: { release_app_constraint: { appId, version } }, select: SELECT_RELEASE_META, }); } -export async function createDeployment(appId: string, version: number) { - return prisma.deployment.create({ +export type Deployment = prisma.Deployment & { + release: ReleaseMeta; +}; + +export function getDeployments(appId: string): Promise { + return prismaClient.deployment.findMany({ + where: { appId }, + orderBy: { createdAt: 'desc' }, + include: { + release: { + select: SELECT_RELEASE_META, + }, + }, + }); +} + +export async function createDeployment(appId: string, version: number): Promise { + return prismaClient.deployment.create({ data: { app: { connect: { id: appId }, @@ -299,14 +311,17 @@ export async function createDeployment(appId: string, version: number) { }); } -export async function deploy(appId: string, releaseInput: CreateReleaseParams) { +export async function deploy( + appId: string, + releaseInput: CreateReleaseParams, +): Promise { const release = await createRelease(appId, releaseInput); const deployment = await createDeployment(appId, release.version); return deployment; } -export async function findActiveDeployment(appId: string) { - return prisma.deployment.findFirst({ +export async function findActiveDeployment(appId: string): Promise { + return prismaClient.deployment.findFirst({ where: { appId }, orderBy: { createdAt: 'desc' }, include: { @@ -318,7 +333,7 @@ export async function findActiveDeployment(appId: string) { } export async function loadReleaseDom(appId: string, version: number): Promise { - const release = await prisma.release.findUnique({ + const release = await prismaClient.release.findUnique({ where: { release_app_constraint: { appId, version } }, }); if (!release) { diff --git a/packages/toolpad-app/src/toolpad/Deployments.tsx b/packages/toolpad-app/src/toolpad/Deployments.tsx new file mode 100644 index 00000000000..efb626a78f7 --- /dev/null +++ b/packages/toolpad-app/src/toolpad/Deployments.tsx @@ -0,0 +1,148 @@ +import { + Container, + Typography, + Box, + Paper, + Skeleton, + Button, + styled, + Breadcrumbs, + Stack, +} from '@mui/material'; +import { DataGridPro, GridColumns } from '@mui/x-data-grid-pro'; +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import client from '../api'; +import { Maybe } from '../utils/types'; +import ToolpadAppShell from './ToolpadAppShell'; +import DefinitionList from '../components/DefinitionList'; +import getReadableDuration from '../utils/readableDuration'; +import { Deployment } from '../server/data'; + +const DeploymentActions = styled('div')(({ theme }) => ({ + marginTop: theme.spacing(1), +})); + +type ActiveDeploymentProps = { + value?: Maybe; +}; + +function ActiveDeployment({ value }: ActiveDeploymentProps) { + const url = value ? String(new URL(`/deploy/${value.appId}`, window.location.href)) : null; + + return ( + + + {value ? ( + Version "{value.version}" + ) : ( + + )} + + Currently active deployment + +
Deployed:
+
{value ? value.createdAt.toLocaleString('short') : }
+
+ + + + +
+ ); +} + +function NoActiveDeployment() { + return ( + + App not deployed yet + + ); +} + +export default function Deployments() { + const { appId } = useParams(); + + if (!appId) { + throw new Error(`Missing queryParam "appId"`); + } + + const { + data: rawDeployments = [], + isLoading, + error, + } = client.useQuery('getDeployments', [appId]); + + const deployments = React.useMemo( + () => + rawDeployments.map((deployment) => ({ + ...deployment, + createdAtRelative: getReadableDuration(deployment.createdAt), + })), + [rawDeployments], + ); + + const columns = React.useMemo>( + () => [ + { + field: 'version', + valueGetter: ({ row }) => row.release.version, + headerName: '', + width: 50, + }, + { + field: 'createdAtRelative', + headerName: 'Deployed', + width: 150, + }, + { + field: 'description', + valueGetter: ({ row }) => row.release.description, + headerName: 'Description', + flex: 1, + }, + ], + [], + ); + + const activeDeploymentQuery = client.useQuery('findActiveDeployment', [appId]); + + return ( + + + + + Deployments + + + + {activeDeploymentQuery.isLoading || activeDeploymentQuery.data ? ( + + ) : ( + + )} + + + + + + + + + ); +} diff --git a/packages/toolpad-app/src/toolpad/Toolpad.tsx b/packages/toolpad-app/src/toolpad/Toolpad.tsx index f0bf5bb9222..93efb1b4895 100644 --- a/packages/toolpad-app/src/toolpad/Toolpad.tsx +++ b/packages/toolpad-app/src/toolpad/Toolpad.tsx @@ -7,6 +7,7 @@ import Releases from './Releases'; import AppEditor from './AppEditor'; import Home from './Home'; import ErrorAlert from './AppEditor/PageEditor/ErrorAlert'; +import Deployments from './Deployments'; const Centered = styled('div')({ height: '100%', @@ -41,6 +42,7 @@ function AppWorkspace() { } /> } /> + } /> } /> diff --git a/packages/toolpad-app/src/toolpad/ToolpadAppShell.tsx b/packages/toolpad-app/src/toolpad/ToolpadAppShell.tsx index 70725cc45a1..d469de05bca 100644 --- a/packages/toolpad-app/src/toolpad/ToolpadAppShell.tsx +++ b/packages/toolpad-app/src/toolpad/ToolpadAppShell.tsx @@ -20,6 +20,9 @@ export default function ToolpadAppShell({ appId, ...props }: ToolpadAppShellProp + } {...props} From 78cea6e7bdd262abf73692d6d394e4468c44fd37 Mon Sep 17 00:00:00 2001 From: Jan Potoms <2109932+Janpot@users.noreply.github.com> Date: Thu, 1 Sep 2022 09:08:12 +0200 Subject: [PATCH 3/3] remove releases --- .../src/toolpad/AppEditor/AppEditorShell.tsx | 11 +- .../toolpad-app/src/toolpad/Deployments.tsx | 148 --------------- packages/toolpad-app/src/toolpad/Release.tsx | 164 ----------------- packages/toolpad-app/src/toolpad/Releases.tsx | 174 ------------------ packages/toolpad-app/src/toolpad/Toolpad.tsx | 6 - .../src/toolpad/ToolpadAppShell.tsx | 31 ---- 6 files changed, 5 insertions(+), 529 deletions(-) delete mode 100644 packages/toolpad-app/src/toolpad/Deployments.tsx delete mode 100644 packages/toolpad-app/src/toolpad/Release.tsx delete mode 100644 packages/toolpad-app/src/toolpad/Releases.tsx delete mode 100644 packages/toolpad-app/src/toolpad/ToolpadAppShell.tsx diff --git a/packages/toolpad-app/src/toolpad/AppEditor/AppEditorShell.tsx b/packages/toolpad-app/src/toolpad/AppEditor/AppEditorShell.tsx index e97a5a082ae..50faae41020 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/AppEditorShell.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/AppEditorShell.tsx @@ -22,7 +22,7 @@ import RocketLaunchIcon from '@mui/icons-material/RocketLaunch'; import invariant from 'invariant'; import DialogForm from '../../components/DialogForm'; import { useDomLoader } from '../DomLoader'; -import ToolpadAppShell from '../ToolpadAppShell'; +import ToolpadShell from '../ToolpadShell'; import PagePanel from './PagePanel'; import client from '../../api'; import useBoolean from '../../utils/useBoolean'; @@ -113,12 +113,12 @@ function getSaveStateMessage(isSaving: boolean, hasUnsavedChanges: boolean): str return 'All changes saved!'; } -export interface ToolpadAppShellProps { +export interface ToolpadShellProps { appId: string; actions?: React.ReactNode; } -export default function AppEditorShell({ appId, ...props }: ToolpadAppShellProps) { +export default function AppEditorShell({ appId, ...props }: ToolpadShellProps) { const domLoader = useDomLoader(); const { @@ -155,8 +155,7 @@ export default function AppEditorShell({ appId, ...props }: ToolpadAppShellProps }, [hasUnsavedChanges]); return ( - {isSaveStateVisible ? ( @@ -227,6 +226,6 @@ export default function AppEditorShell({ appId, ...props }: ToolpadAppShellProps onClose={handleCreateReleasDialogClose} /> - + ); } diff --git a/packages/toolpad-app/src/toolpad/Deployments.tsx b/packages/toolpad-app/src/toolpad/Deployments.tsx deleted file mode 100644 index efb626a78f7..00000000000 --- a/packages/toolpad-app/src/toolpad/Deployments.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { - Container, - Typography, - Box, - Paper, - Skeleton, - Button, - styled, - Breadcrumbs, - Stack, -} from '@mui/material'; -import { DataGridPro, GridColumns } from '@mui/x-data-grid-pro'; -import * as React from 'react'; -import { useParams } from 'react-router-dom'; -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import client from '../api'; -import { Maybe } from '../utils/types'; -import ToolpadAppShell from './ToolpadAppShell'; -import DefinitionList from '../components/DefinitionList'; -import getReadableDuration from '../utils/readableDuration'; -import { Deployment } from '../server/data'; - -const DeploymentActions = styled('div')(({ theme }) => ({ - marginTop: theme.spacing(1), -})); - -type ActiveDeploymentProps = { - value?: Maybe; -}; - -function ActiveDeployment({ value }: ActiveDeploymentProps) { - const url = value ? String(new URL(`/deploy/${value.appId}`, window.location.href)) : null; - - return ( - - - {value ? ( - Version "{value.version}" - ) : ( - - )} - - Currently active deployment - -
Deployed:
-
{value ? value.createdAt.toLocaleString('short') : }
-
- - - - -
- ); -} - -function NoActiveDeployment() { - return ( - - App not deployed yet - - ); -} - -export default function Deployments() { - const { appId } = useParams(); - - if (!appId) { - throw new Error(`Missing queryParam "appId"`); - } - - const { - data: rawDeployments = [], - isLoading, - error, - } = client.useQuery('getDeployments', [appId]); - - const deployments = React.useMemo( - () => - rawDeployments.map((deployment) => ({ - ...deployment, - createdAtRelative: getReadableDuration(deployment.createdAt), - })), - [rawDeployments], - ); - - const columns = React.useMemo>( - () => [ - { - field: 'version', - valueGetter: ({ row }) => row.release.version, - headerName: '', - width: 50, - }, - { - field: 'createdAtRelative', - headerName: 'Deployed', - width: 150, - }, - { - field: 'description', - valueGetter: ({ row }) => row.release.description, - headerName: 'Description', - flex: 1, - }, - ], - [], - ); - - const activeDeploymentQuery = client.useQuery('findActiveDeployment', [appId]); - - return ( - - - - - Deployments - - - - {activeDeploymentQuery.isLoading || activeDeploymentQuery.data ? ( - - ) : ( - - )} - - - - - - - - - ); -} diff --git a/packages/toolpad-app/src/toolpad/Release.tsx b/packages/toolpad-app/src/toolpad/Release.tsx deleted file mode 100644 index 4ad7f8a65cf..00000000000 --- a/packages/toolpad-app/src/toolpad/Release.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { - Button, - Container, - Toolbar, - Typography, - Paper, - Breadcrumbs, - Link as MuiLink, - Stack, -} from '@mui/material'; -import * as React from 'react'; -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import RocketLaunchIcon from '@mui/icons-material/RocketLaunch'; -import { Link, useParams } from 'react-router-dom'; -import client from '../api'; -import ToolpadAppShell from './ToolpadAppShell'; -import DefinitionList from '../components/DefinitionList'; -import useLatest from '../utils/useLatest'; -import useDialog from '../utils/useDialog'; -import { ConfirmDialog } from '../components/SystemDialogs'; - -function getDeploymentStatusMessage(version: number, activeVersion?: number): React.ReactNode { - if (typeof activeVersion === 'undefined') { - return App is not deployed; - } - - return version === activeVersion ? ( - This is the deployed release - ) : ( - - - Version "{activeVersion}" - {' '} - is currently deployed - - ); -} - -interface ConfirmDeployDialogProps { - data?: { - version: number; - }; - open: boolean; - onClose: (confirm: boolean) => void; -} - -function ConfirmDeployDialog({ open, onClose, data: dataProp }: ConfirmDeployDialogProps) { - const data = useLatest(dataProp); - - return data ? ( - - Press "Deploy" to change the canonical URL of your application to - version "{data.version}". - - ) : null; -} - -interface ActiveReleaseMessageProps { - appId: string; - version: number; - activeVersion?: number; -} - -function DeploymentStatus({ appId, activeVersion, version }: ActiveReleaseMessageProps) { - const msg: React.ReactNode = getDeploymentStatusMessage(version, activeVersion); - const isActiveDeployment = activeVersion === version; - - const deployReleaseMutation = client.useMutation('createDeployment'); - - const { element, show: showConfirmDialog } = useDialog(ConfirmDeployDialog); - - const handleDeployClick = React.useCallback(async () => { - const ok = await showConfirmDialog({ version }); - - if (!ok) { - return; - } - - if (version) { - await deployReleaseMutation.mutateAsync([appId, version]); - client.invalidateQueries('findActiveDeployment', [appId]); - } - }, [appId, deployReleaseMutation, showConfirmDialog, version]); - - const canDeploy = deployReleaseMutation.isIdle && !isActiveDeployment; - const isNewerVersion = version >= (activeVersion ?? -Infinity); - - return ( - - {msg} - - - - - {element} - - ); -} - -export default function Release() { - const { version: rawVersion, appId } = useParams(); - const version = Number(rawVersion); - - if (!appId) { - throw new Error(`Missing queryParam "appId"`); - } - - const releaseQuery = client.useQuery('getRelease', [appId, version]); - - const activeDeploymentQuery = client.useQuery('findActiveDeployment', [appId]); - - const permaLink = String(new URL(`/app/${appId}/${version}`, window.location.href)); - - return ( - - - - - - Releases - - Version "{version}" - - - Version "{version}" - - -
Created:
-
{releaseQuery?.data?.createdAt.toLocaleString('short')}
-
Description:
-
{releaseQuery?.data?.description}
-
Permalink:
-
- {permaLink} -
-
- -
-
-
-
- ); -} diff --git a/packages/toolpad-app/src/toolpad/Releases.tsx b/packages/toolpad-app/src/toolpad/Releases.tsx deleted file mode 100644 index eccb8680f57..00000000000 --- a/packages/toolpad-app/src/toolpad/Releases.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { - Container, - Typography, - Box, - Paper, - Skeleton, - Button, - styled, - Breadcrumbs, - Stack, -} from '@mui/material'; -import { DataGridPro, GridColumns } from '@mui/x-data-grid-pro'; -import * as React from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import RocketLaunchIcon from '@mui/icons-material/RocketLaunch'; -import type { Deployment } from '../../prisma/generated/client'; -import client from '../api'; -import { Maybe } from '../utils/types'; -import ToolpadAppShell from './ToolpadAppShell'; -import DefinitionList from '../components/DefinitionList'; - -interface ReleaseRow { - createdAt: Date; - version: number; - description: string; -} - -const DeploymentActions = styled('div')(({ theme }) => ({ - marginTop: theme.spacing(1), -})); - -type ActiveDeploymentProps = { - value?: Maybe; -}; - -function ActiveDeployment({ value }: ActiveDeploymentProps) { - const url = value ? String(new URL(`/deploy/${value.appId}`, window.location.href)) : null; - - return ( - - - {value ? ( - Version "{value.version}" - ) : ( - - )} - - Currently active deployment - -
Deployed:
-
{value ? value.createdAt.toLocaleString('short') : }
-
- - - - -
- ); -} - -interface NoActiveDeploymentProps { - appId: string; - releases?: ReleaseRow[]; -} - -function NoActiveDeployment({ appId, releases = [] }: NoActiveDeploymentProps) { - const latestRelease = releases.length > 0 ? releases[0] : null; - - const deployReleaseMutation = client.useMutation('createDeployment'); - - const handleDeployClick = React.useCallback(async () => { - if (latestRelease) { - await deployReleaseMutation.mutateAsync([appId, latestRelease.version]); - client.invalidateQueries('findActiveDeployment', [appId]); - } - }, [appId, deployReleaseMutation, latestRelease]); - - return ( - - App not deployed yet - - - {latestRelease ? ( - - ) : ( - There are no releases to deploy - )} - - - ); -} - -export default function Releases() { - const { appId } = useParams(); - - if (!appId) { - throw new Error(`Missing queryParam "appId"`); - } - - const navigate = useNavigate(); - const { data: releases = [], isLoading, error } = client.useQuery('getReleases', [appId]); - - const activeDeploymentQuery = client.useQuery('findActiveDeployment', [appId]); - const activeVersion = activeDeploymentQuery.data?.release.version; - - const columns = React.useMemo>( - () => [ - { - field: 'version', - headerName: 'Version', - }, - { - field: 'description', - headerName: 'Description', - flex: 1, - }, - { - field: 'active', - headerName: 'Deployed', - type: 'boolean', - valueGetter: (params) => params.row.version === activeVersion, - }, - { - field: 'createdAt', - headerName: 'Created', - type: 'date', - }, - ], - [activeVersion], - ); - - return ( - - - - - Releases - - - - {activeDeploymentQuery.isLoading || activeDeploymentQuery.data ? ( - - ) : ( - - )} - - - - row.version} - loading={isLoading} - error={(error as any)?.message} - onRowClick={({ row }) => navigate(`/app/${appId}/releases/${row.version}`)} - /> - - - - - ); -} diff --git a/packages/toolpad-app/src/toolpad/Toolpad.tsx b/packages/toolpad-app/src/toolpad/Toolpad.tsx index 93efb1b4895..b463aaf0d76 100644 --- a/packages/toolpad-app/src/toolpad/Toolpad.tsx +++ b/packages/toolpad-app/src/toolpad/Toolpad.tsx @@ -2,12 +2,9 @@ import * as React from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { Box, CircularProgress, NoSsr, styled } from '@mui/material'; import { ErrorBoundary } from 'react-error-boundary'; -import Release from './Release'; -import Releases from './Releases'; import AppEditor from './AppEditor'; import Home from './Home'; import ErrorAlert from './AppEditor/PageEditor/ErrorAlert'; -import Deployments from './Deployments'; const Centered = styled('div')({ height: '100%', @@ -41,9 +38,6 @@ function AppWorkspace() { } /> - } /> - } /> - } /> ); diff --git a/packages/toolpad-app/src/toolpad/ToolpadAppShell.tsx b/packages/toolpad-app/src/toolpad/ToolpadAppShell.tsx deleted file mode 100644 index d469de05bca..00000000000 --- a/packages/toolpad-app/src/toolpad/ToolpadAppShell.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import { Button } from '@mui/material'; -import { Link } from 'react-router-dom'; -import ToolpadShell from './ToolpadShell'; - -export interface ToolpadAppShellProps { - appId: string; - actions?: React.ReactNode; - children?: React.ReactNode; -} - -export default function ToolpadAppShell({ appId, ...props }: ToolpadAppShellProps) { - return ( - - - - - - } - {...props} - /> - ); -}