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

feat(web): plugin playground/reflect code editor to viewer #1224

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3c377d0
display plugin list
devshun Oct 22, 2024
362287b
display code input
devshun Oct 23, 2024
642c6c8
feat file upload todo refactor
devshun Oct 25, 2024
bd6cc93
refactor(web): update Code component and FileListItem component
devshun Oct 27, 2024
ae44b45
feat upload file & refrect editor
devshun Oct 31, 2024
93718e9
fix plugin download
devshun Oct 31, 2024
a2d2b01
fixup! fix plugin download
devshun Nov 4, 2024
02ce6eb
Refactor validateFileTitle function in Plugins/utils.ts
devshun Nov 4, 2024
50b7be2
feat reflect viewer
devshun Nov 5, 2024
f62b092
Revert "Refactor validateFileTitle function in Plugins/utils.ts"
devshun Nov 5, 2024
f395ee2
Merge branch 'main' of https://github.com/reearth/reearth-visualizer …
devshun Nov 5, 2024
ac0bab4
fixup! Merge branch 'main' of https://github.com/reearth/reearth-visu…
devshun Nov 5, 2024
00ddfa7
Refactor ALLOWED_FILE_EXTENSIONS in PluginPlayground/Plugins/constant…
devshun Nov 5, 2024
a389041
Refactor PluginListWrapper padding in PluginPlayground/Plugins/index.tsx
devshun Nov 5, 2024
ee060e0
Refactor PluginPlayground/Plugins/hook.ts
devshun Nov 5, 2024
fb316e2
Merge branch 'playground/display-code-editor' of https://github.com/r…
devshun Nov 5, 2024
f29eed2
fix type error
devshun Nov 5, 2024
e97cdb0
use uuid
devshun Nov 7, 2024
ff7229a
initialize selected file when file delete
devshun Nov 7, 2024
e2b6480
add error handling download
devshun Nov 7, 2024
4ceecdd
fix prop name
devshun Nov 7, 2024
6270b1a
add fb for error message
devshun Nov 7, 2024
e7d4c3d
Merge branch 'playground/display-code-editor' of https://github.com/r…
devshun Nov 7, 2024
b6a5505
fixup! Merge branch 'playground/display-code-editor' of https://githu…
devshun Nov 7, 2024
e1f81d5
Merge branch 'main' into playground/display-code-editor
airslice Nov 8, 2024
90f337c
refactor(web): streamline error handling in file title validation
devshun Nov 9, 2024
df4ca9d
Merge branch 'main' of https://github.com/reearth/reearth-visualizer …
devshun Nov 9, 2024
1dd6c93
Merge branch 'playground/display-code-editor' of https://github.com/r…
devshun Nov 9, 2024
1bec19e
Provide feedback to the user if fetching the YAML JSON fails.
devshun Nov 9, 2024
9bc0153
fixup! refactor(web): streamline error handling in file title validation
devshun Nov 9, 2024
3f0d214
Merge branch 'playground/display-code-editor' of https://github.com/r…
devshun Nov 9, 2024
031c2ab
update demo widget details in constants and hook
devshun Nov 9, 2024
e15c4d8
refactor: simplify getYmlJson function and improve error handling
devshun Nov 9, 2024
15c1a61
tmp file out put
devshun Nov 13, 2024
6e10a0e
fix typo
devshun Nov 13, 2024
cceb0bc
Merge branch 'main' of https://github.com/reearth/reearth-visualizer …
devshun Nov 13, 2024
13a74cc
fixup! fix typo
devshun Nov 13, 2024
f940da6
Merge branch 'main' of https://github.com/reearth/reearth-visualizer …
devshun Nov 13, 2024
b7b7915
fix lint error
devshun Nov 16, 2024
3b75842
add error not found extensions
devshun Nov 16, 2024
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
125 changes: 125 additions & 0 deletions web/src/beta/features/PluginPlayground/Code/hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import Visualizer from "@reearth/beta/features/Visualizer";
import { useNotification } from "@reearth/services/state";
import * as yaml from "js-yaml";
import { ComponentProps, useCallback, useState } from "react";

import { WidgetLocation } from "../../Visualizer/Crust/Widgets/types";
import { FileType } from "../Plugins/constants";

type Widgets = ComponentProps<typeof Visualizer>["widgets"];

type ReearthYML = {
id: string;
name: string;
version: string;
extensions?: {
id: string;
type: string;
name: string;
description: string;
widgetLayout?: {
defaultLocation: {
zone: WidgetLocation["zone"];
section: WidgetLocation["section"];
area: WidgetLocation["area"];
};
};
}[];
};

type Props = {
files: FileType[];
};

const getYmlJson = (file: FileType) => {
if (file.sourceCode === "") {
return { success: false, message: "YAML file is empty" } as const;
}
airslice marked this conversation as resolved.
Show resolved Hide resolved

try {
const data = yaml.load(file.sourceCode) as ReearthYML;
return { success: true, data } as const;
} catch (error) {
const message =
error instanceof yaml.YAMLException
? error.message
: "Failed to parse YAML";
return { success: false, message } as const;
}
};

export default ({ files }: Props) => {
const [widgets, setWidgets] = useState<Widgets>();
const [, setNotification] = useNotification();

const executeCode = useCallback(() => {
const ymlFile = files.find((file) => file.title.endsWith(".yml"));

if (!ymlFile) return;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error notification when no YAML file is found

Currently, if no YAML file is found in the provided files, the function returns silently without notifying the user. It would be helpful to inform the user that no YAML file was found to provide better feedback.

Apply this diff to add an error notification:

 if (!ymlFile) {
+  setNotification({ type: "error", text: "No YAML file found." });
   return;
 }

Committable suggestion skipped: line range outside the PR's diff.


const getYmlResult = getYmlJson(ymlFile);

if (!getYmlResult.success) {
setNotification({ type: "error", text: getYmlResult.message });
return;
}

const ymlJson = getYmlResult.data;

if (!ymlJson.extensions) return;
Copy link

@coderabbitai coderabbitai bot Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ensure that ymlJson.extensions is a non-empty array before proceeding.

Before using ymlJson.extensions, it's good practice to verify that it's a non-empty array to prevent runtime errors.

Apply this diff to add the check:

-if (!ymlJson.extensions) return;
+if (!Array.isArray(ymlJson.extensions) || ymlJson.extensions.length === 0) {
+  setNotification({ type: "error", text: "No extensions found in YAML file." });
+  return;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!ymlJson.extensions) return;
if (!Array.isArray(ymlJson.extensions) || ymlJson.extensions.length === 0) {
setNotification({ type: "error", text: "No extensions found in YAML file." });
return;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add this just to double-check

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mulengawilfred, sounds good! Adding this check will help ensure that ymlJson.extensions is a non-empty array before proceeding, which will prevent potential runtime errors.


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!


const widgets = ymlJson.extensions.reduce<NonNullable<Widgets>>(
(prv, cur) => {
const file = files.find((file) => file.title === `${cur.id}.js`);

if (!file) {
return prv;
}

const { zone, section, area } = cur.widgetLayout?.defaultLocation ?? {
zone: "outer",
section: "left",
area: "top"
};

const zoneAlignSystem = prv?.alignSystem?.[zone] ?? {};
const sectionAlignSystem = zoneAlignSystem[section] ?? {};
const areaAlignSystem = sectionAlignSystem[area] ?? { widgets: [] };

return {
...prv,
alignSystem: {
...prv?.alignSystem,
[zone]: {
...zoneAlignSystem,
[section]: {
...sectionAlignSystem,
[area]: {
...areaAlignSystem,
widgets: [
...(areaAlignSystem.widgets ?? []),
{
id: cur.id,
__REEARTH_SOURCECODE: file.sourceCode
}
]
}
}
}
}
};
},
{
alignSystem: {},
floating: [],
ownBuiltinWidgets: []
}
);
Comment on lines +77 to +123
Copy link

@coderabbitai coderabbitai bot Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid using spread syntax on accumulators within reduce to enhance performance.

Using the spread operator (...) on the accumulator inside a reduce function can lead to O(n²) time complexity, negatively affecting performance.

Refactor the code to mutate the accumulator directly:

-const widgets = ymlJson.extensions.reduce<NonNullable<Widgets>>(
-  (prv, cur) => {
-    // ...spreading accumulator
-    return {
-      ...prv,
-      // nested spreading
-    };
-  },
-  {
-    alignSystem: {},
-    floating: [],
-    ownBuiltinWidgets: [],
-  }
-);

+const widgets: NonNullable<Widgets> = {
+  alignSystem: {},
+  floating: [],
+  ownBuiltinWidgets: [],
+};
+
+ymlJson.extensions.forEach((cur) => {
+  const file = files.find((file) => file.title === `${cur.id}.js`);
+  if (!file) {
+    return;
+  }
+  const { zone, section, area } = cur.widgetLayout?.defaultLocation ?? {
+    zone: "outer",
+    section: "left",
+    area: "top",
+  };
+  const areaWidgets = widgets.alignSystem[zone]?.[section]?.[area]?.widgets ?? [];
+  areaWidgets.push({
+    id: cur.id,
+    __REEARTH_SOURCECODE: file.sourceCode,
+  });
+  widgets.alignSystem[zone] = widgets.alignSystem[zone] || {};
+  widgets.alignSystem[zone][section] = widgets.alignSystem[zone][section] || {};
+  widgets.alignSystem[zone][section][area] = {
+    widgets: areaWidgets,
+  };
+});

This approach avoids unnecessary object spreads and enhances performance.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const widgets = ymlJson.extensions.reduce<NonNullable<Widgets>>(
(prv, cur) => {
const file = files.find((file) => file.title === `${cur.id}.js`);
if (!file) {
return prv;
}
const { zone, section, area } = cur.widgetLayout?.defaultLocation ?? {
zone: "outer",
section: "left",
area: "top"
};
const zoneAlignSystem = prv?.alignSystem?.[zone] ?? {};
const sectionAlignSystem = zoneAlignSystem[section] ?? {};
const areaAlignSystem = sectionAlignSystem[area] ?? { widgets: [] };
return {
...prv,
alignSystem: {
...prv?.alignSystem,
[zone]: {
...zoneAlignSystem,
[section]: {
...sectionAlignSystem,
[area]: {
...areaAlignSystem,
widgets: [
...(areaAlignSystem.widgets ?? []),
{
id: cur.id,
__REEARTH_SOURCECODE: file.sourceCode
}
]
}
}
}
}
};
},
{
alignSystem: {},
floating: [],
ownBuiltinWidgets: []
}
);
const widgets: NonNullable<Widgets> = {
alignSystem: {},
floating: [],
ownBuiltinWidgets: [],
};
ymlJson.extensions.forEach((cur) => {
const file = files.find((file) => file.title === `${cur.id}.js`);
if (!file) {
return;
}
const { zone, section, area } = cur.widgetLayout?.defaultLocation ?? {
zone: "outer",
section: "left",
area: "top",
};
const areaWidgets = widgets.alignSystem[zone]?.[section]?.[area]?.widgets ?? [];
areaWidgets.push({
id: cur.id,
__REEARTH_SOURCECODE: file.sourceCode,
});
widgets.alignSystem[zone] = widgets.alignSystem[zone] || {};
widgets.alignSystem[zone][section] = widgets.alignSystem[zone][section] || {};
widgets.alignSystem[zone][section][area] = {
widgets: areaWidgets,
};
});
🧰 Tools
🪛 Biome

[error] 108-108: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth considering this change as well

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mulengawilfred, glad you find the suggestion helpful. Let me know if you have any questions or need further assistance.

setWidgets(widgets);
}, [files, setNotification]);

return {
executeCode,
widgets
};
};
59 changes: 59 additions & 0 deletions web/src/beta/features/PluginPlayground/Code/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Button, CodeInput } from "@reearth/beta/lib/reearth-ui";
import { styled } from "@reearth/services/theme";
import { FC } from "react";

type Props = {
fileTitle: string;
sourceCode: string;
onChangeSourceCode: (value: string | undefined) => void;
executeCode: () => void;
};

const getLanguageByFileExtension = (fileTitle: string) => {
const ext = fileTitle.split(".").pop();
switch (ext) {
case "js":
return "javascript";
case "yml":
case "yaml":
return "yaml";
default:
return "plaintext";
}
};

const Code: FC<Props> = ({
fileTitle,
sourceCode,
onChangeSourceCode,
executeCode
}) => {
return (
<Wrapper>
<Header>
<Button icon="playRight" iconButton onClick={executeCode} />
<p>Widget</p>
</Header>
<CodeInput
language={getLanguageByFileExtension(fileTitle)}
value={sourceCode}
onChange={onChangeSourceCode}
/>
</Wrapper>
);
};

const Header = styled("div")(() => ({
display: "flex",
alignItems: "center",
justifyContent: "space-between"
}));

const Wrapper = styled("div")(({ theme }) => ({
display: "flex",
flexDirection: "column",
gap: theme.spacing.small,
height: "100%"
}));

export default Code;
17 changes: 15 additions & 2 deletions web/src/beta/features/PluginPlayground/PluginInspector/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { Button } from "@reearth/beta/lib/reearth-ui";
import { FC } from "react";

const PluginInspector: FC = () => {
return <div>Plugin inspector content</div>;
type Props = {
handlePluginDownload: () => void;
};

const PluginInspector: FC<Props> = ({ handlePluginDownload }) => {
return (
<div>
<Button
icon="install"
title="Download Package"
onClick={handlePluginDownload}
/>
</div>
);
};

export default PluginInspector;
119 changes: 119 additions & 0 deletions web/src/beta/features/PluginPlayground/Plugins/FileListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { TextInput } from "@reearth/beta/lib/reearth-ui";
import { EntryItem } from "@reearth/beta/ui/components";
import { styled } from "@reearth/services/theme";
import { FC, useCallback, useMemo, useState } from "react";

import { FileType } from "./constants";
import usePlugins from "./hook";

type UsePluginsReturn = ReturnType<typeof usePlugins>;

type Props = {
file: FileType;
selected: boolean;
onClick?: () => void;
isEditing?: boolean;
confirmFileTitle: (value: string, id: string) => void;
deleteFile?: UsePluginsReturn["deleteFile"];
};

const FileListItem: FC<Props> = ({
file,
selected,
onClick,
confirmFileTitle,
deleteFile,
isEditing: isEditingProp
}) => {
const [isEditing, setIsEditing] = useState(isEditingProp);

const handleInputConfirm = useCallback(
(value: string) => {
confirmFileTitle(value, file.id);
setIsEditing(false);
},
[confirmFileTitle, file.id]
);

const optionsMenu = useMemo(() => {
const menuItems = [
...(!file.disableDelete
? [
{
id: "delete",
title: "Delete",
icon: "trash" as const,
onClick: () => deleteFile?.(file.id)
}
]
: []),
...(!file.disableEdit
? [
{
id: "rename",
title: "Rename",
icon: "pencilSimple" as const,
onClick: () => setIsEditing(true)
}
]
: [])
];

return menuItems.length > 0 ? menuItems : undefined;
}, [deleteFile, file.id, file.disableDelete, file.disableEdit]);

return (
<Wrapper>
<EntryItem
icon="file"
title={
isEditing ? (
<TextInput
size="small"
extendWidth
autoFocus
value={file.title}
onBlur={handleInputConfirm}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleInputConfirm(e.currentTarget.value);
}
}}
/>
) : (
<TitleWrapper>{file.title}</TitleWrapper>
)
}
highlighted={selected}
optionsMenuWidth={100}
onClick={onClick}
optionsMenu={!isEditing ? optionsMenu : undefined}
/>
</Wrapper>
);
};

const Wrapper = styled("li")(({ theme }) => ({
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: theme.spacing.small,
padding: `${theme.spacing.smallest}px ${theme.spacing.small}px ${theme.spacing.smallest}px ${theme.spacing.normal}px`,
borderRadius: theme.radius.small,
cursor: "pointer",
"&:not(:first-child)": {
marginTop: theme.spacing.smallest
}
}));

const TitleWrapper = styled("div")(({ theme }) => ({
padding: `0 ${theme.spacing.smallest + 1}px`,
color: theme.content.main,
fontSize: theme.fonts.sizes.body,
fontWeight: theme.fonts.weight.regular,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}));

export default FileListItem;
36 changes: 36 additions & 0 deletions web/src/beta/features/PluginPlayground/Plugins/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export type PluginType = {
id: string;
title: string;
files: FileType[];
};

export type FileType = {
id: string;
title: string;
sourceCode: string;
disableEdit?: boolean;
disableDelete?: boolean;
};

export const REEARTH_YML_FILE = {
id: "reearth-yml",
title: "reearth.yml",
sourceCode: `id: demo-widget
name: Test plugin
version: 1.0.0
extensions:
- id: demo-widget
type: widget
name: Demo Widget
description: Demo widget
widgetLayout:
defaultLocation:
zone: outer
section: left
area: top
`,
disableEdit: true,
disableDelete: true
} as const satisfies FileType;

export const ALLOWED_FILE_EXTENSIONS = ["js"] as const;
Loading
Loading