diff --git a/packages/app/src/components/WidgetsWrapper/index.jsx b/packages/app/src/components/WidgetsWrapper/index.jsx
new file mode 100644
index 00000000..866b0159
--- /dev/null
+++ b/packages/app/src/components/WidgetsWrapper/index.jsx
@@ -0,0 +1,197 @@
+import React from "react"
+import lodable from "@loadable/component"
+import * as antd from "antd"
+
+import { SortableList, SortableItem, DragHandle } from "components/SortableList"
+
+import "./index.less"
+
+class WidgetComponent extends React.Component {
+ state = {
+ mountedCssFiles: [],
+ loading: true,
+ }
+
+ componentDidMount = async () => {
+ if (Array.isArray(this.props.manifest.cssFiles)) {
+ for await (const cssFile of this.props.manifest.cssFiles) {
+ const cssFileElement = document.createElement("link")
+
+ cssFileElement.rel = "stylesheet"
+ cssFileElement.href = cssFile
+
+ document.head.appendChild(cssFileElement)
+
+ await this.setState({
+ mountedCssFiles: [
+ ...this.state.mountedCssFiles,
+ cssFileElement,
+ ]
+ })
+
+ continue
+ }
+ }
+
+ this.setState({
+ loading: false,
+ })
+ }
+
+ componentWillUnmount() {
+ this.state.mountedCssFiles.forEach((cssFileElement) => {
+ cssFileElement.remove()
+ })
+ }
+
+ // catch if render error
+ componentDidCatch = (error, errorInfo) => {
+ console.error(error, errorInfo)
+
+ this.setState({
+ loading: false,
+ renderError: error,
+ })
+ }
+
+ render() {
+ const { RenderComponent, manifest } = this.props
+
+ const RenderComponentCTX = {
+
+ }
+
+ if (this.state.renderError) {
+ return
+ }
+
+ if (this.state.loading) {
+ return
+ }
+
+ try {
+ if (!manifest) {
+ throw new Error("Widget has no manifest")
+ }
+
+ if (!RenderComponent) {
+ throw new Error("Widget has not valid render")
+ }
+
+ return
+
+
+ } catch (error) {
+ console.error(error)
+
+ return
+ Invalid widget
+
+ }
+ }
+}
+
+const generateRemoteComponent = (props) => {
+ return lodable(async () => {
+ try {
+ let virtualModule = await import(props.url)
+
+ virtualModule = virtualModule.default
+
+ if (!virtualModule) {
+ throw new Error("Widget has not valid module")
+ }
+
+ let RenderComponent = virtualModule.renderComponent
+
+ if (!RenderComponent) {
+ throw new Error("Widget has not valid render")
+ }
+
+ console.log(`Generate widget ${virtualModule.manifest.id}`)
+
+ return () =>
+ } catch (error) {
+ console.error(error)
+
+ return () =>
+ Error loading widget
+
+ }
+ }, {
+ fallback:
+ })
+}
+
+export default class WidgetsWrapper extends React.Component {
+ state = {
+ widgetsRender: app.cores.settings.get("widgets.urls").map((url, index) => {
+ return {
+ id: `${url}_${index}`,
+ url,
+ RenderItem: generateRemoteComponent({
+ url,
+ index: index,
+ })
+ }
+ }),
+ }
+
+ handleOnSortEnd = (widgetsRender) => {
+ this.setState({
+ widgetsRender
+ })
+
+ const urls = widgetsRender.map((widgetRender) => {
+ return widgetRender.url
+ })
+
+ app.cores.settings.set("widgets.urls", urls)
+ }
+
+ render() {
+ return
+ {
+ const RenderItem = item.RenderItem
+
+ return
+
+
+ }}
+ useDragOverlay
+ activeDragActions={[
+ {
+ id: "add",
+ icon: "Plus",
+ disabled: true,
+ onClick: () => {
+ // TODO: Open widget browser
+ }
+ },
+ ]}
+ />
+
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/components/WidgetsWrapper/index.less b/packages/app/src/components/WidgetsWrapper/index.less
new file mode 100644
index 00000000..0d7be4b6
--- /dev/null
+++ b/packages/app/src/components/WidgetsWrapper/index.less
@@ -0,0 +1,26 @@
+.widgets_wrapper {
+ gap: 20px;
+
+ width: 20vw;
+
+ .widgets_wrapper_list {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ }
+
+ .widget_item {
+ display: flex;
+ flex-direction: column;
+
+ border: 1px var(--border-color) solid;
+
+ border-radius: 12px;
+
+ padding: 10px;
+
+ width: 100%;
+
+ overflow: hidden;
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/pages/index.jsx b/packages/app/src/pages/index.jsx
index f9cf1dfc..34902439 100755
--- a/packages/app/src/pages/index.jsx
+++ b/packages/app/src/pages/index.jsx
@@ -2,6 +2,7 @@ import React from "react"
import * as antd from "antd"
import { Translation } from "react-i18next"
+import WidgetsWrapper from "components/WidgetsWrapper"
import { PagePanelWithNavMenu } from "components/PagePanels"
import { Icons } from "components/Icons"
@@ -50,13 +51,16 @@ export default class Home extends React.Component {
-
+
>
}
return
+ ]}
extraPanel={extraPanel}
primaryPanelClassName="full"
useSetQueryType