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

Make sure to only send frontend dom nodes to the browser #635

Merged
merged 4 commits into from
Jul 6, 2022
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
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