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;
}