Skip to content

Commit

Permalink
Make sure to only send frontend dom nodes to the browser (#635)
Browse files Browse the repository at this point in the history
* Make sure to only send frontend dom nodes to the browser

* move dom depenedncy out of ToolpadApp

* remove unused
  • Loading branch information
Janpot authored Jul 6, 2022
1 parent 7622368 commit aee51c2
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import type { GetServerSideProps, NextPage } from 'next';
import * as React from 'react';
import { asArray } from '../../../../src/utils/collections';
import ToolpadApp, { ToolpadAppProps } from '../../../../src/runtime/ToolpadApp';
import { createRenderTree } from '../../../../src/appDom';

export const getServerSideProps: GetServerSideProps<ToolpadAppProps> = async (context) => {
const { loadVersionedDom, parseVersion } = await import('../../../../src/server/data');
const { loadDom, parseVersion } = await import('../../../../src/server/data');

const [appId] = asArray(context.query.appId);
const version = parseVersion(context.query.version);
Expand All @@ -14,12 +15,12 @@ export const getServerSideProps: GetServerSideProps<ToolpadAppProps> = async (co
};
}

const dom = await loadVersionedDom(appId, version);
const dom = await loadDom(appId, version);

return {
props: {
appId,
dom,
dom: createRenderTree(dom),
version,
basename: `/app/${appId}/${version}`,
},
Expand Down
7 changes: 4 additions & 3 deletions packages/toolpad-app/pages/deploy/[appId]/[[...path]].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import type { GetServerSideProps, NextPage } from 'next';
import * as React from 'react';
import { asArray } from '../../../src/utils/collections';
import ToolpadApp, { ToolpadAppProps } from '../../../src/runtime/ToolpadApp';
import { createRenderTree } from '../../../src/appDom';

export const getServerSideProps: GetServerSideProps<ToolpadAppProps> = async (context) => {
const { loadVersionedDom, findActiveDeployment } = await import('../../../src/server/data');
const { loadDom, findActiveDeployment } = await import('../../../src/server/data');

const [appId] = asArray(context.query.appId);

Expand All @@ -24,12 +25,12 @@ export const getServerSideProps: GetServerSideProps<ToolpadAppProps> = async (co

const { version } = activeDeployment;

const dom = await loadVersionedDom(appId, version);
const dom = await loadDom(appId, version);

return {
props: {
appId,
dom,
dom: createRenderTree(dom),
version,
basename: `/deploy/${appId}`,
},
Expand Down
17 changes: 14 additions & 3 deletions packages/toolpad-app/src/appDom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,15 +768,26 @@ export function getNewParentIndexAfterNode(
return createFractionalIndex(node.parentIndex, nodeAfter?.parentIndex || null);
}

const RENDERTREE_NODES = ['app', 'page', 'element', 'query', 'theme', 'codeComponent'] as const;

export type RenderTreeNodeType = typeof RENDERTREE_NODES[number];
export type RenderTreeNode = { [K in RenderTreeNodeType]: AppDomNodeOfType<K> }[RenderTreeNodeType];
export type RenderTreeNodes = Record<NodeId, RenderTreeNode>;

export interface RenderTree {
root: NodeId;
nodes: RenderTreeNodes;
}

/**
* We need to make sure no secrets end up in the frontend html, so let's only send the
* nodes that we need to build frontend, and that we know don't contain secrets.
* TODO: Would it make sense to create a separate datastructure that represents the render tree?
*/
export function createRenderTree(dom: AppDom): AppDom {
const frontendNodes = new Set(['app', 'page', 'element', 'query', 'theme', 'codeComponent']);
export function createRenderTree(dom: AppDom): RenderTree {
const frontendNodes = new Set<string>(RENDERTREE_NODES);
return {
...dom,
nodes: filterValues(dom.nodes, (node) => frontendNodes.has(node.type)) as AppDomNodes,
nodes: filterValues(dom.nodes, (node) => frontendNodes.has(node.type)) as RenderTreeNodes,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ const EXTRA_LIBS_HTTP_MODULES = [
];

interface CodeComponentEditorContentProps {
theme?: appDom.ThemeNode;
codeComponentNode: appDom.CodeComponentNode;
}

function CodeComponentEditorContent({ theme, codeComponentNode }: CodeComponentEditorContentProps) {
function CodeComponentEditorContent({ codeComponentNode }: CodeComponentEditorContentProps) {
const domApi = useDomApi();
const dom = useDom();

const { data: typings } = useQuery<Record<string, string>>(['/typings.json'], async () => {
return fetch('/typings.json').then((res) => res.json());
Expand Down Expand Up @@ -180,7 +180,7 @@ function CodeComponentEditorContent({ theme, codeComponentNode }: CodeComponentE
resetKeys={[CodeComponent]}
fallbackRender={({ error: runtimeError }) => <ErrorAlert error={runtimeError} />}
>
<AppThemeProvider node={theme}>
<AppThemeProvider dom={dom}>
<CodeComponent {...defaultProps} />
</AppThemeProvider>
</ErrorBoundary>
Expand All @@ -203,14 +203,8 @@ export default function CodeComponentEditor({ appId }: CodeComponentEditorProps)
const dom = useDom();
const { nodeId } = useParams();
const codeComponentNode = appDom.getMaybeNode(dom, nodeId as NodeId, 'codeComponent');
const root = appDom.getApp(dom);
const { themes = [] } = appDom.getChildNodes(dom, root);
return codeComponentNode ? (
<CodeComponentEditorContent
key={nodeId}
codeComponentNode={codeComponentNode}
theme={themes[0]}
/>
<CodeComponentEditorContent key={nodeId} codeComponentNode={codeComponentNode} />
) : (
<Typography sx={{ p: 4 }}>Non-existing Code Component &quot;{nodeId}&quot;</Typography>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/toolpad-app/src/runtime/AppCanvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as appDom from '../../appDom';

export interface AppCanvasState {
appId: string;
dom: appDom.AppDom;
dom: appDom.RenderTree;
}

export interface ToolpadBridge {
Expand Down
12 changes: 9 additions & 3 deletions packages/toolpad-app/src/runtime/AppThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,17 @@ export function createToolpadTheme(themeNode?: appDom.ThemeNode | null): Theme {
}

export interface ThemeProviderProps {
node?: appDom.ThemeNode | null;
dom: appDom.AppDom;
children?: React.ReactNode;
}

export default function AppThemeProvider({ node, children }: ThemeProviderProps) {
const theme = React.useMemo(() => createToolpadTheme(node), [node]);
export default function AppThemeProvider({ dom, children }: ThemeProviderProps) {
const theme = React.useMemo(() => {
const root = appDom.getApp(dom);
const { themes = [] } = appDom.getChildNodes(dom, root);
const themeNode = themes.length > 0 ? themes[0] : null;
return createToolpadTheme(themeNode);
}, [dom]);

return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
62 changes: 33 additions & 29 deletions packages/toolpad-app/src/runtime/ToolpadApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,29 @@ function RenderedPage({ nodeId }: RenderedNodeProps) {
);
}

interface RenderedPagesProps {
dom: appDom.AppDom;
}

function RenderedPages({ dom }: RenderedPagesProps) {
const root = appDom.getApp(dom);
const { pages = [] } = appDom.getChildNodes(dom, root);

return (
<Routes>
<Route path="/" element={<Navigate replace to="/pages" />} />
<Route path="/pages" element={<AppOverview dom={dom} />} />
{pages.map((page) => (
<Route
key={page.id}
path={`/pages/${page.id}`}
element={<RenderedPage nodeId={page.id} />}
/>
))}
</Routes>
);
}

const FullPageCentered = styled('div')({
width: '100%',
height: '100%',
Expand All @@ -574,6 +597,14 @@ function AppError({ error }: FallbackProps) {
);
}

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});

export interface ToolpadAppProps {
hidePreviewBanner?: boolean;
basename: string;
Expand All @@ -589,25 +620,8 @@ export default function ToolpadApp({
dom,
hidePreviewBanner,
}: ToolpadAppProps) {
const root = appDom.getApp(dom);
const { pages = [], themes = [] } = appDom.getChildNodes(dom, root);

const theme = themes.length > 0 ? themes[0] : null;

const appContext = React.useMemo(() => ({ appId, version }), [appId, version]);

const queryClient = React.useMemo(
() =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
}),
[],
);

const [resetNodeErrorsKey, setResetNodeErrorsKey] = React.useState(0);

React.useEffect(() => setResetNodeErrorsKey((key) => key + 1), [dom]);
Expand All @@ -616,7 +630,7 @@ export default function ToolpadApp({
<AppRoot id={HTML_ID_APP_ROOT}>
<NoSsr>
<DomContextProvider value={dom}>
<AppThemeProvider node={theme}>
<AppThemeProvider dom={dom}>
<CssBaseline />
{version === 'preview' && !hidePreviewBanner ? (
<Alert severity="info">This is a preview version of the application.</Alert>
Expand All @@ -629,17 +643,7 @@ export default function ToolpadApp({
<AppContextProvider value={appContext}>
<QueryClientProvider client={queryClient}>
<BrowserRouter basename={basename}>
<Routes>
<Route path="/" element={<Navigate replace to="/pages" />} />
<Route path="/pages" element={<AppOverview dom={dom} />} />
{pages.map((page) => (
<Route
key={page.id}
path={`/pages/${page.id}`}
element={<RenderedPage nodeId={page.id} />}
/>
))}
</Routes>
<RenderedPages dom={dom} />
</BrowserRouter>
</QueryClientProvider>
</AppContextProvider>
Expand Down
14 changes: 7 additions & 7 deletions packages/toolpad-app/src/server/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export async function saveDom(appId: string, app: appDom.AppDom): Promise<void>
]);
}

export async function loadDom(appId: string): Promise<appDom.AppDom> {
async function loadPreviewDom(appId: string): Promise<appDom.AppDom> {
const dbNodes = await prisma.domNode.findMany({
where: { appId },
include: { attributes: true },
Expand Down Expand Up @@ -264,7 +264,7 @@ export async function createRelease(
appId: string,
{ description }: CreateReleaseParams,
): Promise<Pick<Release, keyof typeof SELECT_RELEASE_META>> {
const currentDom = await loadDom(appId);
const currentDom = await loadPreviewDom(appId);
const snapshot = Buffer.from(JSON.stringify(currentDom), 'utf-8');

const lastRelease = await findLastReleaseInternal(appId);
Expand Down Expand Up @@ -339,15 +339,15 @@ async function getConnection<P = unknown>(
appId: string,
id: string,
): Promise<appDom.ConnectionNode<P>> {
const dom = await loadDom(appId);
const dom = await loadPreviewDom(appId);
return appDom.getNode(dom, id as NodeId, 'connection') as appDom.ConnectionNode<P>;
}

export async function getConnectionParams<P = unknown>(
appId: string,
id: string,
): Promise<P | null> {
const dom = await loadDom(appId);
const dom = await loadPreviewDom(appId);
const node = appDom.getNode(dom, id as NodeId, 'connection') as appDom.ConnectionNode<P>;
return node.attributes.params.value;
}
Expand All @@ -357,7 +357,7 @@ export async function setConnectionParams<P>(
connectionId: NodeId,
params: P,
): Promise<void> {
let dom = await loadDom(appId);
let dom = await loadPreviewDom(appId);
const existing = appDom.getNode(dom, connectionId, 'connection');

dom = appDom.setNodeNamespacedProp(
Expand Down Expand Up @@ -439,6 +439,6 @@ export function parseVersion(param?: string | string[]): VersionOrPreview | null
return Number.isNaN(parsed) ? null : parsed;
}

export async function loadVersionedDom(appId: string, version: VersionOrPreview) {
return version === 'preview' ? loadDom(appId) : loadReleaseDom(appId, version);
export async function loadDom(appId: string, version: VersionOrPreview = 'preview') {
return version === 'preview' ? loadPreviewDom(appId) : loadReleaseDom(appId, version);
}
4 changes: 2 additions & 2 deletions packages/toolpad-app/src/server/handleDataRequest.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NextApiRequest, NextApiResponse } from 'next';
import Cors from 'cors';
import { NodeId } from '@mui/toolpad-core';
import { execQuery, loadVersionedDom } from './data';
import { execQuery, loadDom } from './data';
import initMiddleware from './initMiddleware';
import { ApiResult, VersionOrPreview } from '../types';
import * as appDom from '../appDom';
Expand All @@ -27,7 +27,7 @@ export default async (
) => {
await cors(req, res);
const queryNodeId = req.query.queryId as NodeId;
const dom = await loadVersionedDom(appId, version);
const dom = await loadDom(appId, version);
const query = appDom.getNode(dom, queryNodeId, 'query');

const result = await execQuery(
Expand Down

0 comments on commit aee51c2

Please sign in to comment.