### What problem does this PR solve? feat: Display mindmap in drawer #2247 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
parent
3044cb85fd
commit
6a0702f55f
@ -15,9 +15,6 @@ server {
|
|||||||
include proxy.conf;
|
include proxy.conf;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /HPImageArchive {
|
|
||||||
proxy_pass https://cn.bing.com;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useFetchMindMap, useFetchRelatedQuestions } from '@/hooks/chat-hooks';
|
import { useFetchMindMap, useFetchRelatedQuestions } from '@/hooks/chat-hooks';
|
||||||
|
import { useSetModalState } from '@/hooks/common-hooks';
|
||||||
import { useTestChunkRetrieval } from '@/hooks/knowledge-hooks';
|
import { useTestChunkRetrieval } from '@/hooks/knowledge-hooks';
|
||||||
import {
|
import {
|
||||||
useGetPaginationWithRouter,
|
useGetPaginationWithRouter,
|
||||||
@ -7,7 +8,13 @@ import {
|
|||||||
import { IAnswer } from '@/interfaces/database/chat';
|
import { IAnswer } from '@/interfaces/database/chat';
|
||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import { get, isEmpty, trim } from 'lodash';
|
import { get, isEmpty, trim } from 'lodash';
|
||||||
import { ChangeEventHandler, useCallback, useEffect, useState } from 'react';
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
export const useSendQuestion = (kbIds: string[]) => {
|
export const useSendQuestion = (kbIds: string[]) => {
|
||||||
const { send, answer, done } = useSendMessageWithSse(api.ask);
|
const { send, answer, done } = useSendMessageWithSse(api.ask);
|
||||||
@ -16,11 +23,6 @@ export const useSendQuestion = (kbIds: string[]) => {
|
|||||||
const [currentAnswer, setCurrentAnswer] = useState({} as IAnswer);
|
const [currentAnswer, setCurrentAnswer] = useState({} as IAnswer);
|
||||||
const { fetchRelatedQuestions, data: relatedQuestions } =
|
const { fetchRelatedQuestions, data: relatedQuestions } =
|
||||||
useFetchRelatedQuestions();
|
useFetchRelatedQuestions();
|
||||||
const {
|
|
||||||
fetchMindMap,
|
|
||||||
data: mindMap,
|
|
||||||
loading: mindMapLoading,
|
|
||||||
} = useFetchMindMap();
|
|
||||||
const [searchStr, setSearchStr] = useState<string>('');
|
const [searchStr, setSearchStr] = useState<string>('');
|
||||||
const [isFirstRender, setIsFirstRender] = useState(true);
|
const [isFirstRender, setIsFirstRender] = useState(true);
|
||||||
const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]);
|
const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]);
|
||||||
@ -43,10 +45,7 @@ export const useSendQuestion = (kbIds: string[]) => {
|
|||||||
page: 1,
|
page: 1,
|
||||||
size: pagination.pageSize,
|
size: pagination.pageSize,
|
||||||
});
|
});
|
||||||
fetchMindMap({
|
|
||||||
question: q,
|
|
||||||
kb_ids: kbIds,
|
|
||||||
});
|
|
||||||
fetchRelatedQuestions(q);
|
fetchRelatedQuestions(q);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@ -54,7 +53,6 @@ export const useSendQuestion = (kbIds: string[]) => {
|
|||||||
testChunk,
|
testChunk,
|
||||||
kbIds,
|
kbIds,
|
||||||
fetchRelatedQuestions,
|
fetchRelatedQuestions,
|
||||||
fetchMindMap,
|
|
||||||
setPagination,
|
setPagination,
|
||||||
pagination.pageSize,
|
pagination.pageSize,
|
||||||
],
|
],
|
||||||
@ -117,11 +115,10 @@ export const useSendQuestion = (kbIds: string[]) => {
|
|||||||
sendingLoading,
|
sendingLoading,
|
||||||
answer: currentAnswer,
|
answer: currentAnswer,
|
||||||
relatedQuestions: relatedQuestions?.slice(0, 5) ?? [],
|
relatedQuestions: relatedQuestions?.slice(0, 5) ?? [],
|
||||||
mindMap,
|
|
||||||
mindMapLoading,
|
|
||||||
searchStr,
|
searchStr,
|
||||||
isFirstRender,
|
isFirstRender,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
|
isSearchStrEmpty: isEmpty(trim(searchStr)),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -191,3 +188,51 @@ export const useTestRetrieval = (
|
|||||||
setSelectedDocumentIds,
|
setSelectedDocumentIds,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useShowMindMapDrawer = (kbIds: string[], question: string) => {
|
||||||
|
const { visible, showModal, hideModal } = useSetModalState();
|
||||||
|
|
||||||
|
const {
|
||||||
|
fetchMindMap,
|
||||||
|
data: mindMap,
|
||||||
|
loading: mindMapLoading,
|
||||||
|
} = useFetchMindMap();
|
||||||
|
|
||||||
|
const handleShowModal = useCallback(() => {
|
||||||
|
fetchMindMap({ question: trim(question), kb_ids: kbIds });
|
||||||
|
showModal();
|
||||||
|
}, [fetchMindMap, showModal, question, kbIds]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mindMap,
|
||||||
|
mindMapVisible: visible,
|
||||||
|
mindMapLoading,
|
||||||
|
showMindMapModal: handleShowModal,
|
||||||
|
hideMindMapModal: hideModal,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePendingMindMap = () => {
|
||||||
|
const [count, setCount] = useState<number>(0);
|
||||||
|
const ref = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
const setCountInterval = useCallback(() => {
|
||||||
|
ref.current = setInterval(() => {
|
||||||
|
setCount((pre) => {
|
||||||
|
if (pre > 40) {
|
||||||
|
clearInterval(ref?.current);
|
||||||
|
}
|
||||||
|
return pre + 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCountInterval();
|
||||||
|
return () => {
|
||||||
|
clearInterval(ref?.current);
|
||||||
|
};
|
||||||
|
}, [setCountInterval]);
|
||||||
|
|
||||||
|
return Number(((count / 43) * 100).toFixed(0));
|
||||||
|
};
|
||||||
|
|||||||
@ -16,17 +16,19 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainLayout {
|
// .mainLayout {
|
||||||
background: transparent;
|
// background: transparent;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
.transparentSearchSide {
|
// .transparentSearchSide {
|
||||||
background-color: rgb(251 251 251 / 88%) !important;
|
// background-color: rgb(251 251 251 / 88%) !important;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.searchSide {
|
.searchSide {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
max-width: 400px !important;
|
||||||
|
min-width: auto !important;
|
||||||
|
|
||||||
:global(.ant-layout-sider-children) {
|
:global(.ant-layout-sider-children) {
|
||||||
height: auto;
|
height: auto;
|
||||||
@ -45,19 +47,19 @@
|
|||||||
.list {
|
.list {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
// height: 100%;
|
|
||||||
height: calc(100vh - 76px);
|
height: calc(100vh - 76px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background-color: transparent;
|
// background-color: transparent;
|
||||||
&::-webkit-scrollbar-track {
|
// &::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
// background: transparent;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
.checkbox {
|
.checkbox {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.knowledgeName {
|
.knowledgeName {
|
||||||
width: 116px;
|
width: 116px;
|
||||||
|
max-width: 270px;
|
||||||
}
|
}
|
||||||
.embeddingId {
|
.embeddingId {
|
||||||
width: 170px;
|
width: 170px;
|
||||||
@ -70,27 +72,17 @@
|
|||||||
|
|
||||||
.content {
|
.content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px 16% 10px;
|
||||||
.hide {
|
.hide {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainMixin() {
|
|
||||||
overflow: auto;
|
|
||||||
padding: 20px 10px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.largeMain {
|
|
||||||
width: 100%;
|
|
||||||
.mainMixin();
|
|
||||||
}
|
|
||||||
.main {
|
.main {
|
||||||
width: 60%;
|
margin: 0 auto;
|
||||||
.mainMixin();
|
width: 100%;
|
||||||
}
|
max-width: 1200px;
|
||||||
|
|
||||||
.graph {
|
|
||||||
width: 40%;
|
|
||||||
padding: 20px 10px 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlightContent {
|
.highlightContent {
|
||||||
@ -103,6 +95,9 @@
|
|||||||
.documentReference {
|
.documentReference {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.pagination {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.answerWrapper {
|
.answerWrapper {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
@ -122,9 +117,9 @@
|
|||||||
border-start-start-radius: 30px !important;
|
border-start-start-radius: 30px !important;
|
||||||
border-end-start-radius: 30px !important;
|
border-end-start-radius: 30px !important;
|
||||||
}
|
}
|
||||||
:global(.ant-input-group-addon) {
|
// :global(.ant-input-group-addon) {
|
||||||
background-color: transparent;
|
// background-color: transparent;
|
||||||
}
|
// }
|
||||||
input {
|
input {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
@ -138,7 +133,7 @@
|
|||||||
.globalInput {
|
.globalInput {
|
||||||
width: 600px;
|
width: 600px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 30%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
.input();
|
.input();
|
||||||
}
|
}
|
||||||
@ -187,3 +182,12 @@
|
|||||||
max-height: 40vh;
|
max-height: 40vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mindMapFloatButton {
|
||||||
|
top: 20%;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
:global(.ant-float-btn-content, .ant-float-btn-icon) {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import FileIcon from '@/components/file-icon';
|
import FileIcon from '@/components/file-icon';
|
||||||
import HightLightMarkdown from '@/components/highlight-markdown';
|
import HightLightMarkdown from '@/components/highlight-markdown';
|
||||||
import { ImageWithPopover } from '@/components/image';
|
import { ImageWithPopover } from '@/components/image';
|
||||||
import IndentedTree from '@/components/indented-tree/indented-tree';
|
|
||||||
import PdfDrawer from '@/components/pdf-drawer';
|
import PdfDrawer from '@/components/pdf-drawer';
|
||||||
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||||
import RetrievalDocuments from '@/components/retrieval-documents';
|
import RetrievalDocuments from '@/components/retrieval-documents';
|
||||||
|
import SvgIcon from '@/components/svg-icon';
|
||||||
import {
|
import {
|
||||||
useNextFetchKnowledgeList,
|
useNextFetchKnowledgeList,
|
||||||
useSelectTestingResult,
|
useSelectTestingResult,
|
||||||
@ -15,6 +15,7 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
Divider,
|
Divider,
|
||||||
Flex,
|
Flex,
|
||||||
|
FloatButton,
|
||||||
Input,
|
Input,
|
||||||
Layout,
|
Layout,
|
||||||
List,
|
List,
|
||||||
@ -25,14 +26,16 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
Tag,
|
Tag,
|
||||||
|
Tooltip,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import MarkdownContent from '../chat/markdown-content';
|
import MarkdownContent from '../chat/markdown-content';
|
||||||
import { useFetchBackgroundImage, useSendQuestion } from './hooks';
|
import { useSendQuestion, useShowMindMapDrawer } from './hooks';
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
|
import MindMapDrawer from './mindmap-drawer';
|
||||||
import SearchSidebar from './sidebar';
|
import SearchSidebar from './sidebar';
|
||||||
|
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
@ -56,29 +59,28 @@ const SearchPage = () => {
|
|||||||
answer,
|
answer,
|
||||||
sendingLoading,
|
sendingLoading,
|
||||||
relatedQuestions,
|
relatedQuestions,
|
||||||
mindMap,
|
|
||||||
searchStr,
|
searchStr,
|
||||||
loading,
|
loading,
|
||||||
isFirstRender,
|
isFirstRender,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
|
isSearchStrEmpty,
|
||||||
} = useSendQuestion(checkedWithoutEmbeddingIdList);
|
} = useSendQuestion(checkedWithoutEmbeddingIdList);
|
||||||
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
|
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
|
||||||
useClickDrawer();
|
useClickDrawer();
|
||||||
const imgUrl = useFetchBackgroundImage();
|
|
||||||
const { pagination } = useGetPaginationWithRouter();
|
const { pagination } = useGetPaginationWithRouter();
|
||||||
|
const {
|
||||||
|
mindMapVisible,
|
||||||
|
hideMindMapModal,
|
||||||
|
showMindMapModal,
|
||||||
|
mindMapLoading,
|
||||||
|
mindMap,
|
||||||
|
} = useShowMindMapDrawer(checkedWithoutEmbeddingIdList, searchStr);
|
||||||
|
|
||||||
const onChange: PaginationProps['onChange'] = (pageNumber, pageSize) => {
|
const onChange: PaginationProps['onChange'] = (pageNumber, pageSize) => {
|
||||||
pagination.onChange?.(pageNumber, pageSize);
|
pagination.onChange?.(pageNumber, pageSize);
|
||||||
handleTestChunk(selectedDocumentIds, pageNumber, pageSize);
|
handleTestChunk(selectedDocumentIds, pageNumber, pageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMindMapEmpty = useMemo(() => {
|
|
||||||
return (
|
|
||||||
(Array.isArray(mindMap?.children) && mindMap.children.length === 0) ||
|
|
||||||
!Array.isArray(mindMap?.children)
|
|
||||||
);
|
|
||||||
}, [mindMap]);
|
|
||||||
|
|
||||||
const InputSearch = (
|
const InputSearch = (
|
||||||
<Search
|
<Search
|
||||||
value={searchStr}
|
value={searchStr}
|
||||||
@ -96,10 +98,7 @@ const SearchPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Layout
|
<Layout className={styles.searchPage}>
|
||||||
className={styles.searchPage}
|
|
||||||
style={{ backgroundImage: `url(${imgUrl})` }}
|
|
||||||
>
|
|
||||||
<SearchSidebar
|
<SearchSidebar
|
||||||
isFirstRender={isFirstRender}
|
isFirstRender={isFirstRender}
|
||||||
checkedList={checkedWithoutEmbeddingIdList}
|
checkedList={checkedWithoutEmbeddingIdList}
|
||||||
@ -108,20 +107,14 @@ const SearchPage = () => {
|
|||||||
<Layout className={isFirstRender ? styles.mainLayout : ''}>
|
<Layout className={isFirstRender ? styles.mainLayout : ''}>
|
||||||
<Content>
|
<Content>
|
||||||
{isFirstRender ? (
|
{isFirstRender ? (
|
||||||
<Flex
|
<Flex justify="center" className={styles.firstRenderContent}>
|
||||||
justify="center"
|
|
||||||
align="center"
|
|
||||||
className={styles.firstRenderContent}
|
|
||||||
>
|
|
||||||
<Flex vertical align="center" gap={'large'}>
|
<Flex vertical align="center" gap={'large'}>
|
||||||
{InputSearch}
|
{InputSearch}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
) : (
|
) : (
|
||||||
<Flex className={styles.content}>
|
<Flex className={styles.content}>
|
||||||
<section
|
<section className={styles.main}>
|
||||||
className={isMindMapEmpty ? styles.largeMain : styles.main}
|
|
||||||
>
|
|
||||||
{InputSearch}
|
{InputSearch}
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
@ -226,28 +219,43 @@ const SearchPage = () => {
|
|||||||
{...pagination}
|
{...pagination}
|
||||||
total={total}
|
total={total}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
className={styles.pagination}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<section
|
|
||||||
className={isMindMapEmpty ? styles.hide : styles.graph}
|
|
||||||
>
|
|
||||||
<IndentedTree
|
|
||||||
data={mindMap}
|
|
||||||
show
|
|
||||||
style={{ width: '100%', height: '100%' }}
|
|
||||||
></IndentedTree>
|
|
||||||
</section>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
{!isFirstRender &&
|
||||||
|
!isSearchStrEmpty &&
|
||||||
|
!isEmpty(checkedWithoutEmbeddingIdList) && (
|
||||||
|
<Tooltip title={t('chunk.mind')} zIndex={1}>
|
||||||
|
<FloatButton
|
||||||
|
className={styles.mindMapFloatButton}
|
||||||
|
onClick={showMindMapModal}
|
||||||
|
icon={
|
||||||
|
<SvgIcon name="paper-clip" width={24} height={30}></SvgIcon>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{visible && (
|
||||||
<PdfDrawer
|
<PdfDrawer
|
||||||
visible={visible}
|
visible={visible}
|
||||||
hideModal={hideModal}
|
hideModal={hideModal}
|
||||||
documentId={documentId}
|
documentId={documentId}
|
||||||
chunk={selectedChunk}
|
chunk={selectedChunk}
|
||||||
></PdfDrawer>
|
></PdfDrawer>
|
||||||
|
)}
|
||||||
|
{mindMapVisible && (
|
||||||
|
<MindMapDrawer
|
||||||
|
visible={mindMapVisible}
|
||||||
|
hideModal={hideMindMapModal}
|
||||||
|
data={mindMap}
|
||||||
|
loading={mindMapLoading}
|
||||||
|
></MindMapDrawer>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
36
web/src/pages/search/mindmap-drawer.tsx
Normal file
36
web/src/pages/search/mindmap-drawer.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import IndentedTree from '@/components/indented-tree/indented-tree';
|
||||||
|
import { IModalProps } from '@/interfaces/common';
|
||||||
|
import { Drawer, Flex, Progress } from 'antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { usePendingMindMap } from './hooks';
|
||||||
|
|
||||||
|
interface IProps extends IModalProps<any> {
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MindMapDrawer = ({ data, hideModal, visible, loading }: IProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const percent = usePendingMindMap();
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title={t('chunk.mind')}
|
||||||
|
onClose={hideModal}
|
||||||
|
open={visible}
|
||||||
|
width={'40vw'}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Flex justify="center">
|
||||||
|
<Progress type="circle" percent={percent} size={200} />
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<IndentedTree
|
||||||
|
data={data}
|
||||||
|
show
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
></IndentedTree>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MindMapDrawer;
|
||||||
@ -138,7 +138,7 @@ const SearchSidebar = ({
|
|||||||
[styles.transparentSearchSide]: isFirstRender,
|
[styles.transparentSearchSide]: isFirstRender,
|
||||||
})}
|
})}
|
||||||
theme={'light'}
|
theme={'light'}
|
||||||
width={240}
|
width={'20%'}
|
||||||
>
|
>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<Tree
|
<Tree
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user