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

Add LoadingOverlay for Component And Bundle Loading #388

Merged
merged 20 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from 17 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
alextaing marked this conversation as resolved.
Show resolved Hide resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions e2e-tests/tests/infra/StudioPlaywrightPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,19 @@ export default class StudioPlaywrightPage {
await this.waitForIFrameImagesToLoad();
await expect(this.preview.locator("body")).toHaveScreenshot();
}

async reload() {
await this.page.reload();
await this.waitForLoadState();
}

/**
* Waits for the LoadingOverlay to finish.
*/
async waitForLoadState() {
await this.page.waitForLoadState();
await expect(
this.page.getByRole("button", { name: "Open Add Element Menu" })
).toBeVisible();
}
}
2 changes: 1 addition & 1 deletion e2e-tests/tests/infra/studioTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ export const studioTest = base.extend<StudioTestFixtures>({
const opts = { createRemote, debug, testInfo, tailwindConfigPath };
await setupAcceptance(opts, async (port: number, tmpDir: string) => {
await page.goto("localhost:" + port);
await page.waitForLoadState();
const studioPage = new StudioPlaywrightPage(page, tmpDir);
await studioPage.waitForLoadState();
await use(studioPage);
});
},
Expand Down
2 changes: 1 addition & 1 deletion e2e-tests/tests/remove-static-page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ studioTest("can remove a static page", async ({ page, studioPage }) => {
await studioPage.takePageScreenshotAfterImgRender();

// Ensure that the page is still deleted after a browser refresh.
await page.reload();
await studioPage.reload();
await studioPage.takePageScreenshotAfterImgRender();
});
2 changes: 1 addition & 1 deletion packages/studio-ui/.size-limit.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = [
{
path: "lib/src/index.js",
path: "lib/**/*.js",
alextaing marked this conversation as resolved.
Show resolved Hide resolved
limit: "850 kB",
gzip: false,
},
Expand Down
4 changes: 2 additions & 2 deletions packages/studio-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import LeftSidebar from "./components/LeftSidebar";

export default function App() {
return (
<div className="App">
<>
<Toast />
<div className="flex flex-col w-screen h-screen">
<ActionsBar />
Expand All @@ -16,6 +16,6 @@ export default function App() {
<EditorSidebar />
</div>
</div>
</div>
</>
);
}
75 changes: 75 additions & 0 deletions packages/studio-ui/src/AppWithLazyLoading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import LoadingOverlay from "./components/LoadingOverlay";
import { Suspense, lazy, useEffect, useState } from "react";
import useStudioStore from "./store/useStudioStore";
import ProgressBar from "./components/ProgressBar";
import loadComponents from "./utils/loadComponents";
import classNames from "classnames";

const AppPromise = import("./App");
const App = lazy(() => AppPromise);

export default function AppWithLazyLoading() {
const [loadedCount, totalCount] = useStudioStore((store) => [
Object.keys(store.fileMetadatas.UUIDToImportedComponent).length,
Object.keys(store.fileMetadatas.UUIDToFileMetadata).length,
]);
const componentsLoaded = loadedCount === totalCount;
const [appLoaded, setAppLoaded] = useState(false);

useEffect(() => {
loadComponents();
void AppPromise.then(() => setAppLoaded(true));
}, []);

return (
<LoadingOverlay
loading={!componentsLoaded || !appLoaded}
overlay={
<>
{renderComponentLoadingProgress(
loadedCount,
totalCount,
componentsLoaded
)}
{renderBundleMessage(appLoaded)}
</>
}
>
<Suspense>
alextaing marked this conversation as resolved.
Show resolved Hide resolved
<App />
</Suspense>
</LoadingOverlay>
);
}

function renderComponentLoadingProgress(
loadedCount: number,
totalCount: number,
componentsLoaded: boolean
) {
const msg = componentsLoaded
? "components loaded!"
: `loading components... (${loadedCount}/${totalCount})`;

return (
<>
<ProgressBar progressFraction={loadedCount / totalCount} />
<div
className={classNames("text-indigo-800", {
"animate-pulse": !componentsLoaded,
})}
>
{msg}
</div>
</>
);
}

function renderBundleMessage(appLoaded: boolean) {
const className = classNames("text-sky-600 mt-4", {
"animate-pulse": !appLoaded,
});
const msg = appLoaded ? "JS bundle loaded!" : "... loading JS bundle ...";
oshi97 marked this conversation as resolved.
Show resolved Hide resolved

return <div className={className}>{msg}</div>;
}
4 changes: 2 additions & 2 deletions packages/studio-ui/src/components/ComponentPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ function useElement(
} else {
const importedComponent = UUIDToImportedComponent[c.metadataUUID];
if (!importedComponent) {
console.warn(
`Expected to find component loaded for ${c.componentName} but none found - possibly due to a race condition.`
console.error(
`Expected to find component loaded for ${c.componentName} but none found.`
);
return undefined;
}
Expand Down
35 changes: 35 additions & 0 deletions packages/studio-ui/src/components/LoadingOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { PropsWithChildren, ReactNode } from "react";
import classNames from "classnames";

export default function LoadingOverlay(
props: PropsWithChildren<{
loading: boolean;
overlay: ReactNode;
}>
) {
const { loading, overlay } = props;

const childrenWrapperClassname = classNames(
"transition-opacity duration-500",
{
"opacity-100": !loading,
"opacity-0 pointer-events-none": loading,
}
);

const overlayClassname = classNames(
"transition-opacity duration-500",
"h-full w-full flex absolute justify-center items-center flex-col",
{
"opacity-100": loading,
"opacity-0 pointer-events-none": !loading,
}
);

return (
<>
<div className={overlayClassname}>{overlay}</div>
<div className={childrenWrapperClassname}>{props.children}</div>
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import useImportedComponents from "../hooks/useImportedComponents";
import useStudioStore from "../store/useStudioStore";
import PreviewPanel from "./PreviewPanel";
import Highlighter from "./Highlighter";
import IFramePortal from "./IFramePortal";
Expand All @@ -14,10 +12,6 @@ export default function PreviewWithUseComponents() {
anchorSelect: "",
});
const [iframeEl, setIframeEl] = useState<HTMLIFrameElement | null>(null);
const componentTree = useStudioStore((store) =>
store.actions.getComponentTree()
);
void useImportedComponents(componentTree);

return (
<>
Expand Down
21 changes: 21 additions & 0 deletions packages/studio-ui/src/components/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { CSSProperties, memo, useMemo } from "react";

export default memo(ProgressBar);

function ProgressBar(props: { progressFraction: number }) {
const progressStyles: CSSProperties = useMemo(() => {
return {
width: `${Math.floor(props.progressFraction * 20) * 5}%`,
transition: "width 1s",
};
}, [props.progressFraction]);

return (
<div className="w-9/12 bg-indigo-200 h-8 rounded items-center flex">
<div
className="bg-indigo-400 h-full rounded-l animate-pulse"
style={progressStyles}
></div>
</div>
);
}
22 changes: 0 additions & 22 deletions packages/studio-ui/src/hooks/useImportedComponents.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion packages/studio-ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { default as App } from "./App";
export { default as AppWithLazyLoading } from "./AppWithLazyLoading";
export { hotReloadStudioData, hotReloadGitData } from "./store/hotReloadStore";
export { StudioHMRUpdateID, GitDataHMRUpdateID } from "@yext/studio-plugin";
5 changes: 0 additions & 5 deletions packages/studio-ui/src/store/StudioActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import StudioConfigSlice from "./models/slices/StudioConfigSlice";
import AddComponentAction from "./StudioActions/AddComponentAction";
import CreateComponentStateAction from "./StudioActions/CreateComponentStateAction";
import UpdateActivePageAction from "./StudioActions/UpdateActivePageAction";
import ImportComponentAction from "./StudioActions/ImportComponentAction";
import GenerateTestDataAction from "./StudioActions/GenerateTestDataAction";
import CreatePageAction from "./StudioActions/CreatePageAction";
import dynamicImportFromBrowser from "../utils/dynamicImportFromBrowser";
Expand All @@ -27,7 +26,6 @@ export default class StudioActions {
public addComponent: AddComponentAction["addComponent"];
public createComponentState: CreateComponentStateAction["createComponentState"];
public updateActivePage: UpdateActivePageAction["updateActivePage"];
public importComponent: ImportComponentAction["importComponent"];
public generateTestData: GenerateTestDataAction["generateTestData"];
public createPage: CreatePageAction["createPage"];

Expand All @@ -45,9 +43,6 @@ export default class StudioActions {
getPages,
this
).updateActivePage;
this.importComponent = new ImportComponentAction(
getFileMetadatas
).importComponent;
this.generateTestData = new GenerateTestDataAction(
getPages,
this
Expand Down

This file was deleted.

6 changes: 1 addition & 5 deletions packages/studio-ui/src/store/hotReloadStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
import useStudioStore from "./useStudioStore";
import removeTopLevelFragments from "../utils/removeTopLevelFragments";
import dynamicImportFromBrowser from "../utils/dynamicImportFromBrowser";
import path from "path-browserify";
import getFunctionComponent from "../utils/getFunctionComponent";

/**
Expand Down Expand Up @@ -66,10 +65,7 @@ async function syncFileMetadata(studioData: StudioData, file: string) {
const importedFile = await dynamicImportFromBrowser(
file + `?timestamp=${Date.now()}`
);
const componentFunction = getFunctionComponent(
importedFile,
path.basename(file, ".tsx")
);
const componentFunction = getFunctionComponent(importedFile);
if (componentFunction) {
useStudioStore
.getState()
Expand Down
12 changes: 2 additions & 10 deletions packages/studio-ui/src/utils/getFunctionComponent.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import { FunctionComponent } from "react";
import { ImportType } from "../store/models/ImportType";

export default function getFunctionComponent(
importedValue: Record<string, unknown>,
name: string
importedValue: Record<string, unknown>
): ImportType | undefined {
if (typeof importedValue[name] === "function") {
oshi97 marked this conversation as resolved.
Show resolved Hide resolved
return importedValue[name] as FunctionComponent;
} else if (typeof importedValue["default"] === "function") {
return importedValue["default"] as FunctionComponent;
} else {
console.error(`${name} is not a valid functional component.`);
}
return importedValue["default"] as ImportType | undefined;
}
Loading