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

fix(preview): notify parent from iframe when preview is not available #8778

Merged
merged 3 commits into from
Apr 15, 2024
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
21 changes: 21 additions & 0 deletions .bitmap
Original file line number Diff line number Diff line change
Expand Up @@ -1766,6 +1766,20 @@
"mainFile": "index.ts",
"rootDir": "components/ui/navigation/lane-switcher"
},
"ui/pages/preview-not-found": {
"name": "ui/pages/preview-not-found",
"scope": "teambit.ui-foundation",
"version": "0.0.84",
"mainFile": "index.ts",
"rootDir": "components/ui/pages/preview-not-found"
},
"ui/pages/static-error": {
"name": "ui/pages/static-error",
"scope": "teambit.ui-foundation",
"version": "0.0.92",
"mainFile": "index.ts",
"rootDir": "components/ui/pages/static-error"
},
"ui/preview-placeholder": {
"name": "ui/preview-placeholder",
"scope": "teambit.preview",
Expand All @@ -1787,6 +1801,13 @@
"mainFile": "index.ts",
"rootDir": "components/ui/react-router/slot-router"
},
"ui/rendering/html": {
"name": "ui/rendering/html",
"scope": "teambit.ui-foundation",
"version": "0.0.85",
"mainFile": "index.ts",
"rootDir": "components/ui/rendering/html"
},
"ui/test-compare": {
"name": "ui/test-compare",
"scope": "teambit.defender",
Expand Down
12 changes: 12 additions & 0 deletions components/ui/pages/preview-not-found/image-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React, { SVGProps } from 'react';

export function ImageIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" {...props}>
<path
fill="currentColor"
d="M576 0c35.347 0 64 28.651 64 64v384c0 35.347-28.653 64-64 64H64c-35.349 0-64-28.652-64-64V64C0 28.651 28.651 0 64 0h512zM455.228 330.745l-2.988 2.692-49.519 50.565 98.016 95.968 75.264.032c11.596 0 21.756-6.173 27.367-15.393 2.327-3.844 3.876-8.204 4.42-12.877l.215-3.731v-4.576L497.496 332.957c-11.661-11.413-29.82-12.108-42.269-2.212zm-259.512-67.723l-3.013 2.659L32.002 426.333v21.108c0 16.413 12.356 29.936 28.269 31.785l3.733.215 388.707-.019-77.195-74.289-137.553-139.452c-11.535-11.535-29.685-12.422-42.245-2.661zM576 32H64c-16.413 0-29.936 12.356-31.785 28.269L32 64v317.088l138.077-138.031c23.675-23.675 61.292-24.923 86.438-3.739l4.38 4.053 117.794 119.448 50.685-51.764c23.425-23.93 61.025-25.575 86.396-4.659l4.596 4.171 87.635 87.606V63.997c0-16.413-12.356-29.936-28.269-31.785l-3.731-.215zM416 96c35.347 0 64 28.651 64 64s-28.652 64-64 64-64-28.651-64-64 28.652-64 64-64zm0 32c-17.675 0-32 14.326-32 32s14.325 32 32 32 32-14.326 32-32-14.325-32-32-32z"
/>
</svg>
);
}
2 changes: 2 additions & 0 deletions components/ui/pages/preview-not-found/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { PreviewNotFoundPage } from './preview-not-found';
export type { PreviewNotFoundPageProps } from './preview-not-found';
32 changes: 32 additions & 0 deletions components/ui/pages/preview-not-found/preview-not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { CSSProperties } from 'react';
import { ImageIcon } from './image-icon';

const styles: Record<string, CSSProperties> = {
container: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',

height: '100%',
},
image: {
width: '2.6em',
marginBottom: '1em',
},
message: {
fontSize: '1em',
textAlign: 'center',
},
};

export type PreviewNotFoundPageProps = React.HTMLAttributes<HTMLDivElement>;

export function PreviewNotFoundPage(props: PreviewNotFoundPageProps) {
return (
<div {...props} style={{ ...styles.container, ...props.style }}>
<ImageIcon style={styles.image} />
<div style={styles.message}>No preview available</div>
</div>
);
}
1 change: 1 addition & 0 deletions components/ui/pages/static-error/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { noPreview, notFound, serverError } from './static-error-pages';
14 changes: 14 additions & 0 deletions components/ui/pages/static-error/render-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React, { ReactNode } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { Html, Assets } from '@teambit/ui-foundation.ui.rendering.html';

export function fullPageToStaticString(content: ReactNode, assets?: Assets) {
const html = (
<Html assets={assets} fullHeight>
{content}
</Html>
);
const stringified = renderToStaticMarkup(html);

return stringified;
}
56 changes: 56 additions & 0 deletions components/ui/pages/static-error/static-error-pages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import { staticBookFontClass, staticBookFontUrl } from '@teambit/base-ui.theme.fonts.book';
import { NotFoundPage } from '@teambit/design.ui.pages.not-found';
import { ServerErrorPage } from '@teambit/design.ui.pages.server-error';
import { PreviewNotFoundPage } from '@teambit/ui-foundation.ui.pages.preview-not-found';

import { fullPageToStaticString } from './render-page';

const center = `
body {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;

font-family: sans-serif;
}
`;

const sizing = `
body {
font-size: 18px;
color: #878c9a;
width: 100% !important;
height: 500px;
}

@media screen and (max-width: 250px) {
body {
font-size: 14px;
color: #c7c7c7;
}
}
`;

const assets = {
style: [center],
css: [staticBookFontUrl],
};

const noPreviewAssets = {
style: [center, sizing],
css: [staticBookFontUrl],
};

export function notFound(): string {
return fullPageToStaticString(<NotFoundPage className={staticBookFontClass} />, assets);
}

export function serverError(): string {
return fullPageToStaticString(<ServerErrorPage className={staticBookFontClass} />, assets);
}

export function noPreview(): string {
return fullPageToStaticString(<PreviewNotFoundPage className={staticBookFontClass} />, noPreviewAssets);
}
10 changes: 10 additions & 0 deletions components/ui/rendering/html/dev-tools.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';

export function CrossIframeDevTools() {
return (
<script>
{'/* Allow to use react dev-tools inside the examples */\n'}
{'try { window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__; } catch {}'}
</script>
);
}
6 changes: 6 additions & 0 deletions components/ui/rendering/html/full-height-style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';

export function FullHeightStyle() {
// return <style> {'html { height: 100%; } body { margin: 0; height: 100%; } #root { height: 100%; }'} </style>;
return <style>{'body { margin: 0; }'}</style>;
}
92 changes: 92 additions & 0 deletions components/ui/rendering/html/html.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import { CrossIframeDevTools } from './dev-tools';
import { MountPoint, fillMountPoint } from './mount-point';
import { popAssets, StoredAssets } from './stored-assets';
import { SsrStyles, removeSsrStyles } from './ssr-styles';
import { FullHeightStyle } from './full-height-style';

export const LOAD_EVENT = '_DOM_LOADED_';

export type Assets = Partial<{
/** page title */
title: string;
/** js files to load */
js: string[];
/** css files to load */
css: string[];
/** raw css styles */
style: string[];
/** raw data to be stored in the dom. Use Html.popAssets to retrieve it from the dom */
json: Record<string, string>;
}>;

export interface HtmlProps extends React.HtmlHTMLAttributes<HTMLHtmlElement> {
withDevTools?: boolean;
fullHeight?: boolean;
assets?: Assets;
ssr?: boolean;
notifyParentOnLoad?: boolean;
}

const NotifyParentScript = () => (
<script
dangerouslySetInnerHTML={{
__html: `
// only send loaded event when mounted in an iframe
if (window.parent && window !== window.parent) {
document.addEventListener('DOMContentLoaded', function() {
window.parent.postMessage({ event: '${LOAD_EVENT}' }, '*');
});
};
`,
}}
></script>
);

/** html template for the main UI, when ssr is active */
export function Html({
assets = {},
withDevTools = false,
fullHeight,
ssr,
children = <MountPoint />,
notifyParentOnLoad = true,
...rest
}: HtmlProps) {
return (
<html lang="en" {...rest}>
<head>
<title>{assets.title || 'bit scope'}</title>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

{ssr && <SsrStyles />}
{fullHeight && <FullHeightStyle />}
{withDevTools && <CrossIframeDevTools />}

{assets.style?.map((x, idx) => (
<style key={idx}>{x}</style>
))}
{assets.css?.map((x, idx) => (
<link key={idx} href={x} rel="stylesheet" type="text/css" />
))}
{notifyParentOnLoad && <NotifyParentScript />}
</head>
<body>
{children}
{assets.json && <StoredAssets data={assets.json} />}
{/* load scripts after showing the the whole html */}
{assets.js?.map((x, idx) => (
<script key={idx} src={x} />
))}
</body>
</html>
);
}

Html.fillContent = fillMountPoint;
Html.popAssets = popAssets;

export function ssrCleanup() {
removeSsrStyles();
}
4 changes: 4 additions & 0 deletions components/ui/rendering/html/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { Html, ssrCleanup, LOAD_EVENT } from './html';
export { MountPoint, mountPointId } from './mount-point';
export { popAssets } from './stored-assets';
export type { HtmlProps, Assets } from './html';
13 changes: 13 additions & 0 deletions components/ui/rendering/html/mount-point.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React, { ReactNode } from 'react';

export const mountPointId = 'root';
const placeholderRegex = /<div id="root"><\/div>/;

export function MountPoint({ children }: { children?: ReactNode }) {
return <div id={mountPointId}>{children}</div>;
}

export function fillMountPoint(htmlTemplate: string, content: string) {
const filled = htmlTemplate.replace(placeholderRegex, content);
return filled;
}
15 changes: 15 additions & 0 deletions components/ui/rendering/html/ssr-styles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';

export function SsrStyles() {
return (
<style id="before-hydrate-styles">
.--ssr-hidden {'{'}
display: none;
{'}'}
</style>
);
}

export function removeSsrStyles() {
document.getElementById('before-hydrate-styles')?.remove();
}
30 changes: 30 additions & 0 deletions components/ui/rendering/html/stored-assets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';

export function StoredAssets({ data }: { data: Record<string, string> }) {
return (
<div className="state" style={{ display: 'none' }}>
{Object.entries(data).map(([key, content]) => (
// TODO - we falsely assume content is html safe
<script key={key} data-aspect={key} type="application/json" dangerouslySetInnerHTML={{ __html: content }} />
))}
</div>
);
}

/** read and remove stored data from the dom */
export function popAssets() {
const rawAssets = new Map<string, string>();

const inDom = Array.from(document.querySelectorAll('body > .state > *'));

inDom.forEach((elem) => {
const aspectName = elem.getAttribute('data-aspect');
if (!aspectName) return;

rawAssets.set(aspectName, elem.innerHTML);
});

document.querySelector('body > .state')?.remove();

return rawAssets;
}
14 changes: 14 additions & 0 deletions scopes/preview/ui/component-preview/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { compact } from 'lodash';
import { connectToChild } from 'penpal';
import { usePubSubIframe } from '@teambit/pubsub';
import { ComponentModel } from '@teambit/component';
import { LOAD_EVENT } from '@teambit/ui-foundation.ui.rendering.html';
import { toPreviewUrl } from './urls';
import { computePreviewScale } from './compute-preview-scale';
import { useIframeContentHeight } from './use-iframe-content-height';
Expand Down Expand Up @@ -108,6 +109,19 @@ export function ComponentPreview({
usePubSubIframe(pubsub ? currentRef : undefined);
// const pubsubContext = usePubSub();
// pubsubContext?.connect(iframeHeight);

useEffect(() => {
const handleLoad = (event) => {
if (event.data && event.data.event === LOAD_EVENT) {
onLoad && onLoad(event);
}
};
window.addEventListener('message', handleLoad);
return () => {
window.removeEventListener('message', handleLoad);
};
}, []);

useEffect(() => {
if (!iframeRef.current) return;
connectToChild({
Expand Down
3 changes: 0 additions & 3 deletions workspace.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -295,11 +295,8 @@
"@teambit/ui-foundation.ui.notifications.notification-center": "^0.0.523",
"@teambit/ui-foundation.ui.notifications.notification-context": "^0.0.501",
"@teambit/ui-foundation.ui.notifications.store": "^0.0.500",
"@teambit/ui-foundation.ui.pages.preview-not-found": "^0.0.84",
"@teambit/ui-foundation.ui.pages.static-error": "^0.0.92",
"@teambit/ui-foundation.ui.react-router.extend-path": "^0.0.486",
"@teambit/ui-foundation.ui.react-router.use-query": "^0.0.501",
"@teambit/ui-foundation.ui.rendering.html": "^0.0.85",
"@teambit/ui-foundation.ui.side-bar": "^0.0.875",
"@teambit/ui-foundation.ui.top-bar": "^0.0.514",
"@teambit/ui-foundation.ui.tree.drawer": "^0.0.518",
Expand Down