yangdx
commited on
Commit
·
74d6dff
1
Parent(s):
14d6097
feat: add sortable columns to document manager
Browse files- Add sorting capability for ID, created_at and updated_at columns
- Implement ascending/descending sort with visual indicators
- Handle special case for filename sorting in ID column
- Add hover effects on sortable column headers
lightrag_webui/src/features/DocumentManager.tsx
CHANGED
@@ -21,7 +21,7 @@ import { errorMessage } from '@/lib/utils'
|
|
21 |
import { toast } from 'sonner'
|
22 |
import { useBackendState } from '@/stores/state'
|
23 |
|
24 |
-
import { RefreshCwIcon, ActivityIcon } from 'lucide-react'
|
25 |
import { DocStatusResponse } from '@/api/lightrag'
|
26 |
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
|
27 |
|
@@ -99,6 +99,10 @@ const pulseStyle = `
|
|
99 |
}
|
100 |
`;
|
101 |
|
|
|
|
|
|
|
|
|
102 |
export default function DocumentManager() {
|
103 |
const [showPipelineStatus, setShowPipelineStatus] = useState(false)
|
104 |
const { t } = useTranslation()
|
@@ -108,6 +112,52 @@ export default function DocumentManager() {
|
|
108 |
const currentTab = useSettingsStore.use.currentTab()
|
109 |
const showFileName = useSettingsStore.use.showFileName()
|
110 |
const setShowFileName = useSettingsStore.use.setShowFileName()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
111 |
|
112 |
// Store previous status counts
|
113 |
const prevStatusCounts = useRef({
|
@@ -268,6 +318,11 @@ export default function DocumentManager() {
|
|
268 |
return () => clearInterval(interval)
|
269 |
}, [health, fetchDocuments, t, currentTab])
|
270 |
|
|
|
|
|
|
|
|
|
|
|
271 |
return (
|
272 |
<Card className="!rounded-none !overflow-hidden flex flex-col h-full min-h-0">
|
273 |
<CardHeader className="py-2 px-6">
|
@@ -344,18 +399,57 @@ export default function DocumentManager() {
|
|
344 |
<Table className="w-full">
|
345 |
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
|
346 |
<TableRow className="border-b bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75 shadow-[inset_0_-1px_0_rgba(0,0,0,0.1)]">
|
347 |
-
<TableHead
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
348 |
<TableHead>{t('documentPanel.documentManager.columns.summary')}</TableHead>
|
349 |
<TableHead>{t('documentPanel.documentManager.columns.status')}</TableHead>
|
350 |
<TableHead>{t('documentPanel.documentManager.columns.length')}</TableHead>
|
351 |
<TableHead>{t('documentPanel.documentManager.columns.chunks')}</TableHead>
|
352 |
-
<TableHead
|
353 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
354 |
</TableRow>
|
355 |
</TableHeader>
|
356 |
<TableBody className="text-sm overflow-auto">
|
357 |
-
{Object.entries(docs.statuses).
|
358 |
-
|
|
|
|
|
|
|
359 |
<TableRow key={doc.id}>
|
360 |
<TableCell className="truncate font-mono overflow-visible max-w-[250px]">
|
361 |
{showFileName ? (
|
@@ -415,8 +509,8 @@ export default function DocumentManager() {
|
|
415 |
{new Date(doc.updated_at).toLocaleString()}
|
416 |
</TableCell>
|
417 |
</TableRow>
|
418 |
-
))
|
419 |
-
)}
|
420 |
</TableBody>
|
421 |
</Table>
|
422 |
</div>
|
|
|
21 |
import { toast } from 'sonner'
|
22 |
import { useBackendState } from '@/stores/state'
|
23 |
|
24 |
+
import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon } from 'lucide-react'
|
25 |
import { DocStatusResponse } from '@/api/lightrag'
|
26 |
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
|
27 |
|
|
|
99 |
}
|
100 |
`;
|
101 |
|
102 |
+
// Type definitions for sort field and direction
|
103 |
+
type SortField = 'created_at' | 'updated_at' | 'id';
|
104 |
+
type SortDirection = 'asc' | 'desc';
|
105 |
+
|
106 |
export default function DocumentManager() {
|
107 |
const [showPipelineStatus, setShowPipelineStatus] = useState(false)
|
108 |
const { t } = useTranslation()
|
|
|
112 |
const currentTab = useSettingsStore.use.currentTab()
|
113 |
const showFileName = useSettingsStore.use.showFileName()
|
114 |
const setShowFileName = useSettingsStore.use.setShowFileName()
|
115 |
+
|
116 |
+
// Sort state
|
117 |
+
const [sortField, setSortField] = useState<SortField>('updated_at')
|
118 |
+
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
|
119 |
+
|
120 |
+
// Handle sort column click
|
121 |
+
const handleSort = (field: SortField) => {
|
122 |
+
if (sortField === field) {
|
123 |
+
// Toggle sort direction if clicking the same field
|
124 |
+
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc')
|
125 |
+
} else {
|
126 |
+
// Set new sort field with default desc direction
|
127 |
+
setSortField(field)
|
128 |
+
setSortDirection('desc')
|
129 |
+
}
|
130 |
+
}
|
131 |
+
|
132 |
+
// Sort documents based on current sort field and direction
|
133 |
+
const sortDocuments = (documents: DocStatusResponse[]) => {
|
134 |
+
return [...documents].sort((a, b) => {
|
135 |
+
let valueA, valueB;
|
136 |
+
|
137 |
+
// Special handling for ID field based on showFileName setting
|
138 |
+
if (sortField === 'id' && showFileName) {
|
139 |
+
valueA = getDisplayFileName(a);
|
140 |
+
valueB = getDisplayFileName(b);
|
141 |
+
} else if (sortField === 'id') {
|
142 |
+
valueA = a.id;
|
143 |
+
valueB = b.id;
|
144 |
+
} else {
|
145 |
+
// Date fields
|
146 |
+
valueA = new Date(a[sortField]).getTime();
|
147 |
+
valueB = new Date(b[sortField]).getTime();
|
148 |
+
}
|
149 |
+
|
150 |
+
// Apply sort direction
|
151 |
+
const sortMultiplier = sortDirection === 'asc' ? 1 : -1;
|
152 |
+
|
153 |
+
// Compare values
|
154 |
+
if (typeof valueA === 'string' && typeof valueB === 'string') {
|
155 |
+
return sortMultiplier * valueA.localeCompare(valueB);
|
156 |
+
} else {
|
157 |
+
return sortMultiplier * (valueA > valueB ? 1 : valueA < valueB ? -1 : 0);
|
158 |
+
}
|
159 |
+
});
|
160 |
+
}
|
161 |
|
162 |
// Store previous status counts
|
163 |
const prevStatusCounts = useRef({
|
|
|
318 |
return () => clearInterval(interval)
|
319 |
}, [health, fetchDocuments, t, currentTab])
|
320 |
|
321 |
+
// Add dependency on sort state to re-render when sort changes
|
322 |
+
useEffect(() => {
|
323 |
+
// This effect ensures the component re-renders when sort state changes
|
324 |
+
}, [sortField, sortDirection]);
|
325 |
+
|
326 |
return (
|
327 |
<Card className="!rounded-none !overflow-hidden flex flex-col h-full min-h-0">
|
328 |
<CardHeader className="py-2 px-6">
|
|
|
399 |
<Table className="w-full">
|
400 |
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
|
401 |
<TableRow className="border-b bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75 shadow-[inset_0_-1px_0_rgba(0,0,0,0.1)]">
|
402 |
+
<TableHead
|
403 |
+
onClick={() => handleSort('id')}
|
404 |
+
className="cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 select-none"
|
405 |
+
>
|
406 |
+
<div className="flex items-center">
|
407 |
+
{t('documentPanel.documentManager.columns.id')}
|
408 |
+
{sortField === 'id' && (
|
409 |
+
<span className="ml-1">
|
410 |
+
{sortDirection === 'asc' ? <ArrowUpIcon size={14} /> : <ArrowDownIcon size={14} />}
|
411 |
+
</span>
|
412 |
+
)}
|
413 |
+
</div>
|
414 |
+
</TableHead>
|
415 |
<TableHead>{t('documentPanel.documentManager.columns.summary')}</TableHead>
|
416 |
<TableHead>{t('documentPanel.documentManager.columns.status')}</TableHead>
|
417 |
<TableHead>{t('documentPanel.documentManager.columns.length')}</TableHead>
|
418 |
<TableHead>{t('documentPanel.documentManager.columns.chunks')}</TableHead>
|
419 |
+
<TableHead
|
420 |
+
onClick={() => handleSort('created_at')}
|
421 |
+
className="cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 select-none"
|
422 |
+
>
|
423 |
+
<div className="flex items-center">
|
424 |
+
{t('documentPanel.documentManager.columns.created')}
|
425 |
+
{sortField === 'created_at' && (
|
426 |
+
<span className="ml-1">
|
427 |
+
{sortDirection === 'asc' ? <ArrowUpIcon size={14} /> : <ArrowDownIcon size={14} />}
|
428 |
+
</span>
|
429 |
+
)}
|
430 |
+
</div>
|
431 |
+
</TableHead>
|
432 |
+
<TableHead
|
433 |
+
onClick={() => handleSort('updated_at')}
|
434 |
+
className="cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 select-none"
|
435 |
+
>
|
436 |
+
<div className="flex items-center">
|
437 |
+
{t('documentPanel.documentManager.columns.updated')}
|
438 |
+
{sortField === 'updated_at' && (
|
439 |
+
<span className="ml-1">
|
440 |
+
{sortDirection === 'asc' ? <ArrowUpIcon size={14} /> : <ArrowDownIcon size={14} />}
|
441 |
+
</span>
|
442 |
+
)}
|
443 |
+
</div>
|
444 |
+
</TableHead>
|
445 |
</TableRow>
|
446 |
</TableHeader>
|
447 |
<TableBody className="text-sm overflow-auto">
|
448 |
+
{Object.entries(docs.statuses).flatMap(([status, documents]) => {
|
449 |
+
// Apply sorting to documents
|
450 |
+
const sortedDocuments = sortDocuments(documents);
|
451 |
+
|
452 |
+
return sortedDocuments.map(doc => (
|
453 |
<TableRow key={doc.id}>
|
454 |
<TableCell className="truncate font-mono overflow-visible max-w-[250px]">
|
455 |
{showFileName ? (
|
|
|
509 |
{new Date(doc.updated_at).toLocaleString()}
|
510 |
</TableCell>
|
511 |
</TableRow>
|
512 |
+
));
|
513 |
+
})}
|
514 |
</TableBody>
|
515 |
</Table>
|
516 |
</div>
|