From b89d021f2ad81033ae2d294d9291c2c6745635ae Mon Sep 17 00:00:00 2001 From: Ourai Lin Date: Sat, 19 Oct 2024 20:25:10 +0800 Subject: [PATCH 1/5] fix(ui): route plugins register failed --- ui/src/App.tsx | 29 ++++++++++++++++++--- ui/src/router/index.tsx | 14 ++++++---- ui/src/utils/pluginKit/index.ts | 46 ++++++++++++++++++++++++++++----- 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index affac7a92..1b7d1b82e 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -17,14 +17,37 @@ * under the License. */ -import { RouterProvider, createBrowserRouter } from 'react-router-dom'; +import { + type RouteObject, + RouterProvider, + createBrowserRouter, +} from 'react-router-dom'; +import { useState, useEffect } from 'react'; import './i18n/init'; -import '@/utils/pluginKit'; -import routes from '@/router'; +import { subscribe, unsubscribe } from '@/utils/pluginKit'; +import resolveRoutes from '@/router'; function App() { + const [routes, setRoutes] = useState([]); + + useEffect(() => { + const callback = () => { + setRoutes(resolveRoutes()); + }; + + subscribe('registered', callback); + + return () => { + unsubscribe('registered', callback); + }; + }, []); + + if (routes.length === 0) { + return
initializing
; + } + const router = createBrowserRouter(routes, { basename: process.env.REACT_APP_BASE_URL, }); diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index 432cb5816..8c111e0f0 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -27,8 +27,6 @@ import baseRoutes, { RouteNode } from './routes'; import RouteGuard from './RouteGuard'; import RouteErrorBoundary from './RouteErrorBoundary'; -const routes: RouteNode[] = []; - const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => { routeNodes.forEach((rn) => { if (rn.page === 'pages/Layout') { @@ -76,8 +74,14 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => { } }); }; -const mergedRoutes = mergeRoutePlugins(baseRoutes); -routeWrapper(mergedRoutes, routes); +function resolveRoutes(): RouteObject[] { + const routes: RouteNode[] = []; + const mergedRoutes = mergeRoutePlugins(baseRoutes); + + routeWrapper(mergedRoutes, routes); + + return routes as RouteObject[]; +} -export default routes as RouteObject[]; +export default resolveRoutes; diff --git a/ui/src/utils/pluginKit/index.ts b/ui/src/utils/pluginKit/index.ts index 66d5c3760..43b0d691c 100644 --- a/ui/src/utils/pluginKit/index.ts +++ b/ui/src/utils/pluginKit/index.ts @@ -42,15 +42,46 @@ import { Plugin, PluginInfo, PluginType } from './interface'; * @field description: Plugin description, optionally configurable. Usually read from the `i18n` file */ +type EventName = string; +type EventHandler = () => void; + class Plugins { plugins: Plugin[] = []; registeredPlugins: Type.ActivatedPlugin[] = []; + events: Record = {}; + constructor() { this.init(); } + on(name: EventName, handler: EventHandler) { + if (!this.events[name]) { + this.events[name] = []; + } + + this.events[name].push(handler); + } + + off(name: EventName, handler?: EventHandler) { + const handlers = this.events[name]; + + if (!handlers || handlers.length === 0) { + return; + } + + if (handler) { + this.events[name] = handlers.filter((func) => func !== handler); + } else { + delete this.events[name]; + } + } + + trigger(name: EventName) { + (this.events[name] || []).forEach((handler) => handler()); + } + init() { this.registerBuiltin(); @@ -101,12 +132,10 @@ class Plugins { return func; }) .filter((p) => p); - return new Promise((resolve) => { - plugins.forEach(async (p) => { - const plugin = await p(); - this.register(plugin); - }); - resolve(true); + return Promise.all(plugins.map((p) => p())).then((resolvedPlugins) => { + resolvedPlugins.forEach((plugin) => this.register(plugin)); + this.trigger('registered'); + return true; }); } @@ -150,6 +179,9 @@ class Plugins { const plugins = new Plugins(); +const subscribe = plugins.on.bind(plugins); +const unsubscribe = plugins.off.bind(plugins); + const getRoutePlugins = () => { return plugins .getPlugins() @@ -287,5 +319,7 @@ export { useCaptchaPlugin, useRenderPlugin, PluginType, + subscribe, + unsubscribe, }; export default plugins; From 89d7ca56fffc7bfcd37916e4b28af62c478aef7e Mon Sep 17 00:00:00 2001 From: Ourai Lin Date: Sun, 20 Oct 2024 11:40:47 +0800 Subject: [PATCH 2/5] refactor(ui): extract event logic of plugin kit out --- ui/src/utils/pluginKit/emitter.ts | 34 ++++++++++++++++++++++++++++ ui/src/utils/pluginKit/index.ts | 37 ++++--------------------------- 2 files changed, 38 insertions(+), 33 deletions(-) create mode 100644 ui/src/utils/pluginKit/emitter.ts diff --git a/ui/src/utils/pluginKit/emitter.ts b/ui/src/utils/pluginKit/emitter.ts new file mode 100644 index 000000000..eee427b61 --- /dev/null +++ b/ui/src/utils/pluginKit/emitter.ts @@ -0,0 +1,34 @@ +type EventName = string; +type EventHandler = () => void; + +class SimpleEventEmitter { + events: Record = {}; + + on(name: EventName, handler: EventHandler) { + if (!this.events[name]) { + this.events[name] = []; + } + + this.events[name].push(handler); + } + + off(name: EventName, handler?: EventHandler) { + const handlers = this.events[name]; + + if (!handlers || handlers.length === 0) { + return; + } + + if (handler) { + this.events[name] = handlers.filter((func) => func !== handler); + } else { + delete this.events[name]; + } + } + + emit(name: EventName) { + (this.events[name] || []).forEach((handler) => handler()); + } +} + +export default SimpleEventEmitter; diff --git a/ui/src/utils/pluginKit/index.ts b/ui/src/utils/pluginKit/index.ts index 43b0d691c..39d1edb5c 100644 --- a/ui/src/utils/pluginKit/index.ts +++ b/ui/src/utils/pluginKit/index.ts @@ -29,6 +29,7 @@ import request from '@/utils/request'; import { initI18nResource } from './utils'; import { Plugin, PluginInfo, PluginType } from './interface'; +import SimpleEventEmitter from './emitter'; /** * This information is to be defined for all components. @@ -42,46 +43,16 @@ import { Plugin, PluginInfo, PluginType } from './interface'; * @field description: Plugin description, optionally configurable. Usually read from the `i18n` file */ -type EventName = string; -type EventHandler = () => void; - -class Plugins { +class Plugins extends SimpleEventEmitter { plugins: Plugin[] = []; registeredPlugins: Type.ActivatedPlugin[] = []; - events: Record = {}; - constructor() { + super(); this.init(); } - on(name: EventName, handler: EventHandler) { - if (!this.events[name]) { - this.events[name] = []; - } - - this.events[name].push(handler); - } - - off(name: EventName, handler?: EventHandler) { - const handlers = this.events[name]; - - if (!handlers || handlers.length === 0) { - return; - } - - if (handler) { - this.events[name] = handlers.filter((func) => func !== handler); - } else { - delete this.events[name]; - } - } - - trigger(name: EventName) { - (this.events[name] || []).forEach((handler) => handler()); - } - init() { this.registerBuiltin(); @@ -134,7 +105,7 @@ class Plugins { .filter((p) => p); return Promise.all(plugins.map((p) => p())).then((resolvedPlugins) => { resolvedPlugins.forEach((plugin) => this.register(plugin)); - this.trigger('registered'); + this.emit('registered'); return true; }); } From 2c4f9563321a7adf8d4a7b1fe23f834ac4ef6491 Mon Sep 17 00:00:00 2001 From: Ourai Lin Date: Sun, 20 Oct 2024 12:13:23 +0800 Subject: [PATCH 3/5] refactor(ui): optimize initial loading --- ui/src/App.tsx | 11 +++++++++-- .../InitialLoadingPlaceholder/index.tsx | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 ui/src/components/InitialLoadingPlaceholder/index.tsx diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 1b7d1b82e..f799042a0 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -28,8 +28,9 @@ import './i18n/init'; import { subscribe, unsubscribe } from '@/utils/pluginKit'; import resolveRoutes from '@/router'; +import InitialLoadingPlaceholder from '@/components/InitialLoadingPlaceholder'; -function App() { +function useResolvedRoutes() { const [routes, setRoutes] = useState([]); useEffect(() => { @@ -44,8 +45,14 @@ function App() { }; }, []); + return routes; +} + +function App() { + const routes = useResolvedRoutes(); + if (routes.length === 0) { - return
initializing
; + return ; } const router = createBrowserRouter(routes, { diff --git a/ui/src/components/InitialLoadingPlaceholder/index.tsx b/ui/src/components/InitialLoadingPlaceholder/index.tsx new file mode 100644 index 000000000..28ba1cfd7 --- /dev/null +++ b/ui/src/components/InitialLoadingPlaceholder/index.tsx @@ -0,0 +1,18 @@ +import { Spinner } from 'react-bootstrap'; + +function InitialLoadingPlaceholder() { + return ( +
+ + Initializing +
+ ); +} + +export default InitialLoadingPlaceholder; From 91f9ace807fc429296cd4e5a27bbaf7b341b9d6a Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 21 Oct 2024 16:19:32 +0800 Subject: [PATCH 4/5] refactor(ui): optimize initial loading and extract event logic of plugin kit out --- ui/src/App.tsx | 33 +++--------------------- ui/src/router/index.tsx | 22 ++++++++++------ ui/src/utils/pluginKit/emitter.ts | 34 ------------------------- ui/src/utils/pluginKit/index.ts | 42 +++++++++---------------------- 4 files changed, 31 insertions(+), 100 deletions(-) delete mode 100644 ui/src/utils/pluginKit/emitter.ts diff --git a/ui/src/App.tsx b/ui/src/App.tsx index f799042a0..8c425f293 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -17,44 +17,19 @@ * under the License. */ -import { - type RouteObject, - RouterProvider, - createBrowserRouter, -} from 'react-router-dom'; -import { useState, useEffect } from 'react'; +import { RouterProvider, createBrowserRouter } from 'react-router-dom'; import './i18n/init'; -import { subscribe, unsubscribe } from '@/utils/pluginKit'; -import resolveRoutes from '@/router'; +import '@/utils/pluginKit'; +import { useMergeRoutes } from '@/router'; import InitialLoadingPlaceholder from '@/components/InitialLoadingPlaceholder'; -function useResolvedRoutes() { - const [routes, setRoutes] = useState([]); - - useEffect(() => { - const callback = () => { - setRoutes(resolveRoutes()); - }; - - subscribe('registered', callback); - - return () => { - unsubscribe('registered', callback); - }; - }, []); - - return routes; -} - function App() { - const routes = useResolvedRoutes(); - + const routes = useMergeRoutes(); if (routes.length === 0) { return ; } - const router = createBrowserRouter(routes, { basename: process.env.REACT_APP_BASE_URL, }); diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index 8c111e0f0..ea2fdf3c2 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { Suspense, lazy } from 'react'; +import { Suspense, lazy, useEffect, useState } from 'react'; import { RouteObject } from 'react-router-dom'; import Layout from '@/pages/Layout'; @@ -75,13 +75,21 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => { }); }; -function resolveRoutes(): RouteObject[] { - const routes: RouteNode[] = []; - const mergedRoutes = mergeRoutePlugins(baseRoutes); +function useMergeRoutes() { + const [routesState, setRoutes] = useState([]); - routeWrapper(mergedRoutes, routes); + const init = async () => { + const routes = []; + const mergedRoutes = await mergeRoutePlugins(baseRoutes).catch(() => []); + routeWrapper(mergedRoutes, routes); + setRoutes(routes); + }; - return routes as RouteObject[]; + useEffect(() => { + init(); + }, []); + + return routesState; } -export default resolveRoutes; +export { useMergeRoutes }; diff --git a/ui/src/utils/pluginKit/emitter.ts b/ui/src/utils/pluginKit/emitter.ts deleted file mode 100644 index eee427b61..000000000 --- a/ui/src/utils/pluginKit/emitter.ts +++ /dev/null @@ -1,34 +0,0 @@ -type EventName = string; -type EventHandler = () => void; - -class SimpleEventEmitter { - events: Record = {}; - - on(name: EventName, handler: EventHandler) { - if (!this.events[name]) { - this.events[name] = []; - } - - this.events[name].push(handler); - } - - off(name: EventName, handler?: EventHandler) { - const handlers = this.events[name]; - - if (!handlers || handlers.length === 0) { - return; - } - - if (handler) { - this.events[name] = handlers.filter((func) => func !== handler); - } else { - delete this.events[name]; - } - } - - emit(name: EventName) { - (this.events[name] || []).forEach((handler) => handler()); - } -} - -export default SimpleEventEmitter; diff --git a/ui/src/utils/pluginKit/index.ts b/ui/src/utils/pluginKit/index.ts index 39d1edb5c..346c1456a 100644 --- a/ui/src/utils/pluginKit/index.ts +++ b/ui/src/utils/pluginKit/index.ts @@ -29,7 +29,6 @@ import request from '@/utils/request'; import { initI18nResource } from './utils'; import { Plugin, PluginInfo, PluginType } from './interface'; -import SimpleEventEmitter from './emitter'; /** * This information is to be defined for all components. @@ -43,23 +42,23 @@ import SimpleEventEmitter from './emitter'; * @field description: Plugin description, optionally configurable. Usually read from the `i18n` file */ -class Plugins extends SimpleEventEmitter { +class Plugins { plugins: Plugin[] = []; registeredPlugins: Type.ActivatedPlugin[] = []; + initialization: Promise; + constructor() { - super(); - this.init(); + this.initialization = this.init(); } - init() { + async init() { this.registerBuiltin(); - getPluginsStatus().then((plugins) => { - this.registeredPlugins = plugins.filter((p) => p.enabled); - this.registerPlugins(); - }); + const plugins = await getPluginsStatus().catch(() => []); + this.registeredPlugins = plugins.filter((p) => p.enabled); + await this.registerPlugins(); } refresh() { @@ -105,7 +104,6 @@ class Plugins extends SimpleEventEmitter { .filter((p) => p); return Promise.all(plugins.map((p) => p())).then((resolvedPlugins) => { resolvedPlugins.forEach((plugin) => this.register(plugin)); - this.emit('registered'); return true; }); } @@ -122,18 +120,6 @@ class Plugins extends SimpleEventEmitter { this.plugins.push(plugin); } - activatePlugins(activatedPlugins: Type.ActivatedPlugin[]) { - this.plugins.forEach((plugin: any) => { - const { slug_name } = plugin.info; - const activatedPlugin: any = activatedPlugins?.find( - (p) => p.slug_name === slug_name, - ); - if (activatedPlugin) { - plugin.activated = activatedPlugin?.enabled; - } - }); - } - getPlugin(slug_name: string) { return this.plugins.find((p) => p.info.slug_name === slug_name); } @@ -150,10 +136,8 @@ class Plugins extends SimpleEventEmitter { const plugins = new Plugins(); -const subscribe = plugins.on.bind(plugins); -const unsubscribe = plugins.off.bind(plugins); - -const getRoutePlugins = () => { +const getRoutePlugins = async () => { + await plugins.initialization; return plugins .getPlugins() .filter((plugin) => plugin.info.type === PluginType.Route); @@ -183,8 +167,8 @@ const validateRoutePlugin = async (slugName) => { return Boolean(registeredPlugin?.enabled); }; -const mergeRoutePlugins = (routes) => { - const routePlugins = getRoutePlugins(); +const mergeRoutePlugins = async (routes) => { + const routePlugins = await getRoutePlugins(); if (routePlugins.length === 0) { return routes; } @@ -290,7 +274,5 @@ export { useCaptchaPlugin, useRenderPlugin, PluginType, - subscribe, - unsubscribe, }; export default plugins; From 672e14d1f177b3bf24670394ed7a8b3eec0e91f7 Mon Sep 17 00:00:00 2001 From: Ourai Lin Date: Tue, 22 Oct 2024 11:32:52 +0800 Subject: [PATCH 5/5] chore(ui): copy style from `public/index.html` to keep consistency --- .../InitialLoadingPlaceholder/index.scss | 35 +++++++++++++++++++ .../InitialLoadingPlaceholder/index.tsx | 17 ++++----- 2 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 ui/src/components/InitialLoadingPlaceholder/index.scss diff --git a/ui/src/components/InitialLoadingPlaceholder/index.scss b/ui/src/components/InitialLoadingPlaceholder/index.scss new file mode 100644 index 000000000..2b73f1636 --- /dev/null +++ b/ui/src/components/InitialLoadingPlaceholder/index.scss @@ -0,0 +1,35 @@ +// Same as spin in `public/index.html` + +@keyframes _initial-loading-spin { + to { transform: rotate(360deg) } +} + +.InitialLoadingPlaceholder { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + z-index: 9999; + + &-spinnerContainer { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + &-spinner { + box-sizing: border-box; + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: -.125em; + border: .25rem solid currentColor; + border-right-color: transparent; + color: rgba(108, 117, 125, .75); + border-radius: 50%; + animation: 0.75s linear infinite _initial-loading-spin; + } +} diff --git a/ui/src/components/InitialLoadingPlaceholder/index.tsx b/ui/src/components/InitialLoadingPlaceholder/index.tsx index 28ba1cfd7..41e81d1a4 100644 --- a/ui/src/components/InitialLoadingPlaceholder/index.tsx +++ b/ui/src/components/InitialLoadingPlaceholder/index.tsx @@ -1,16 +1,13 @@ -import { Spinner } from 'react-bootstrap'; +// Same as spin in `public/index.html` + +import './index.scss'; function InitialLoadingPlaceholder() { return ( -
- - Initializing +
+
+
+
); }