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/components/TagSelector/index.tsx b/ui/src/components/TagSelector/index.tsx index b3cb4480c..3ce368198 100644 --- a/ui/src/components/TagSelector/index.tsx +++ b/ui/src/components/TagSelector/index.tsx @@ -18,10 +18,11 @@ */ /* eslint-disable no-nested-ternary */ -import { FC, useState, useEffect, useRef } from 'react'; +import { FC, useState, useEffect, useRef, useCallback } from 'react'; import { Dropdown, Button, Form } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; +import debounce from 'lodash/debounce'; import { marked } from 'marked'; import classNames from 'classnames'; @@ -69,7 +70,7 @@ const TagSelector: FC = ({ const [currentIndex, setCurrentIndex] = useState(0); const [repeatIndex, setRepeatIndex] = useState(-1); const [searchValue, setSearchValue] = useState(''); - const [tags, setTags] = useState(null); + const [tags, setTags] = useState([]); const [requiredTags, setRequiredTags] = useState(null); const { t } = useTranslation('translation', { keyPrefix: 'tag_selector' }); const { data: userPermission } = useUserPermission('tag.add'); @@ -146,20 +147,23 @@ const TagSelector: FC = ({ handleMenuShow(false); }; - const fetchTags = (str) => { - if (!showRequiredTag && !str) { - setTags([]); - return; - } - queryTags(str).then((res) => { - const tagArray: Type.Tag[] = filterTags(res || []); - if (str === '') { - setRequiredTags(res); + const fetchTags = useCallback( + debounce((str) => { + if (!showRequiredTag && !str) { + setTags([]); + return; } - handleMenuShow(tagArray.length > 0); - setTags(tagArray); - }); - }; + queryTags(str).then((res) => { + const tagArray: Type.Tag[] = filterTags(res || []); + if (str === '') { + setRequiredTags(res); + } + handleMenuShow(tagArray.length > 0); + setTags(tagArray); + }); + }, 400), + [], + ); const resetSearch = () => { setCurrentIndex(0); 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; }