Feat: Scrolling knowledge base list and set the number of entries per page to 30 #3695 (#3712)

### What problem does this PR solve?

Feat: Scrolling knowledge base list and set the number of entries per
page to 30 #3695

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2024-11-28 15:25:38 +08:00 committed by GitHub
parent 7ae8828e61
commit ec560cc99d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 158 additions and 38 deletions

View File

@ -125,15 +125,16 @@ def detail():
@manager.route('/list', methods=['GET']) @manager.route('/list', methods=['GET'])
@login_required @login_required
def list_kbs(): def list_kbs():
keywords = request.args.get("keywords", "")
page_number = int(request.args.get("page", 1)) page_number = int(request.args.get("page", 1))
items_per_page = int(request.args.get("page_size", 150)) items_per_page = int(request.args.get("page_size", 150))
orderby = request.args.get("orderby", "create_time") orderby = request.args.get("orderby", "create_time")
desc = request.args.get("desc", True) desc = request.args.get("desc", True)
try: try:
tenants = TenantService.get_joined_tenants_by_user_id(current_user.id) tenants = TenantService.get_joined_tenants_by_user_id(current_user.id)
kbs = KnowledgebaseService.get_by_tenant_ids( kbs, total = KnowledgebaseService.get_by_tenant_ids(
[m["tenant_id"] for m in tenants], current_user.id, page_number, items_per_page, orderby, desc) [m["tenant_id"] for m in tenants], current_user.id, page_number, items_per_page, orderby, desc, keywords)
return get_json_result(data=kbs) return get_json_result(data={"kbs": kbs, "total": total})
except Exception as e: except Exception as e:
return server_error_response(e) return server_error_response(e)

View File

@ -16,6 +16,7 @@
from api.db import StatusEnum, TenantPermission from api.db import StatusEnum, TenantPermission
from api.db.db_models import Knowledgebase, DB, Tenant, User, UserTenant,Document from api.db.db_models import Knowledgebase, DB, Tenant, User, UserTenant,Document
from api.db.services.common_service import CommonService from api.db.services.common_service import CommonService
from peewee import fn
class KnowledgebaseService(CommonService): class KnowledgebaseService(CommonService):
@ -34,7 +35,7 @@ class KnowledgebaseService(CommonService):
@classmethod @classmethod
@DB.connection_context() @DB.connection_context()
def get_by_tenant_ids(cls, joined_tenant_ids, user_id, def get_by_tenant_ids(cls, joined_tenant_ids, user_id,
page_number, items_per_page, orderby, desc): page_number, items_per_page, orderby, desc, keywords):
fields = [ fields = [
cls.model.id, cls.model.id,
cls.model.avatar, cls.model.avatar,
@ -51,20 +52,31 @@ class KnowledgebaseService(CommonService):
User.avatar.alias('tenant_avatar'), User.avatar.alias('tenant_avatar'),
cls.model.update_time cls.model.update_time
] ]
kbs = cls.model.select(*fields).join(User, on=(cls.model.tenant_id == User.id)).where( if keywords:
((cls.model.tenant_id.in_(joined_tenant_ids) & (cls.model.permission == kbs = cls.model.select(*fields).join(User, on=(cls.model.tenant_id == User.id)).where(
TenantPermission.TEAM.value)) | ( ((cls.model.tenant_id.in_(joined_tenant_ids) & (cls.model.permission ==
cls.model.tenant_id == user_id)) TenantPermission.TEAM.value)) | (
& (cls.model.status == StatusEnum.VALID.value) cls.model.tenant_id == user_id))
) & (cls.model.status == StatusEnum.VALID.value),
(fn.LOWER(cls.model.name).contains(keywords.lower()))
)
else:
kbs = cls.model.select(*fields).join(User, on=(cls.model.tenant_id == User.id)).where(
((cls.model.tenant_id.in_(joined_tenant_ids) & (cls.model.permission ==
TenantPermission.TEAM.value)) | (
cls.model.tenant_id == user_id))
& (cls.model.status == StatusEnum.VALID.value)
)
if desc: if desc:
kbs = kbs.order_by(cls.model.getter_by(orderby).desc()) kbs = kbs.order_by(cls.model.getter_by(orderby).desc())
else: else:
kbs = kbs.order_by(cls.model.getter_by(orderby).asc()) kbs = kbs.order_by(cls.model.getter_by(orderby).asc())
count = kbs.count()
kbs = kbs.paginate(page_number, items_per_page) kbs = kbs.paginate(page_number, items_per_page)
return list(kbs.dicts()) return list(kbs.dicts()), count
@classmethod @classmethod
@DB.connection_context() @DB.connection_context()

View File

@ -13,7 +13,7 @@ def test_dataset(get_auth):
while True: while True:
res = list_dataset(get_auth, page_number) res = list_dataset(get_auth, page_number)
data = res.get("data") data = res.get("data")
for item in data: for item in data.get("kbs"):
dataset_id = item.get("id") dataset_id = item.get("id")
dataset_list.append(dataset_id) dataset_list.append(dataset_id)
if len(dataset_list) < page_number * 150: if len(dataset_list) < page_number * 150:
@ -42,7 +42,7 @@ def test_dataset_1k_dataset(get_auth):
while True: while True:
res = list_dataset(get_auth, page_number) res = list_dataset(get_auth, page_number)
data = res.get("data") data = res.get("data")
for item in data: for item in data.get("kbs"):
dataset_id = item.get("id") dataset_id = item.get("id")
dataset_list.append(dataset_id) dataset_list.append(dataset_id)
if len(dataset_list) < page_number * 150: if len(dataset_list) < page_number * 150:

View File

@ -34,7 +34,7 @@ export default defineConfig({
proxy: [ proxy: [
{ {
context: ['/api', '/v1'], context: ['/api', '/v1'],
target: 'http://127.0.0.1:9456/', target: 'http://127.0.0.1:9380/',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
logger: console, logger: console,

20
web/package-lock.json generated
View File

@ -57,6 +57,7 @@
"react-force-graph": "^1.44.4", "react-force-graph": "^1.44.4",
"react-hook-form": "^7.53.1", "react-hook-form": "^7.53.1",
"react-i18next": "^14.0.0", "react-i18next": "^14.0.0",
"react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-pdf-highlighter": "^6.1.0", "react-pdf-highlighter": "^6.1.0",
"react-string-replace": "^1.1.1", "react-string-replace": "^1.1.1",
@ -24705,6 +24706,25 @@
} }
} }
}, },
"node_modules/react-infinite-scroll-component": {
"version": "6.1.0",
"resolved": "https://registry.npmmirror.com/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz",
"integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==",
"dependencies": {
"throttle-debounce": "^2.1.0"
},
"peerDependencies": {
"react": ">=16.0.0"
}
},
"node_modules/react-infinite-scroll-component/node_modules/throttle-debounce": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz",
"integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.2.0.tgz", "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.2.0.tgz",

View File

@ -68,6 +68,7 @@
"react-force-graph": "^1.44.4", "react-force-graph": "^1.44.4",
"react-hook-form": "^7.53.1", "react-hook-form": "^7.53.1",
"react-i18next": "^14.0.0", "react-i18next": "^14.0.0",
"react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-pdf-highlighter": "^6.1.0", "react-pdf-highlighter": "^6.1.0",
"react-string-replace": "^1.1.1", "react-string-replace": "^1.1.1",

View File

@ -3,14 +3,17 @@ import { IKnowledge, ITestingResult } from '@/interfaces/database/knowledge';
import i18n from '@/locales/config'; import i18n from '@/locales/config';
import kbService from '@/services/knowledge-service'; import kbService from '@/services/knowledge-service';
import { import {
useInfiniteQuery,
useIsMutating, useIsMutating,
useMutation, useMutation,
useMutationState, useMutationState,
useQuery, useQuery,
useQueryClient, useQueryClient,
} from '@tanstack/react-query'; } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { message } from 'antd'; import { message } from 'antd';
import { useSearchParams } from 'umi'; import { useSearchParams } from 'umi';
import { useHandleSearchChange } from './logic-hooks';
import { useSetPaginationParams } from './route-hook'; import { useSetPaginationParams } from './route-hook';
export const useKnowledgeBaseId = (): string => { export const useKnowledgeBaseId = (): string => {
@ -50,7 +53,7 @@ export const useNextFetchKnowledgeList = (
gcTime: 0, // https://tanstack.com/query/latest/docs/framework/react/guides/caching?from=reactQueryV3 gcTime: 0, // https://tanstack.com/query/latest/docs/framework/react/guides/caching?from=reactQueryV3
queryFn: async () => { queryFn: async () => {
const { data } = await kbService.getList(); const { data } = await kbService.getList();
const list = data?.data ?? []; const list = data?.data?.kbs ?? [];
return shouldFilterListWithoutDocument return shouldFilterListWithoutDocument
? list.filter((x: IKnowledge) => x.chunk_num > 0) ? list.filter((x: IKnowledge) => x.chunk_num > 0)
: list; : list;
@ -60,6 +63,52 @@ export const useNextFetchKnowledgeList = (
return { list: data, loading }; return { list: data, loading };
}; };
export const useInfiniteFetchKnowledgeList = () => {
const { searchString, handleInputChange } = useHandleSearchChange();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });
const PageSize = 30;
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['infiniteFetchKnowledgeList', debouncedSearchString],
queryFn: async ({ pageParam }) => {
const { data } = await kbService.getList({
page: pageParam,
page_size: PageSize,
keywords: debouncedSearchString,
});
const list = data?.data ?? [];
return list;
},
initialPageParam: 1,
getNextPageParam: (lastPage, pages, lastPageParam) => {
if (lastPageParam * PageSize <= lastPage.total) {
return lastPageParam + 1;
}
return undefined;
},
});
return {
data,
loading: isFetching,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
handleInputChange,
searchString,
};
};
export const useCreateKnowledge = () => { export const useCreateKnowledge = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { const {
@ -95,7 +144,9 @@ export const useDeleteKnowledge = () => {
const { data } = await kbService.rmKb({ kb_id: id }); const { data } = await kbService.rmKb({ kb_id: id });
if (data.code === 0) { if (data.code === 0) {
message.success(i18n.t(`message.deleted`)); message.success(i18n.t(`message.deleted`));
queryClient.invalidateQueries({ queryKey: ['fetchKnowledgeList'] }); queryClient.invalidateQueries({
queryKey: ['infiniteFetchKnowledgeList'],
});
} }
return data?.data ?? []; return data?.data ?? [];
}, },

View File

@ -75,6 +75,7 @@ export default {
namePlaceholder: 'Please input name!', namePlaceholder: 'Please input name!',
doc: 'Docs', doc: 'Docs',
searchKnowledgePlaceholder: 'Search', searchKnowledgePlaceholder: 'Search',
noMoreData: 'It is all, nothing more',
}, },
knowledgeDetails: { knowledgeDetails: {
dataset: 'Dataset', dataset: 'Dataset',

View File

@ -75,6 +75,7 @@ export default {
namePlaceholder: '請輸入名稱', namePlaceholder: '請輸入名稱',
doc: '文件', doc: '文件',
searchKnowledgePlaceholder: '搜索', searchKnowledgePlaceholder: '搜索',
noMoreData: 'It is all, nothing more',
}, },
knowledgeDetails: { knowledgeDetails: {
dataset: '數據集', dataset: '數據集',

View File

@ -75,6 +75,7 @@ export default {
namePlaceholder: '请输入名称', namePlaceholder: '请输入名称',
doc: '文档', doc: '文档',
searchKnowledgePlaceholder: '搜索', searchKnowledgePlaceholder: '搜索',
noMoreData: '沒有更多的數據了',
}, },
knowledgeDetails: { knowledgeDetails: {
dataset: '数据集', dataset: '数据集',

View File

@ -2,6 +2,7 @@
.knowledge { .knowledge {
padding: 48px 0; padding: 48px 0;
overflow: auto;
} }
.topWrapper { .topWrapper {

View File

@ -1,18 +1,26 @@
import { useNextFetchKnowledgeList } from '@/hooks/knowledge-hooks'; import { useInfiniteFetchKnowledgeList } from '@/hooks/knowledge-hooks';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { PlusOutlined, SearchOutlined } from '@ant-design/icons'; import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
import { Button, Empty, Flex, Input, Space, Spin } from 'antd'; import {
Button,
Divider,
Empty,
Flex,
Input,
Skeleton,
Space,
Spin,
} from 'antd';
import { useTranslation } from 'react-i18next';
import InfiniteScroll from 'react-infinite-scroll-component';
import { useSaveKnowledge } from './hooks';
import KnowledgeCard from './knowledge-card'; import KnowledgeCard from './knowledge-card';
import KnowledgeCreatingModal from './knowledge-creating-modal'; import KnowledgeCreatingModal from './knowledge-creating-modal';
import { useTranslation } from 'react-i18next'; import { useMemo } from 'react';
import { useSaveKnowledge, useSearchKnowledge } from './hooks';
import styles from './index.less'; import styles from './index.less';
const KnowledgeList = () => { const KnowledgeList = () => {
const { searchString, handleInputChange } = useSearchKnowledge();
const { loading, list: data } = useNextFetchKnowledgeList();
const list = data.filter((x) => x.name.includes(searchString));
const { data: userInfo } = useFetchUserInfo(); const { data: userInfo } = useFetchUserInfo();
const { t } = useTranslation('translation', { keyPrefix: 'knowledgeList' }); const { t } = useTranslation('translation', { keyPrefix: 'knowledgeList' });
const { const {
@ -22,9 +30,23 @@ const KnowledgeList = () => {
onCreateOk, onCreateOk,
loading: creatingLoading, loading: creatingLoading,
} = useSaveKnowledge(); } = useSaveKnowledge();
const {
fetchNextPage,
data,
hasNextPage,
searchString,
handleInputChange,
loading,
} = useInfiniteFetchKnowledgeList();
console.log('🚀 ~ KnowledgeList ~ data:', data);
const nextList = data?.pages?.flatMap((x) => x.kbs) ?? [];
const total = useMemo(() => {
return data?.pages.at(-1).total ?? 0;
}, [data?.pages]);
return ( return (
<Flex className={styles.knowledge} vertical flex={1}> <Flex className={styles.knowledge} vertical flex={1} id="scrollableDiv">
<div className={styles.topWrapper}> <div className={styles.topWrapper}>
<div> <div>
<span className={styles.title}> <span className={styles.title}>
@ -53,21 +75,30 @@ const KnowledgeList = () => {
</Space> </Space>
</div> </div>
<Spin spinning={loading}> <Spin spinning={loading}>
<Flex <InfiniteScroll
gap={'large'} dataLength={nextList?.length ?? 0}
wrap="wrap" next={fetchNextPage}
className={styles.knowledgeCardContainer} hasMore={hasNextPage}
loader={<Skeleton avatar paragraph={{ rows: 1 }} active />}
endMessage={total && <Divider plain>{t('noMoreData')} 🤐</Divider>}
scrollableTarget="scrollableDiv"
> >
{list.length > 0 ? ( <Flex
list.map((item: any) => { gap={'large'}
return ( wrap="wrap"
<KnowledgeCard item={item} key={item.name}></KnowledgeCard> className={styles.knowledgeCardContainer}
); >
}) {nextList?.length > 0 ? (
) : ( nextList.map((item: any) => {
<Empty className={styles.knowledgeEmpty}></Empty> return (
)} <KnowledgeCard item={item} key={item.name}></KnowledgeCard>
</Flex> );
})
) : (
<Empty className={styles.knowledgeEmpty}></Empty>
)}
</Flex>
</InfiniteScroll>
</Spin> </Spin>
<KnowledgeCreatingModal <KnowledgeCreatingModal
loading={creatingLoading} loading={creatingLoading}