diff --git a/ui/src/App.tsx b/ui/src/App.tsx index affac7a92..8c425f293 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -22,9 +22,14 @@ import { RouterProvider, createBrowserRouter } from 'react-router-dom'; import './i18n/init'; import '@/utils/pluginKit'; -import routes from '@/router'; +import { useMergeRoutes } from '@/router'; +import InitialLoadingPlaceholder from '@/components/InitialLoadingPlaceholder'; function App() { + const routes = useMergeRoutes(); + if (routes.length === 0) { + return ; + } const router = createBrowserRouter(routes, { basename: process.env.REACT_APP_BASE_URL, }); 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 new file mode 100644 index 000000000..41e81d1a4 --- /dev/null +++ b/ui/src/components/InitialLoadingPlaceholder/index.tsx @@ -0,0 +1,15 @@ +// Same as spin in `public/index.html` + +import './index.scss'; + +function InitialLoadingPlaceholder() { + return ( +
+
+
+
+
+ ); +} + +export default InitialLoadingPlaceholder; diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index 432cb5816..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'; @@ -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,22 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => { } }); }; -const mergedRoutes = mergeRoutePlugins(baseRoutes); -routeWrapper(mergedRoutes, routes); +function useMergeRoutes() { + const [routesState, setRoutes] = useState([]); + + const init = async () => { + const routes = []; + const mergedRoutes = await mergeRoutePlugins(baseRoutes).catch(() => []); + routeWrapper(mergedRoutes, routes); + setRoutes(routes); + }; + + useEffect(() => { + init(); + }, []); + + return routesState; +} -export default routes as RouteObject[]; +export { useMergeRoutes }; diff --git a/ui/src/utils/pluginKit/index.ts b/ui/src/utils/pluginKit/index.ts index 66d5c3760..346c1456a 100644 --- a/ui/src/utils/pluginKit/index.ts +++ b/ui/src/utils/pluginKit/index.ts @@ -47,17 +47,18 @@ class Plugins { registeredPlugins: Type.ActivatedPlugin[] = []; + initialization: Promise; + constructor() { - 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() { @@ -101,12 +102,9 @@ 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)); + return true; }); } @@ -122,18 +120,6 @@ class Plugins { 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,7 +136,8 @@ class Plugins { const plugins = new Plugins(); -const getRoutePlugins = () => { +const getRoutePlugins = async () => { + await plugins.initialization; return plugins .getPlugins() .filter((plugin) => plugin.info.type === PluginType.Route); @@ -180,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; }