feat(knowledge): add overview page for knowledge base logs

- Implement new overview page to display file and dataset processing logs
This commit is contained in:
2025-11-04 15:32:55 +08:00
parent 74c4f798a5
commit 37dcab1597
12 changed files with 397 additions and 8 deletions

View File

@@ -25,6 +25,7 @@ export const RunningStatusMap = {
[RUNNING_STATUS_KEYS.DONE]: 'Success',
[RUNNING_STATUS_KEYS.FAIL]: 'Failed',
} as const;
export const MODEL_VARIABLE_TYPES = Object.freeze({
Improvise: 'Improvise',
Precise: 'Precise',

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import knowledgeService from '@/services/knowledge_service';
import type { IKnowledge, IKnowledgeGraph, IKnowledgeResult } from '@/interfaces/database/knowledge';
import type { IKnowledge, IKnowledgeGraph, IKnowledgeResult, IOverviewTital, IFileLogList } from '@/interfaces/database/knowledge';
import type { IFetchKnowledgeListRequestBody, IFetchKnowledgeListRequestParams } from '@/interfaces/request/knowledge';
import logger from '@/utils/logger';
@@ -477,3 +477,161 @@ export const useKnowledgeBatchOperations = () => {
};
};
/* ============================================================================
* useKnowledgeOverview
* ============================================================================ */
const LOGS_TABS = {
FILE_LOGS: 'fileLogs',
DATASET_LOGS: 'datasetLogs',
} as const
type LogTabs = typeof LOGS_TABS[keyof typeof LOGS_TABS]
/**
* 知识库详情页 - 概览数据获取Hook
*/
export interface UseKnowledgeOverviewReturn {
overview: IOverviewTital | null;
fileLogs: IFileLogList | null;
datasetLogs: IFileLogList | null;
loading: boolean;
error: string | null;
currentPage: number;
pageSize: number;
activeTab: LogTabs;
keywords: string;
setCurrentPage: (page: number) => void;
setPageSize: (size: number) => void;
setActiveTab: (tab: LogTabs) => void;
setKeywords: (keywords: string) => void;
refresh: () => Promise<void>;
}
export const useKnowledgeOverview = (kb_id: string): UseKnowledgeOverviewReturn => {
const { t } = useTranslation();
const [overview, setOverview] = useState<IOverviewTital | null>(null);
const [fileLogs, setFileLogs] = useState<IFileLogList | null>(null);
const [datasetLogs, setDatasetLogs] = useState<IFileLogList | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [activeTab, setActiveTab] = useState<LogTabs>(LOGS_TABS.FILE_LOGS);
const [keywords, setKeywordsState] = useState('');
const fetchOverviewTital = useCallback(async () => {
if (!kb_id) return;
try {
setLoading(true);
setError(null);
const response = await knowledgeService.getKnowledgeBasicInfo({ kb_id });
if (response.data.code === 0) {
setOverview(response.data.data);
} else {
throw new Error(response.data.message || t('knowledgeHooks.fetchOverviewFailed'));
}
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message || t('knowledgeHooks.fetchOverviewFailed');
setError(errorMessage);
console.error('Failed to fetch overview tital:', err);
} finally {
setLoading(false);
}
}, [kb_id]);
const fetchPipelineLogs = useCallback(async () => {
if (!kb_id) return;
try {
setLoading(true);
setError(null);
const response = await knowledgeService.getDataPipelineLogs({
kb_id,
page: currentPage,
page_size: pageSize,
keywords,
});
if (response.data.code === 0) {
setFileLogs(response.data.data as IFileLogList);
} else {
throw new Error(response.data.message);
}
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message;
setError(errorMessage);
console.error('Failed to fetch data pipeline logs:', err);
} finally {
setLoading(false);
}
}, [kb_id, currentPage, pageSize, keywords]);
const fetchPipelineDatasetLogs = useCallback(async () => {
if (!kb_id) return;
try {
setLoading(true);
setError(null);
const response = await knowledgeService.getPipelineDatasetLogs({
kb_id,
page: currentPage,
page_size: pageSize,
keywords,
});
if (response.data.code === 0) {
setDatasetLogs(response.data.data as IFileLogList);
} else {
throw new Error(response.data.message);
}
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message;
setError(errorMessage);
console.error('Failed to fetch pipeline dataset logs:', err);
} finally {
setLoading(false);
}
}, [kb_id, currentPage, pageSize, keywords]);
useEffect(() => {
fetchOverviewTital();
}, [fetchOverviewTital]);
useEffect(() => {
if (activeTab === LOGS_TABS.FILE_LOGS) {
fetchPipelineLogs();
} else {
fetchPipelineDatasetLogs();
}
}, [activeTab, fetchPipelineLogs, fetchPipelineDatasetLogs]);
// 设置关键词并重置到第一页
const setKeywords = useCallback((kw: string) => {
setKeywordsState(kw);
setCurrentPage(1);
}, []);
const refresh = useCallback(async () => {
await Promise.all([
fetchOverviewTital(),
fetchPipelineLogs(),
fetchPipelineDatasetLogs(),
]);
}, [fetchOverviewTital, fetchPipelineLogs, fetchPipelineDatasetLogs]);
return {
overview,
fileLogs,
datasetLogs,
loading,
error,
currentPage,
pageSize,
activeTab,
keywords,
setCurrentPage,
setPageSize,
setActiveTab,
setKeywords,
refresh,
};
};

View File

@@ -1,4 +1,4 @@
import type { RunningStatus } from '@/constants/knowledge';
import { RunningStatusMap, type RunningStatus } from '@/constants/knowledge';
import type { IDocumentInfo } from './document';
@@ -491,3 +491,55 @@ export interface IKnowledgeGraph {
/** 思维导图数据(已注释) */
// mind_map: TreeData;
}
/** 文档日志项接口 */
export interface IDocumentLog {
fileName: string;
status: RunningStatus;
statusName: typeof RunningStatusMap[keyof typeof RunningStatusMap];
}
/** 日志统计项接口 */
export interface IOverviewTital {
cancelled: number;
failed: number;
finished: number;
processing: number;
}
/** 日志项接口 */
export interface IFileLogItem {
create_date: string;
create_time: number;
document_id: string;
document_name: string;
document_suffix: string;
document_type: string;
dsl: any;
path: string[];
task_id: string;
id: string;
name: string;
kb_id: string;
operation_status: string;
parser_id: string;
pipeline_id: string;
pipeline_title: string;
avatar: string;
process_begin_at: null | string;
process_duration: number;
progress: number;
progress_msg: string;
source_from: string;
status: string;
task_type: string;
tenant_id: string;
update_date: string;
update_time: number;
}
/** 日志列表接口 */
export interface IFileLogList {
logs: Array<IFileLogItem & IDocumentLog>;
total: number;
}

View File

@@ -427,6 +427,7 @@ export default {
raptor: 'Raptor',
processingType: 'Processing Type',
dataPipeline: 'Data Pipeline',
ingestionPipeline: 'Ingestion Pipeline',
operations: 'Operations',
taskId: 'Task ID',
duration: 'Duration',
@@ -453,6 +454,7 @@ export default {
retrievalTestingDescription:
'Conduct a retrieval test to check if RAGFlow can recover the intended content for the LLM.',
Parse: 'Parse',
file: 'File',
dataset: 'Dataset',
testing: 'Retrieval testing',
files: 'files',

View File

@@ -421,6 +421,7 @@ export default {
raptor: 'Raptor',
processingType: '处理类型',
dataPipeline: '数据管道',
ingestionPipeline: '摄取管道',
operations: '操作',
taskId: '任务ID',
duration: '耗时',
@@ -447,6 +448,7 @@ export default {
retrievalTestingDescription:
'进行检索测试,检查 RAGFlow 是否能够为大语言模型LLM恢复预期的内容。',
Parse: '解析',
file: '文件',
dataset: '知识库',
testing: '检索测试',
configuration: '配置',

View File

@@ -4,18 +4,20 @@ import {
Dashboard as DashboardIcon,
Search as TestIcon,
Settings as ConfigIcon,
ListAlt as OverviewIcon,
} from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
interface FloatingActionButtonsProps {
onTestClick: () => void;
onConfigClick: () => void;
onOverviewClick: () => void;
}
const FloatingActionButtons: React.FC<FloatingActionButtonsProps> = ({
onTestClick,
onConfigClick,
onOverviewClick,
}) => {
const { t } = useTranslation();
@@ -30,6 +32,11 @@ const FloatingActionButtons: React.FC<FloatingActionButtonsProps> = ({
name: t('knowledge.configSettings'),
onClick: onConfigClick,
},
{
icon: <OverviewIcon />,
name: t('knowledgeDetails.overview'),
onClick: onOverviewClick,
},
];
return (

View File

@@ -405,6 +405,7 @@ function KnowledgeBaseDetail() {
<FloatingActionButtons
onTestClick={() => navigate(`/knowledge/${id}/testing`)}
onConfigClick={() => navigate(`/knowledge/${id}/setting`)}
onOverviewClick={() => navigate(`/knowledge/${id}/overview`)}
/>
{/* 上传文件对话框 */}

View File

@@ -2,4 +2,5 @@ export { default as KnowledgeBaseList } from './list';
export { default as KnowledgeBaseCreate } from './create';
export { default as KnowledgeBaseDetail } from './detail'
export { default as KnowledgeBaseSetting } from './setting';
export { default as KnowledgeBaseTesting } from './testing';
export { default as KnowledgeBaseTesting } from './testing';
export { default as KnowledgeLogsPage } from './overview';

View File

@@ -0,0 +1,162 @@
import React from 'react';
import { Box, Grid, Paper, Typography, IconButton, TextField, Tabs, Tab } from '@mui/material';
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
import FilterListIcon from '@mui/icons-material/FilterList';
import SearchIcon from '@mui/icons-material/Search';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { useKnowledgeOverview } from '@/hooks/knowledge-hooks';
import type { IDocumentLog, IFileLogItem } from '@/interfaces/database/knowledge';
import logger from '@/utils/logger';
import i18n, { LanguageAbbreviation } from '@/locales';
import { enUS, zhCN } from '@mui/x-data-grid/locales';
const PROCESSING_TYPES = {
knowledgeGraph: 'GraphRAG',
raptor: 'RAPTOR',
} as const
type ProcessingType = typeof PROCESSING_TYPES[keyof typeof PROCESSING_TYPES]
const ProcessingTypeMap = {
[PROCESSING_TYPES.knowledgeGraph]: 'Knowledge Graph',
[PROCESSING_TYPES.raptor]: 'RAPTOR',
} as const
function KnowledgeLogsPage() {
const { t } = useTranslation();
const [paginationModel, setPaginationModel] = React.useState({ page: 0, pageSize: 50 });
// 根据当前语言获取DataGrid的localeText
const getDataGridLocale = () => {
const currentLanguage = i18n.language;
return currentLanguage === LanguageAbbreviation.Zh ? zhCN : enUS;
};
// 路由参数与数据Hook
const { id: kbId = '' } = useParams();
const {
overview,
fileLogs,
datasetLogs,
loading,
currentPage,
pageSize,
setCurrentPage,
setPageSize,
activeTab,
keywords,
setActiveTab,
setKeywords,
} = useKnowledgeOverview(kbId);
// 同步分页模型到Hook
React.useEffect(() => {
setPaginationModel({ page: Math.max(currentPage - 1, 0), pageSize });
}, [currentPage, pageSize]);
const columnsFile: GridColDef[] = [
{ field: 'id', headerName: 'ID', width: 100 },
{
field: 'document_name', headerName: t('knowledgeDetails.fileName'), flex: 1, minWidth: 160, valueGetter: (params) => {
logger.info('params', params)
return ''
}
},
{ field: 'source_from', headerName: t('knowledgeDetails.source'), flex: 1, minWidth: 140 },
{ field: 'pipeline_title', headerName: t('knowledgeDetails.ingestionPipeline'), flex: 1, minWidth: 180 },
{ field: 'create_date', headerName: t('knowledgeDetails.startDate') || 'Start Date', flex: 0.8, minWidth: 160, sortable: true },
{ field: 'task_type', headerName: t('knowledgeDetails.task') || 'Task', flex: 0.8, minWidth: 140 },
{ field: 'status', headerName: t('knowledgeDetails.status'), flex: 0.7, minWidth: 120 },
{ field: 'operations', headerName: t('knowledgeDetails.operations') || 'Operations', flex: 0.8, minWidth: 160, sortable: false, filterable: false, align: 'center', headerAlign: 'center' },
];
const columnsDataset: GridColDef[] = [
{ field: 'id', headerName: 'ID', width: 100 },
{ field: 'create_date', headerName: t('knowledgeDetails.startDate') || 'Start Date', flex: 1, minWidth: 160, sortable: true },
{ field: 'task_type', headerName: 'Processing Type', flex: 1, minWidth: 180 },
{ field: 'status', headerName: t('knowledgeDetails.status'), flex: 0.8, minWidth: 120 },
{ field: 'operations', headerName: t('knowledgeDetails.operations') || 'Operations', flex: 0.8, minWidth: 160, sortable: false, filterable: false, align: 'center', headerAlign: 'center' },
];
const columns = React.useMemo(() => (activeTab === 'fileLogs' ?
columnsFile :
columnsDataset),
[activeTab]);
const rows = React.useMemo(() => (activeTab === 'fileLogs' ?
(fileLogs?.logs || []) :
(datasetLogs?.logs || [])),
[activeTab, fileLogs, datasetLogs]);
const rowCount = React.useMemo(() => (activeTab === 'fileLogs' ?
(fileLogs?.total || 0) :
(datasetLogs?.total || 0)),
[activeTab, fileLogs, datasetLogs]);
return (
<Box sx={{ p: 3 }}>
{/* 顶部统计卡片占位 */}
<Grid container spacing={2} sx={{ mb: 2 }}>
<Grid size={{ xs: 12, md: 4 }}>
<Paper sx={{ p: 2 }}>
<Typography variant="subtitle2">{t('datasetOverview.totalFiles')}</Typography>
<Typography variant="h4" sx={{ mt: 1 }}>{fileLogs?.total ?? 0}</Typography>
<Typography variant="caption" color="text.secondary">0% from last week</Typography>
</Paper>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<Paper sx={{ p: 2 }}>
<Typography variant="subtitle2">{t('datasetOverview.downloading')}</Typography>
<Typography variant="h4" sx={{ mt: 1 }}>0</Typography>
<Typography variant="caption" color="text.secondary">{t('knowledgeDetails.success')} {overview?.finished ?? 0} · {t('knowledgeDetails.failed')} {overview?.failed ?? 0}</Typography>
</Paper>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<Paper sx={{ p: 2 }}>
<Typography variant="subtitle2">{t('datasetOverview.processing')}</Typography>
<Typography variant="h4" sx={{ mt: 1 }}>{overview?.processing ?? 0}</Typography>
<Typography variant="caption" color="text.secondary">{t('knowledgeDetails.success')} {overview?.finished ?? 0} · {t('knowledgeDetails.failed')} {overview?.failed ?? 0}</Typography>
</Paper>
</Grid>
</Grid>
{/* Tabs & Filter/Search */}
<Box display="flex" alignItems="center" gap={2} sx={{ mb: 2 }}>
<Tabs value={activeTab} onChange={(_, v) => setActiveTab(v)}>
<Tab value="file" label={t('knowledgeDetails.file')} />
<Tab value="dataset" label={t('knowledgeDetails.dataset')} />
</Tabs>
{/* <IconButton aria-label="filter"><FilterListIcon /></IconButton> */}
<Box display="flex" alignItems="center" gap={1} sx={{ ml: 'auto' }}>
<SearchIcon color="action" />
<TextField size="small" placeholder={t('common.search')} value={keywords} onChange={(e) => setKeywords(e.target.value)} />
</Box>
</Box>
{/* DataGrid 表格占位 */}
<Paper variant="outlined" sx={{ height: 420 }}>
<DataGrid
columns={columns}
rows={rows}
getRowId={(row) => row.id}
density="comfortable"
pageSizeOptions={[20, 50, 100]}
paginationMode="server"
rowCount={rowCount}
paginationModel={paginationModel}
onPaginationModelChange={(model) => {
setPaginationModel(model);
setCurrentPage(model.page + 1);
setPageSize(model.pageSize);
}}
loading={loading}
disableColumnMenu
localeText={getDataGridLocale().components.MuiDataGrid.defaultProps.localeText}
/>
</Paper>
</Box>
);
}
export default KnowledgeLogsPage;

View File

@@ -10,7 +10,8 @@ import {
KnowledgeBaseCreate,
KnowledgeBaseDetail,
KnowledgeBaseSetting,
KnowledgeBaseTesting
KnowledgeBaseTesting,
KnowledgeLogsPage
} from '../pages/knowledge';
import {
ModelsSetting,
@@ -40,6 +41,7 @@ const AppRoutes = () => {
<Route path="testing" element={<KnowledgeBaseTesting />} />
<Route index element={<KnowledgeBaseDetail />} />
<Route path="setting" element={<KnowledgeBaseSetting />} />
<Route path="overview" element={<KnowledgeLogsPage />} />
</Route>
</Route>
<Route index element={<KnowledgeBaseList />} />

View File

@@ -47,9 +47,10 @@ export default {
getMeta: `${api_host}/kb/get_meta`,
getKnowledgeBasicInfo: `${api_host}/kb/basic_info`,
// data pipeline log
fetchDataPipelineLog: `${api_host}/kb/list_pipeline_logs`,
fetchDataPipelineLogs: `${api_host}/kb/list_pipeline_logs`,
get_pipeline_detail: `${api_host}/kb/pipeline_log_detail`,
fetchPipelineDatasetLogs: `${api_host}/kb/list_pipeline_dataset_logs`,
// run pipeline
runGraphRag: `${api_host}/kb/run_graphrag`,
traceGraphRag: `${api_host}/kb/trace_graphrag`,
runRaptor: `${api_host}/kb/run_raptor`,

View File

@@ -251,11 +251,11 @@ const knowledgeService = {
// ===== 数据管道 =====
// 获取数据管道日志
getDataPipelineLog: (
getDataPipelineLogs: (
params?: IFetchKnowledgeListRequestParams,
body?: IFetchDocumentListRequestBody
) => {
return request.post(api.fetchDataPipelineLog, body || {}, { params });
return request.post(api.fetchDataPipelineLogs, body || {}, { params });
},
// 获取管道详情