Skip to content

Commit

Permalink
feat(web): support dev plugins (#1059)
Browse files Browse the repository at this point in the history
  • Loading branch information
airslice authored Jul 19, 2024
1 parent 1de9eb0 commit 2ccc95a
Show file tree
Hide file tree
Showing 12 changed files with 238 additions and 5 deletions.
97 changes: 97 additions & 0 deletions web/src/beta/features/Navbar/useDevPlugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as yaml from "js-yaml";
import { useCallback, useEffect } from "react";

import { fetchAndZipFiles } from "@reearth/beta/utils/file";
import { usePluginsFetcher } from "@reearth/services/api";
import { config } from "@reearth/services/config";
import { useDevPluginExtensionRenderKey, useDevPluginExtensions } from "@reearth/services/state";

type ReearthYML = {
id: string;
version: string;
extensions?: {
id: string;
}[];
};

type Props = {
sceneId?: string;
};

export default ({ sceneId }: Props) => {
const [devPluginExtensions, setDevPluginExtensions] = useDevPluginExtensions();
const [_, updateDevPluginExtensionRenderKey] = useDevPluginExtensionRenderKey();
const { useUploadPluginWithFile } = usePluginsFetcher();

const handleDevPluginExtensionsReload = useCallback(() => {
updateDevPluginExtensionRenderKey(prev => prev + 1);
}, [updateDevPluginExtensionRenderKey]);

const handleInstallPluginFromFile = useCallback(
async (file: File) => {
if (!sceneId || !file) return;
useUploadPluginWithFile(sceneId, file);
},
[sceneId, useUploadPluginWithFile],
);

const handleDevPluginsInstall = useCallback(async () => {
if (!sceneId) return;

const { devPluginUrls } = config() ?? {};
if (!devPluginUrls || devPluginUrls.length === 0) return;

devPluginUrls.forEach(async url => {
const file: File | undefined = await getPluginZipFromUrl(url);
if (!file) return;
handleInstallPluginFromFile(file);
});
}, [sceneId, handleInstallPluginFromFile]);

useEffect(() => {
const { devPluginUrls } = config() ?? {};
if (!devPluginUrls || devPluginUrls.length === 0) return;

const fetchExtensions = async () => {
const extensions = await Promise.all(
devPluginUrls.map(async url => {
const response = await fetch(`${url}/reearth.yml`);
if (!response.ok) {
throw new Error(`Failed to fetch the file: ${response.statusText}`);
}
const yamlText = await response.text();
const data = yaml.load(yamlText) as ReearthYML;
return data.extensions?.map(e => ({ id: e.id, url: `${url}/${e.id}.js` })) ?? [];
}),
);
setDevPluginExtensions(extensions.flatMap(e => e));
};

fetchExtensions();
}, [setDevPluginExtensions]);

return { devPluginExtensions, handleDevPluginsInstall, handleDevPluginExtensionsReload };
};

async function getPluginZipFromUrl(url: string) {
try {
const response = await fetch(`${url}/reearth.yml`);
if (!response.ok) {
throw new Error(`Failed to fetch the file: ${response.statusText}`);
}
const yamlText = await response.text();
const data = yaml.load(yamlText) as ReearthYML;

const extensionUrls = data?.extensions?.map(extensions => `${url}/${extensions.id}.js`);
if (!extensionUrls) return;

const file = await fetchAndZipFiles(
[...extensionUrls, `${url}/reearth.yml`],
`${data.id}-${data.version}.zip`,
);

return file;
} catch (_err) {
return undefined;
}
}
30 changes: 29 additions & 1 deletion web/src/beta/features/Navbar/useRightSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { useMemo } from "react";

import TabButton from "@reearth/beta/components/TabButton";
import { useEditorNavigation } from "@reearth/beta/hooks/navigationHooks";
import { IconButton } from "@reearth/beta/lib/reearth-ui";
import { useT } from "@reearth/services/i18n";
import { styled } from "@reearth/services/theme";

import useDevPlugins from "./useDevPlugins";

import { Tab } from ".";

type Props = {
Expand All @@ -16,6 +19,8 @@ type Props = {
const useRightSide = ({ currentTab, page, sceneId }: Props) => {
const t = useT();
const handleEditorNavigation = useEditorNavigation({ sceneId });
const { devPluginExtensions, handleDevPluginsInstall, handleDevPluginExtensionsReload } =
useDevPlugins({ sceneId });

const rightSide = useMemo(() => {
if (page === "editor") {
Expand All @@ -41,12 +46,34 @@ const useRightSide = ({ currentTab, page, sceneId }: Props) => {
selected={currentTab === "publish"}
label={t("Publish")}
/>
{!!devPluginExtensions && (
<IconButton
icon="pluginInstall"
appearance="simple"
onClick={handleDevPluginsInstall}
/>
)}
{!!devPluginExtensions && (
<IconButton
icon="pluginUpdate"
appearance="simple"
onClick={handleDevPluginExtensionsReload}
/>
)}
</RightSection>
);
} else {
return null;
}
}, [currentTab, handleEditorNavigation, page, t]);
}, [
currentTab,
handleEditorNavigation,
page,
t,
devPluginExtensions,
handleDevPluginsInstall,
handleDevPluginExtensionsReload,
]);

return {
rightSide,
Expand All @@ -56,6 +83,7 @@ const useRightSide = ({ currentTab, page, sceneId }: Props) => {
const RightSection = styled.div`
display: flex;
align-items: flex-end;
justify-content: center;
gap: 4px;
`;

Expand Down
27 changes: 23 additions & 4 deletions web/src/beta/features/Visualizer/Crust/Plugins/Plugin/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type Events,
useGet,
} from "@reearth/core";
import { useDevPluginExtensionRenderKey, useDevPluginExtensions } from "@reearth/services/state";

import type { InfoboxBlock as Block } from "../../Infobox/types";
import type { MapRef } from "../../types";
Expand Down Expand Up @@ -155,10 +156,27 @@ export default function ({
[pluginId, extensionId],
);

const src =
pluginId && extensionId
? `${pluginBaseUrl}/${`${pluginId}/${extensionId}`.replace(/\.\./g, "")}.js`
: undefined;
const [devPluginExtensions] = useDevPluginExtensions();

const devPluginExtensionSrc = useMemo(() => {
if (!devPluginExtensions) return;
return devPluginExtensions.find(e => e.id === extensionId)?.url;
}, [devPluginExtensions, extensionId]);

const src = useMemo(
() =>
pluginId && extensionId
? devPluginExtensionSrc ??
`${pluginBaseUrl}/${`${pluginId}/${extensionId}`.replace(/\.\./g, "")}.js`
: undefined,
[devPluginExtensionSrc, pluginBaseUrl, pluginId, extensionId],
);
const [devPluginExtensionRenderKey] = useDevPluginExtensionRenderKey();

const renderKey = useMemo(
() => (devPluginExtensionSrc ? devPluginExtensionRenderKey : undefined),
[devPluginExtensionRenderKey, devPluginExtensionSrc],
);

return {
skip: !staticExposed,
Expand All @@ -168,6 +186,7 @@ export default function ({
modalVisible,
popupVisible,
externalRef,
renderKey,
exposed: staticExposed,
onError,
onPreInit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export default function Plugin({
modalVisible,
popupVisible,
externalRef,
renderKey,
onPreInit,
onDispose,
exposed,
Expand Down Expand Up @@ -124,6 +125,7 @@ export default function Plugin({
<P
className={className}
src={src}
key={renderKey}
sourceCode={sourceCode}
autoResize={autoResize}
iFrameProps={iFrameProps}
Expand Down
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.
4 changes: 4 additions & 0 deletions web/src/beta/lib/reearth-ui/components/Icon/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ import PencilFilled from "./Icons/PencilFilled.svg?react";
import PencilLilne from "./Icons/PencilLine.svg?react";
import PencilSimple from "./Icons/PencilSimple.svg?react";
import Play from "./Icons/Play.svg?react";
import PluginInstall from "./Icons/PluginInstall.svg?react";
import PluginUpdate from "./Icons/PluginUpdate.svg?react";
import Plus from "./Icons/Plus.svg?react";
import Points from "./Icons/Points.svg?react";
import Polygon from "./Icons/Polygon.svg?react";
Expand Down Expand Up @@ -213,6 +215,8 @@ export default {
pencilSimple: PencilSimple,
play: Play,
plus: Plus,
pluginInstall: PluginInstall,
pluginUpdate: PluginUpdate,
points: Points,
polygon: Polygon,
polygone: Polygone,
Expand Down
39 changes: 39 additions & 0 deletions web/src/beta/utils/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import JSZip from "jszip";

export async function fetchFile(url: string) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
}
return response.text();
}

export async function fetchAndZipFiles(urls: string[], zipFileName: string) {
const zip = new JSZip();

for (const url of urls) {
try {
const content = await fetchFile(url);
const fileName = url.split("/").pop();

if (!fileName) return;

zip.file(fileName, content);
} catch (error) {
console.error(`Failed to fetch ${url}:`, error);
}
}

let file;

await zip
.generateAsync({ type: "blob" })
.then(blob => {
file = new File([blob], zipFileName);
})
.catch(error => {
console.error("Error generating ZIP file:", error);
});

return file;
}
24 changes: 24 additions & 0 deletions web/src/services/api/pluginsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,29 @@ export default () => {
[uploadPluginMutation, t, setNotification],
);

const useUploadPluginWithFile = useCallback(
async (sceneId: string, file?: File) => {
if (!sceneId || !file) return;

const { errors } = await uploadPluginMutation({
variables: { sceneId: sceneId, file },
});

if (errors) {
setNotification({
type: "error",
text: t("Failed to install plugin."),
});
} else {
setNotification({
type: "success",
text: t("Successfully installed plugin!"),
});
}
},
[uploadPluginMutation, t, setNotification],
);

const [uninstallPluginMutation] = useMutation(UNINSTALL_PLUGIN, {
refetchQueries: ["GetScene"],
});
Expand Down Expand Up @@ -219,6 +242,7 @@ export default () => {
useInstallPlugin,
useUpgradePlugin,
useUploadPlugin,
useUploadPluginWithFile,
useUninstallPlugin,
};
};
1 change: 1 addition & 0 deletions web/src/services/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export type Config = {
extensions?: Extensions;
unsafeBuiltinPlugins?: UnsafeBuiltinPlugin[];
multiTenant?: Record<string, AuthInfo>;
devPluginUrls?: string[];
} & AuthInfo;

declare global {
Expand Down
7 changes: 7 additions & 0 deletions web/src/services/state/devPlugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { atom, useAtom } from "jotai";

const devPluginExtensionRenderKey = atom<number>(0);
export const useDevPluginExtensionRenderKey = () => useAtom(devPluginExtensionRenderKey);

const devPluginExtensions = atom<{ id: string; url: string }[] | undefined>(undefined);
export const useDevPluginExtensions = () => useAtom(devPluginExtensions);
2 changes: 2 additions & 0 deletions web/src/services/state/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { atom, useAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";

export * from "./devPlugins";

export { default as useSetError, useError } from "./gqlErrorHandling";

export type WidgetAreaState = {
Expand Down

0 comments on commit 2ccc95a

Please sign in to comment.