feat(DocumentManager): Add document status filter feature, support filtering documents by status
This commit is contained in:
File diff suppressed because one or more lines are too long
2
lightrag/api/webui/index.html
generated
2
lightrag/api/webui/index.html
generated
@@ -8,7 +8,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="logo.png" />
|
<link rel="icon" type="image/svg+xml" href="logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Lightrag</title>
|
<title>Lightrag</title>
|
||||||
<script type="module" crossorigin src="/webui/assets/index-raheqJeu.js"></script>
|
<script type="module" crossorigin src="/webui/assets/index-CfYQAXql.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/webui/assets/index-CD5HxTy1.css">
|
<link rel="stylesheet" crossorigin href="/webui/assets/index-CD5HxTy1.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
@@ -16,15 +16,17 @@ import EmptyCard from '@/components/ui/EmptyCard'
|
|||||||
import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog'
|
import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog'
|
||||||
import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog'
|
import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog'
|
||||||
|
|
||||||
import { getDocuments, scanNewDocuments, DocsStatusesResponse } from '@/api/lightrag'
|
import { getDocuments, scanNewDocuments, DocsStatusesResponse, DocStatus, DocStatusResponse } from '@/api/lightrag'
|
||||||
import { errorMessage } from '@/lib/utils'
|
import { errorMessage } from '@/lib/utils'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useBackendState } from '@/stores/state'
|
import { useBackendState } from '@/stores/state'
|
||||||
|
|
||||||
import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon } from 'lucide-react'
|
import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon, FilterIcon } from 'lucide-react'
|
||||||
import { DocStatusResponse } from '@/api/lightrag'
|
|
||||||
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
|
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
|
||||||
|
|
||||||
|
type StatusFilter = DocStatus | 'all';
|
||||||
|
|
||||||
|
|
||||||
const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): string => {
|
const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): string => {
|
||||||
// Check if file_path exists and is a non-empty string
|
// Check if file_path exists and is a non-empty string
|
||||||
if (!doc.file_path || typeof doc.file_path !== 'string' || doc.file_path.trim() === '') {
|
if (!doc.file_path || typeof doc.file_path !== 'string' || doc.file_path.trim() === '') {
|
||||||
@@ -148,6 +150,10 @@ export default function DocumentManager() {
|
|||||||
const [sortField, setSortField] = useState<SortField>('updated_at')
|
const [sortField, setSortField] = useState<SortField>('updated_at')
|
||||||
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
|
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
|
||||||
|
|
||||||
|
// State for document status filter
|
||||||
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
|
||||||
|
|
||||||
|
|
||||||
// Handle sort column click
|
// Handle sort column click
|
||||||
const handleSort = (field: SortField) => {
|
const handleSort = (field: SortField) => {
|
||||||
if (sortField === field) {
|
if (sortField === field) {
|
||||||
@@ -159,36 +165,54 @@ export default function DocumentManager() {
|
|||||||
setSortDirection('desc')
|
setSortDirection('desc')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const filteredAndSortedDocs = useMemo(() => {
|
||||||
|
if (!docs) return null;
|
||||||
|
|
||||||
// Sort documents based on current sort field and direction
|
let filteredDocs = { ...docs };
|
||||||
const sortDocuments = (documents: DocStatusResponse[]) => {
|
|
||||||
return [...documents].sort((a, b) => {
|
|
||||||
let valueA, valueB;
|
|
||||||
|
|
||||||
// Special handling for ID field based on showFileName setting
|
if (statusFilter !== 'all') {
|
||||||
if (sortField === 'id' && showFileName) {
|
filteredDocs = {
|
||||||
valueA = getDisplayFileName(a);
|
...docs,
|
||||||
valueB = getDisplayFileName(b);
|
statuses: {
|
||||||
} else if (sortField === 'id') {
|
pending: [],
|
||||||
valueA = a.id;
|
processing: [],
|
||||||
valueB = b.id;
|
processed: [],
|
||||||
} else {
|
failed: [],
|
||||||
// Date fields
|
[statusFilter]: docs.statuses[statusFilter] || []
|
||||||
valueA = new Date(a[sortField]).getTime();
|
}
|
||||||
valueB = new Date(b[sortField]).getTime();
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply sort direction
|
if (!sortField || !sortDirection) return filteredDocs;
|
||||||
const sortMultiplier = sortDirection === 'asc' ? 1 : -1;
|
|
||||||
|
|
||||||
// Compare values
|
const sortedStatuses = Object.entries(filteredDocs.statuses).reduce((acc, [status, documents]) => {
|
||||||
if (typeof valueA === 'string' && typeof valueB === 'string') {
|
const sortedDocuments = [...documents].sort((a, b) => {
|
||||||
return sortMultiplier * valueA.localeCompare(valueB);
|
if (sortDirection === 'asc') {
|
||||||
|
return (a[sortField] ?? 0) > (b[sortField] ?? 0) ? 1 : -1;
|
||||||
} else {
|
} else {
|
||||||
return sortMultiplier * (valueA > valueB ? 1 : valueA < valueB ? -1 : 0);
|
return (a[sortField] ?? 0) < (b[sortField] ?? 0) ? 1 : -1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
acc[status as DocStatus] = sortedDocuments;
|
||||||
|
return acc;
|
||||||
|
}, {} as DocsStatusesResponse['statuses']);
|
||||||
|
|
||||||
|
return { ...filteredDocs, statuses: sortedStatuses };
|
||||||
|
}, [docs, sortField, sortDirection, statusFilter]);
|
||||||
|
|
||||||
|
// Calculate document counts for each status
|
||||||
|
const documentCounts = useMemo(() => {
|
||||||
|
if (!docs) return { all: 0 } as Record<string, number>;
|
||||||
|
|
||||||
|
const counts: Record<string, number> = { all: 0 };
|
||||||
|
|
||||||
|
Object.entries(docs.statuses).forEach(([status, documents]) => {
|
||||||
|
counts[status as DocStatus] = documents.length;
|
||||||
|
counts.all += documents.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
return counts;
|
||||||
|
}, [docs]);
|
||||||
|
|
||||||
// Store previous status counts
|
// Store previous status counts
|
||||||
const prevStatusCounts = useRef({
|
const prevStatusCounts = useRef({
|
||||||
@@ -398,6 +422,50 @@ export default function DocumentManager() {
|
|||||||
<CardHeader className="flex-none py-2 px-4">
|
<CardHeader className="flex-none py-2 px-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<CardTitle>{t('documentPanel.documentManager.uploadedTitle')}</CardTitle>
|
<CardTitle>{t('documentPanel.documentManager.uploadedTitle')}</CardTitle>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FilterIcon className="h-4 w-4" />
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={statusFilter === 'all' ? 'secondary' : 'outline'}
|
||||||
|
onClick={() => setStatusFilter('all')}
|
||||||
|
>
|
||||||
|
{t('documentPanel.documentManager.status.all')} ({documentCounts.all})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={statusFilter === 'processed' ? 'secondary' : 'outline'}
|
||||||
|
onClick={() => setStatusFilter('processed')}
|
||||||
|
className={documentCounts.processed > 0 ? 'text-green-600' : 'text-gray-500'}
|
||||||
|
>
|
||||||
|
{t('documentPanel.documentManager.status.completed')} ({documentCounts.processed || 0})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={statusFilter === 'processing' ? 'secondary' : 'outline'}
|
||||||
|
onClick={() => setStatusFilter('processing')}
|
||||||
|
className={documentCounts.processing > 0 ? 'text-blue-600' : 'text-gray-500'}
|
||||||
|
>
|
||||||
|
{t('documentPanel.documentManager.status.processing')} ({documentCounts.processing || 0})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={statusFilter === 'pending' ? 'secondary' : 'outline'}
|
||||||
|
onClick={() => setStatusFilter('pending')}
|
||||||
|
className={documentCounts.pending > 0 ? 'text-yellow-600' : 'text-gray-500'}
|
||||||
|
>
|
||||||
|
{t('documentPanel.documentManager.status.pending')} ({documentCounts.pending || 0})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={statusFilter === 'failed' ? 'secondary' : 'outline'}
|
||||||
|
onClick={() => setStatusFilter('failed')}
|
||||||
|
className={documentCounts.failed > 0 ? 'text-red-600' : 'text-gray-500'}
|
||||||
|
>
|
||||||
|
{t('documentPanel.documentManager.status.failed')} ({documentCounts.failed || 0})
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-gray-500">{t('documentPanel.documentManager.fileNameLabel')}</span>
|
<span className="text-sm text-gray-500">{t('documentPanel.documentManager.fileNameLabel')}</span>
|
||||||
<Button
|
<Button
|
||||||
@@ -477,11 +545,8 @@ export default function DocumentManager() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody className="text-sm overflow-auto">
|
<TableBody className="text-sm overflow-auto">
|
||||||
{Object.entries(docs.statuses).flatMap(([status, documents]) => {
|
{filteredAndSortedDocs?.statuses && Object.entries(filteredAndSortedDocs.statuses).flatMap(([status, documents]) =>
|
||||||
// Apply sorting to documents
|
documents.map((doc) => (
|
||||||
const sortedDocuments = sortDocuments(documents);
|
|
||||||
|
|
||||||
return sortedDocuments.map(doc => (
|
|
||||||
<TableRow key={doc.id}>
|
<TableRow key={doc.id}>
|
||||||
<TableCell className="truncate font-mono overflow-visible max-w-[250px]">
|
<TableCell className="truncate font-mono overflow-visible max-w-[250px]">
|
||||||
{showFileName ? (
|
{showFileName ? (
|
||||||
@@ -541,8 +606,8 @@ export default function DocumentManager() {
|
|||||||
{new Date(doc.updated_at).toLocaleString()}
|
{new Date(doc.updated_at).toLocaleString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
));
|
)))
|
||||||
})}
|
}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -95,6 +95,7 @@
|
|||||||
"metadata": "البيانات الوصفية"
|
"metadata": "البيانات الوصفية"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
|
"all": "الكل",
|
||||||
"completed": "مكتمل",
|
"completed": "مكتمل",
|
||||||
"processing": "قيد المعالجة",
|
"processing": "قيد المعالجة",
|
||||||
"pending": "معلق",
|
"pending": "معلق",
|
||||||
|
@@ -95,6 +95,7 @@
|
|||||||
"metadata": "Metadata"
|
"metadata": "Metadata"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
|
"all": "All",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"processing": "Processing",
|
"processing": "Processing",
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
|
@@ -95,6 +95,7 @@
|
|||||||
"metadata": "Métadonnées"
|
"metadata": "Métadonnées"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
|
"all": "Tous",
|
||||||
"completed": "Terminé",
|
"completed": "Terminé",
|
||||||
"processing": "En traitement",
|
"processing": "En traitement",
|
||||||
"pending": "En attente",
|
"pending": "En attente",
|
||||||
|
@@ -95,6 +95,7 @@
|
|||||||
"metadata": "元数据"
|
"metadata": "元数据"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
|
"all": "全部",
|
||||||
"completed": "已完成",
|
"completed": "已完成",
|
||||||
"processing": "处理中",
|
"processing": "处理中",
|
||||||
"pending": "等待中",
|
"pending": "等待中",
|
||||||
|
Reference in New Issue
Block a user