Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cherrypick some changes from direction 13 branch #1599

Merged
merged 3 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions packages/toolpad-app/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,13 @@ export default class MyDocument extends Document<ToolpadDocumentProps> {
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${config.gaId}', {
page_path: window.location.pathname,
});
`,
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${config.gaId}', {
page_path: window.location.pathname,
});
`,
}}
/>
</Head>
Expand Down
15 changes: 13 additions & 2 deletions packages/toolpad-app/pages/app-canvas/[[...path]].tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import type { NextPage } from 'next';
import { asArray } from '@mui/toolpad-core/utils/collections';
import type { GetServerSideProps, NextPage } from 'next';
import * as React from 'react';
import AppCanvas, { AppCanvasProps } from '../../src/canvas';

const App: NextPage<AppCanvasProps> = () => <AppCanvas basename="/app-canvas" />;
export const getServerSideProps: GetServerSideProps<AppCanvasProps> = async ({ query }) => {
const [appId] = asArray(query.path);
return {
props: {
basename: `/app-canvas/${appId}`,
},
};
};

const App: NextPage<AppCanvasProps> = (props) => <AppCanvas {...props} />;

export default App;
17 changes: 17 additions & 0 deletions packages/toolpad-app/src/appDom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1093,3 +1093,20 @@ export function deref(nodeRef: Maybe<NodeReference>): NodeId | null {
}
return null;
}

export function createDefaultDom(): AppDom {
let dom = createDom();
const appNode = getApp(dom);

// Create default page
const newPageNode = createNode(dom, 'page', {
name: 'Page 1',
attributes: {
title: createConst('Page 1'),
},
});

dom = addNode(dom, newPageNode, appNode, 'pages');

return dom;
}
10 changes: 7 additions & 3 deletions packages/toolpad-app/src/canvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import invariant from 'invariant';
import { throttle } from 'lodash-es';
import { CanvasEventsContext } from '@mui/toolpad-core/runtime';
import { ToolpadComponent } from '@mui/toolpad-core';
import ToolpadApp from '../runtime';
import { NodeHashes, RuntimeState } from '../types';
import getPageViewState from './getPageViewState';
Expand All @@ -22,11 +23,13 @@ const handleScreenUpdate = throttle(
);

export interface AppCanvasProps {
initialState?: AppCanvasState | null;
basename: string;
catalog?: Record<string, ToolpadComponent>;
}

export default function AppCanvas({ basename }: AppCanvasProps) {
const [state, setState] = React.useState<AppCanvasState | null>(null);
export default function AppCanvas({ catalog, basename, initialState = null }: AppCanvasProps) {
const [state, setState] = React.useState<AppCanvasState | null>(initialState);

const appRootRef = React.useRef<HTMLDivElement>();
const appRootCleanupRef = React.useRef<() => void>();
Expand Down Expand Up @@ -130,9 +133,10 @@ export default function AppCanvas({ basename }: AppCanvasProps) {
<CanvasEventsContext.Provider value={bridge.canvasEvents}>
<ToolpadApp
rootRef={onAppRoot}
catalog={catalog}
hidePreviewBanner
version="preview"
basename={`${basename}/${state.appId}`}
basename={basename}
state={state}
/>
</CanvasEventsContext.Provider>
Expand Down
2 changes: 2 additions & 0 deletions packages/toolpad-app/src/createRuntimeState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ function compileModules(dom: appDom.AppDom): Record<string, CompiledModule> {
const result: Record<string, CompiledModule> = {};
const root = appDom.getApp(dom);
const { codeComponents = [], pages = [] } = appDom.getChildNodes(dom, root);

for (const node of codeComponents) {
const src = node.attributes.code.value;
const name = `codeComponents/${node.id}`;
result[name] = compileModule(src, name);
}

for (const node of pages) {
const src = node.attributes.module?.value;
if (src) {
Expand Down
13 changes: 11 additions & 2 deletions packages/toolpad-app/src/runtime/ComponentsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,23 @@ function isToolpadComponent(maybeComponent: unknown): maybeComponent is ToolpadC
}

interface ComponentsContextProps {
catalog?: Record<string, ToolpadComponent>;
dom: appDom.AppDom;
children?: React.ReactNode;
}

export default function ComponentsContext({ dom, children }: ComponentsContextProps) {
export default function ComponentsContext({
catalog: componentsCatalog,
dom,
children,
}: ComponentsContextProps) {
const modules = useAppModules();

const components = React.useMemo(() => {
if (componentsCatalog) {
return componentsCatalog;
}

const catalog = getToolpadComponents(dom);
const result: Record<string, ToolpadComponent<any>> = {};

Expand Down Expand Up @@ -77,7 +86,7 @@ export default function ComponentsContext({ dom, children }: ComponentsContextPr
}

return result;
}, [dom, modules]);
}, [componentsCatalog, dom, modules]);

return <ComponentsContextProvider value={components}>{children}</ComponentsContextProvider>;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/toolpad-app/src/runtime/ToolpadApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,7 @@ const queryClient = new QueryClient({

export interface ToolpadAppProps {
rootRef?: React.Ref<HTMLDivElement>;
catalog?: Record<string, ToolpadComponent>;
hidePreviewBanner?: boolean;
basename: string;
version: VersionOrPreview;
Expand All @@ -938,6 +939,7 @@ export interface ToolpadAppProps {

export default function ToolpadApp({
rootRef,
catalog,
basename,
version,
hidePreviewBanner,
Expand Down Expand Up @@ -969,7 +971,7 @@ export default function ToolpadApp({
<ResetNodeErrorsKeyProvider value={resetNodeErrorsKey}>
<React.Suspense fallback={<AppLoading />}>
<AppModulesProvider modules={state.modules}>
<ComponentsContext dom={dom}>
<ComponentsContext catalog={catalog} dom={dom}>
<AppContextProvider value={appContext}>
<QueryClientProvider client={queryClient}>
<BrowserRouter basename={basename}>
Expand Down
6 changes: 1 addition & 5 deletions packages/toolpad-app/src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type BasicAuthConfig =
};

export type ServerConfig = {
databaseUrl: string;
databaseUrl?: string;
googleSheetsClientId?: string;
googleSheetsClientSecret?: string;
encryptionKeys: string[];
Expand All @@ -28,10 +28,6 @@ function readConfig(): ServerConfig & typeof sharedConfig {
throw new Error(`Serverside config can't be loaded on the client side`);
}

if (!process.env.TOOLPAD_DATABASE_URL) {
throw new Error(`App started without config env variable TOOLPAD_DATABASE_URL`);
}

// Whitespace separated, do not use spaces in your keys
const encryptionKeys: string[] =
process.env.TOOLPAD_ENCRYPTION_KEYS?.split(/\s+/).filter(Boolean) ?? [];
Expand Down
52 changes: 32 additions & 20 deletions packages/toolpad-app/src/server/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ const SELECT_APP_META = excludeFields(prisma.Prisma.AppScalarFieldEnum, ['dom'])

export type AppMeta = Omit<prisma.App, 'dom'>;

function getPrismaClient(): prisma.PrismaClient {
function createPrismaClient(): prisma.PrismaClient {
if (!process.env.TOOLPAD_DATABASE_URL) {
throw new Error(`App started without config env variable TOOLPAD_DATABASE_URL`);
}

if (process.env.NODE_ENV === 'production') {
return new prisma.PrismaClient();
}
Expand All @@ -37,7 +41,13 @@ function getPrismaClient(): prisma.PrismaClient {
return (globalThis as any).prisma;
}

const prismaClient = getPrismaClient();
let clientInstance: prisma.PrismaClient | undefined;
function getPrismaClient(): prisma.PrismaClient {
if (!clientInstance) {
clientInstance = createPrismaClient();
}
return clientInstance;
}

function deserializeValue(dbValue: string, type: prisma.DomNodeAttributeType): unknown {
const serialized = type === 'secret' ? decryptSecret(dbValue) : dbValue;
Expand Down Expand Up @@ -79,6 +89,7 @@ function decryptSecrets(dom: appDom.AppDom): appDom.AppDom {
}

export async function saveDom(appId: string, app: appDom.AppDom): Promise<void> {
const prismaClient = getPrismaClient();
await prismaClient.app.update({
where: {
id: appId,
Expand All @@ -89,6 +100,7 @@ export async function saveDom(appId: string, app: appDom.AppDom): Promise<void>
}

async function loadPreviewDomLegacy(appId: string): Promise<appDom.AppDom> {
const prismaClient = getPrismaClient();
const dbNodes = await prismaClient.domNode.findMany({
where: { appId },
include: { attributes: true },
Expand Down Expand Up @@ -137,6 +149,7 @@ async function loadPreviewDomLegacy(appId: string): Promise<appDom.AppDom> {
}

async function loadPreviewDom(appId: string): Promise<appDom.AppDom> {
const prismaClient = getPrismaClient();
const { dom } = await prismaClient.app.findUniqueOrThrow({
where: { id: appId },
});
Expand All @@ -153,6 +166,7 @@ async function loadPreviewDom(appId: string): Promise<appDom.AppDom> {
}

export async function getApps(): Promise<AppMeta[]> {
const prismaClient = getPrismaClient();
if (config.isDemo) {
return [];
}
Expand All @@ -166,33 +180,18 @@ export async function getApps(): Promise<AppMeta[]> {
}

export async function getActiveDeployments() {
const prismaClient = getPrismaClient();
return prismaClient.deployment.findMany({
distinct: ['appId'],
orderBy: { createdAt: 'desc' },
});
}

export async function getApp(id: string): Promise<AppMeta | null> {
const prismaClient = getPrismaClient();
return prismaClient.app.findUnique({ where: { id }, select: SELECT_APP_META });
}

function createDefaultDom(): appDom.AppDom {
let dom = appDom.createDom();
const appNode = appDom.getApp(dom);

// Create default page
const newPageNode = appDom.createNode(dom, 'page', {
name: 'Page 1',
attributes: {
title: appDom.createConst('Page 1'),
},
});

dom = appDom.addNode(dom, newPageNode, appNode, 'pages');

return dom;
}

export type CreateAppOptions = {
from?:
| {
Expand All @@ -210,6 +209,7 @@ export type CreateAppOptions = {
};

export async function createApp(name: string, opts: CreateAppOptions = {}): Promise<prisma.App> {
const prismaClient = getPrismaClient();
const { from } = opts;

if (config.recaptchaV3SecretKey) {
Expand Down Expand Up @@ -259,7 +259,7 @@ export async function createApp(name: string, opts: CreateAppOptions = {}): Prom
}

if (!dom) {
dom = createDefaultDom();
dom = appDom.createDefaultDom();
}

await saveDom(app.id, dom);
Expand All @@ -274,6 +274,7 @@ interface AppUpdates {
}

export async function updateApp(appId: string, updates: AppUpdates): Promise<void> {
const prismaClient = getPrismaClient();
await prismaClient.app.update({
where: {
id: appId,
Expand All @@ -287,6 +288,7 @@ export async function updateApp(appId: string, updates: AppUpdates): Promise<voi
}

export async function deleteApp(id: string): Promise<void> {
const prismaClient = getPrismaClient();
await prismaClient.app.delete({
where: { id },
select: {
Expand All @@ -303,13 +305,15 @@ export interface CreateReleaseParams {
export type ReleaseMeta = Pick<prisma.Release, keyof typeof SELECT_RELEASE_META>;

async function findLastReleaseInternal(appId: string) {
const prismaClient = getPrismaClient();
return prismaClient.release.findFirst({
where: { appId },
orderBy: { version: 'desc' },
});
}

export async function findLastRelease(appId: string): Promise<ReleaseMeta | null> {
const prismaClient = getPrismaClient();
return prismaClient.release.findFirst({
where: { appId },
orderBy: { version: 'desc' },
Expand All @@ -321,6 +325,7 @@ export async function createRelease(
appId: string,
{ description }: CreateReleaseParams,
): Promise<ReleaseMeta> {
const prismaClient = getPrismaClient();
const currentDom = await loadPreviewDom(appId);
const snapshot = Buffer.from(JSON.stringify(currentDom), 'utf-8');

Expand All @@ -341,6 +346,7 @@ export async function createRelease(
}

export async function getReleases(appId: string): Promise<ReleaseMeta[]> {
const prismaClient = getPrismaClient();
return prismaClient.release.findMany({
where: { appId },
select: SELECT_RELEASE_META,
Expand All @@ -351,6 +357,7 @@ export async function getReleases(appId: string): Promise<ReleaseMeta[]> {
}

export async function getRelease(appId: string, version: number): Promise<ReleaseMeta | null> {
const prismaClient = getPrismaClient();
return prismaClient.release.findUnique({
where: { release_app_constraint: { appId, version } },
select: SELECT_RELEASE_META,
Expand All @@ -362,6 +369,7 @@ export type Deployment = prisma.Deployment & {
};

export function getDeployments(appId: string): Promise<Deployment[]> {
const prismaClient = getPrismaClient();
return prismaClient.deployment.findMany({
where: { appId },
orderBy: { createdAt: 'desc' },
Expand All @@ -374,6 +382,7 @@ export function getDeployments(appId: string): Promise<Deployment[]> {
}

export async function createDeployment(appId: string, version: number): Promise<Deployment> {
const prismaClient = getPrismaClient();
return prismaClient.deployment.create({
data: {
app: {
Expand Down Expand Up @@ -401,6 +410,7 @@ export async function deploy(
}

export async function findActiveDeployment(appId: string): Promise<Deployment | null> {
const prismaClient = getPrismaClient();
return prismaClient.deployment.findFirst({
where: { appId },
orderBy: { createdAt: 'desc' },
Expand All @@ -417,6 +427,7 @@ function parseSnapshot(snapshot: Buffer): appDom.AppDom {
}

async function loadReleaseDom(appId: string, version: number): Promise<appDom.AppDom> {
const prismaClient = getPrismaClient();
const release = await prismaClient.release.findUnique({
where: { release_app_constraint: { appId, version } },
});
Expand Down Expand Up @@ -548,6 +559,7 @@ export async function loadRuntimeState(
}

export async function duplicateApp(id: string, name: string): Promise<AppMeta> {
const prismaClient = getPrismaClient();
const dom = await loadPreviewDom(id);
const appFromDom: CreateAppOptions = {
from: {
Expand Down
Loading