Skip to content

Commit

Permalink
refactor: support for dev plugin urls
Browse files Browse the repository at this point in the history
  • Loading branch information
airslice committed Jul 7, 2024
1 parent 1548e2d commit 4215b92
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 8 deletions.
3 changes: 2 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"@monaco-editor/react": "4.6.0",
"@popperjs/core": "2.11.8",
"@reearth/cesium-mvt-imagery-provider": "1.5.4",
"js-yaml": "4.1.0",
"@reearth/core": "0.0.4",
"@rot1024/use-transition": "1.0.0",
"@sentry/browser": "7.77.0",
Expand Down Expand Up @@ -201,4 +202,4 @@
"uuid": "9.0.1",
"xstate": "4.38.2"
}
}
}
26 changes: 24 additions & 2 deletions web/src/classic/components/molecules/EarthEditor/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,22 @@ export type Props = {
currentProject?: Project;
currentProjectStatus?: Status;
workspaceId?: string;
devPluginExtensions?: { id: string; url: string }[];
onPublishmentStatusClick?: (p: publishingType) => void;
onPreviewOpen?: () => void;
onDevPluginExtensionsReload?: () => void;
onDevPluginsInstall?: () => void;
} & CommonHeaderProps;

const Header: React.FC<Props> = ({
currentProject,
currentProjectStatus,
workspaceId,
devPluginExtensions,
onPublishmentStatusClick,
onPreviewOpen,
onDevPluginExtensionsReload,
onDevPluginsInstall,
...props
}) => {
const t = useT();
Expand All @@ -58,7 +64,23 @@ const Header: React.FC<Props> = ({

const right = (
<RightArea justify="flex-end" align="center">
<PreviewButton
{!!devPluginExtensions && (
<HeaderButton
text={t("Install Dev Plugins")}
buttonType="secondary"
onClick={onDevPluginsInstall}
margin="0 12px 0 0"
/>
)}
{!!devPluginExtensions && (
<HeaderButton
text={t("Reload Dev Plugin Extensions")}
buttonType="secondary"
onClick={onDevPluginExtensionsReload}
margin="0 12px 0 0"
/>
)}
<HeaderButton
text={t("Preview")}
buttonType="secondary"
onClick={onPreviewOpen}
Expand Down Expand Up @@ -110,7 +132,7 @@ const Header: React.FC<Props> = ({
return <CommonHeader {...props} center={center} right={right} />;
};

const PreviewButton = styled(Button)`
const HeaderButton = styled(Button)`
white-space: nowrap;
`;

Expand Down
27 changes: 23 additions & 4 deletions web/src/classic/components/molecules/Visualizer/Plugin/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { RefObject } from "react";
import type { API as IFrameAPI } from "@reearth/classic/components/atoms/Plugin";
import { defaultIsMarshalable } from "@reearth/classic/components/atoms/Plugin";
import events, { EventEmitter, Events, mergeEvents } from "@reearth/classic/util/event";
import { useDevPluginExtensionRenderKey, useDevPluginExtensions } from "@reearth/services/state";

import { useGet } from "../utils";

Expand Down Expand Up @@ -127,10 +128,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 @@ -140,6 +158,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 @@ -90,6 +90,7 @@ export default function Plugin({
modalVisible,
popupVisible,
externalRef,
renderKey,
onPreInit,
onDispose,
exposed,
Expand Down Expand Up @@ -126,6 +127,7 @@ export default function Plugin({
popupContainer={pluginPopupContainer}
externalRef={externalRef}
isMarshalable={isMarshalable}
key={renderKey}
exposed={exposed}
onError={onError}
onPreInit={onPreInit}
Expand Down
159 changes: 158 additions & 1 deletion web/src/classic/components/organisms/EarthEditor/Header/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { useApolloClient } from "@apollo/client";
import * as yaml from "js-yaml";
import JSZip from "jszip";
import { useState, useCallback, useEffect, useMemo } from "react";
import { useNavigate } from "react-router-dom";

Expand All @@ -11,10 +14,19 @@ import {
usePublishProjectMutation,
useCheckProjectAliasLazyQuery,
useCreateTeamMutation,
useUploadPluginMutation,
} from "@reearth/classic/gql";
import { useAuth } from "@reearth/services/auth";
import { config } from "@reearth/services/config";
import { useT } from "@reearth/services/i18n";
import { useSceneId, useWorkspace, useProject, useNotification } from "@reearth/services/state";
import {
useSceneId,
useWorkspace,
useProject,
useNotification,
useDevPluginExtensionRenderKey,
useDevPluginExtensions,
} from "@reearth/services/state";

export default () => {
const url = window.REEARTH_CONFIG?.published?.split("{}");
Expand Down Expand Up @@ -205,6 +217,79 @@ export default () => {
});
}, [t, setNotification]);

const [devPluginExtensions, setDevPluginExtensions] = useDevPluginExtensions();

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]);

const [_, updateDevPluginExtensionRenderKey] = useDevPluginExtensionRenderKey();

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

const client = useApolloClient();
const [uploadPluginMutation] = useUploadPluginMutation();

const handleInstallPluginFromFile = useCallback(
async (file: File) => {
if (!sceneId) return;
const results = await Promise.all(
Array.from([file]).map(f =>
uploadPluginMutation({
variables: { sceneId: sceneId, file: f },
}),
),
);
if (!results || results.some(r => r.errors)) {
await client.resetStore();
setNotification({
type: "error",
text: t("Failed to install plugin."),
});
} else {
setNotification({
type: "success",
text: t("Successfully installed plugin!"),
});
client.resetStore();
}
},
[sceneId, uploadPluginMutation, client, setNotification, t],
);

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]);

return {
workspaces,
workspaceId,
Expand All @@ -222,6 +307,7 @@ export default () => {
validAlias,
validatingAlias,
url,
devPluginExtensions,
handlePublicationModalOpen,
handlePublicationModalClose,
handleWorkspaceModalOpen,
Expand All @@ -234,6 +320,8 @@ export default () => {
handleCopyToClipBoard,
handlePreviewOpen,
handleLogout,
handleDevPluginExtensionsReload,
handleDevPluginsInstall,
};
};

Expand All @@ -248,3 +336,72 @@ const convertStatus = (status?: PublishmentStatus): Status | undefined => {
}
return undefined;
};

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

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();
}

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;
}

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const Header: React.FC<Props> = ({ className }) => {
validAlias,
validatingAlias,
url,
devPluginExtensions,
handlePublicationModalOpen,
handlePublicationModalClose,
handleWorkspaceModalOpen,
Expand All @@ -42,6 +43,8 @@ const Header: React.FC<Props> = ({ className }) => {
handleCopyToClipBoard,
handlePreviewOpen,
handleLogout,
handleDevPluginExtensionsReload,
handleDevPluginsInstall,
} = useHooks();

return (
Expand All @@ -55,13 +58,16 @@ const Header: React.FC<Props> = ({ className }) => {
workspaceId={workspaceId}
currentWorkspace={currentWorkspace}
modalShown={workspaceModalVisible}
devPluginExtensions={devPluginExtensions}
onPublishmentStatusClick={handlePublicationModalOpen}
onSignOut={handleLogout}
onWorkspaceCreate={handleTeamCreate}
onWorkspaceChange={handleTeamChange}
onPreviewOpen={handlePreviewOpen}
openModal={handleWorkspaceModalOpen}
onModalClose={handleWorkspaceModalClose}
onDevPluginExtensionsReload={handleDevPluginExtensionsReload}
onDevPluginsInstall={handleDevPluginsInstall}
/>
<PublicationModal
className={className}
Expand Down
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
6 changes: 6 additions & 0 deletions web/src/services/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,9 @@ const unselectProject = atom(null, (_get, set) => {
set(sceneMode, "3d");
});
export const useUnselectProject = () => useAtom(unselectProject)[1];

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

const devPluginExtensions = atom<{ id: string; url: string }[] | undefined>(undefined);
export const useDevPluginExtensions = () => useAtom(devPluginExtensions);

0 comments on commit 4215b92

Please sign in to comment.