Merge pull request #1202 from danielaskdd/sort-file
Feat: Add sortable columns to document manager
This commit is contained in:
File diff suppressed because one or more lines are too long
1
lightrag/api/webui/assets/index-Bwboeqcm.css
generated
Normal file
1
lightrag/api/webui/assets/index-Bwboeqcm.css
generated
Normal file
File diff suppressed because one or more lines are too long
1
lightrag/api/webui/assets/index-CP4Boz-Y.css
generated
1
lightrag/api/webui/assets/index-CP4Boz-Y.css
generated
File diff suppressed because one or more lines are too long
4
lightrag/api/webui/index.html
generated
4
lightrag/api/webui/index.html
generated
@@ -8,8 +8,8 @@
|
|||||||
<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-CZkfsko8.js"></script>
|
<script type="module" crossorigin src="/webui/assets/index-BoPw3HVA.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/webui/assets/index-CP4Boz-Y.css">
|
<link rel="stylesheet" crossorigin href="/webui/assets/index-Bwboeqcm.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
@@ -21,7 +21,7 @@ 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 } from 'lucide-react'
|
import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon } from 'lucide-react'
|
||||||
import { DocStatusResponse } from '@/api/lightrag'
|
import { DocStatusResponse } from '@/api/lightrag'
|
||||||
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
|
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
|
||||||
|
|
||||||
@@ -47,18 +47,41 @@ const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): str
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pulseStyle = `
|
const pulseStyle = `
|
||||||
/* Fixed tooltip styles for small tables */
|
/* Tooltip styles */
|
||||||
.tooltip-fixed {
|
|
||||||
position: fixed !important;
|
|
||||||
z-index: 9999 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Parent container for tooltips */
|
|
||||||
.tooltip-container {
|
.tooltip-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: fixed; /* Use fixed positioning to escape overflow constraints */
|
||||||
|
z-index: 9999; /* Ensure tooltip appears above all other elements */
|
||||||
|
max-width: 600px;
|
||||||
|
white-space: normal;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.95);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
pointer-events: none; /* Prevent tooltip from interfering with mouse events */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .tooltip {
|
||||||
|
background-color: rgba(255, 255, 255, 0.95);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position tooltip helper class */
|
||||||
|
.tooltip-helper {
|
||||||
|
position: absolute;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% {
|
0% {
|
||||||
background-color: rgb(255 0 0 / 0.1);
|
background-color: rgb(255 0 0 / 0.1);
|
||||||
@@ -99,6 +122,10 @@ const pulseStyle = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Type definitions for sort field and direction
|
||||||
|
type SortField = 'created_at' | 'updated_at' | 'id';
|
||||||
|
type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
export default function DocumentManager() {
|
export default function DocumentManager() {
|
||||||
const [showPipelineStatus, setShowPipelineStatus] = useState(false)
|
const [showPipelineStatus, setShowPipelineStatus] = useState(false)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -109,6 +136,52 @@ export default function DocumentManager() {
|
|||||||
const showFileName = useSettingsStore.use.showFileName()
|
const showFileName = useSettingsStore.use.showFileName()
|
||||||
const setShowFileName = useSettingsStore.use.setShowFileName()
|
const setShowFileName = useSettingsStore.use.setShowFileName()
|
||||||
|
|
||||||
|
// Sort state
|
||||||
|
const [sortField, setSortField] = useState<SortField>('updated_at')
|
||||||
|
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
|
||||||
|
|
||||||
|
// Handle sort column click
|
||||||
|
const handleSort = (field: SortField) => {
|
||||||
|
if (sortField === field) {
|
||||||
|
// Toggle sort direction if clicking the same field
|
||||||
|
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc')
|
||||||
|
} else {
|
||||||
|
// Set new sort field with default desc direction
|
||||||
|
setSortField(field)
|
||||||
|
setSortDirection('desc')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort documents based on current sort field and direction
|
||||||
|
const sortDocuments = (documents: DocStatusResponse[]) => {
|
||||||
|
return [...documents].sort((a, b) => {
|
||||||
|
let valueA, valueB;
|
||||||
|
|
||||||
|
// Special handling for ID field based on showFileName setting
|
||||||
|
if (sortField === 'id' && showFileName) {
|
||||||
|
valueA = getDisplayFileName(a);
|
||||||
|
valueB = getDisplayFileName(b);
|
||||||
|
} else if (sortField === 'id') {
|
||||||
|
valueA = a.id;
|
||||||
|
valueB = b.id;
|
||||||
|
} else {
|
||||||
|
// Date fields
|
||||||
|
valueA = new Date(a[sortField]).getTime();
|
||||||
|
valueB = new Date(b[sortField]).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sort direction
|
||||||
|
const sortMultiplier = sortDirection === 'asc' ? 1 : -1;
|
||||||
|
|
||||||
|
// Compare values
|
||||||
|
if (typeof valueA === 'string' && typeof valueB === 'string') {
|
||||||
|
return sortMultiplier * valueA.localeCompare(valueB);
|
||||||
|
} else {
|
||||||
|
return sortMultiplier * (valueA > valueB ? 1 : valueA < valueB ? -1 : 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Store previous status counts
|
// Store previous status counts
|
||||||
const prevStatusCounts = useRef({
|
const prevStatusCounts = useRef({
|
||||||
processed: 0,
|
processed: 0,
|
||||||
@@ -130,64 +203,47 @@ export default function DocumentManager() {
|
|||||||
// Reference to the card content element
|
// Reference to the card content element
|
||||||
const cardContentRef = useRef<HTMLDivElement>(null);
|
const cardContentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Add tooltip position adjustment based on mouse position
|
// Add tooltip position adjustment for fixed positioning
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!docs) return;
|
if (!docs) return;
|
||||||
|
|
||||||
// Function to handle mouse movement - throttled to reduce layout calculations
|
// Function to position tooltips
|
||||||
let lastExecution = 0;
|
const positionTooltips = () => {
|
||||||
const throttleInterval = 50; // ms
|
// Get all tooltip containers
|
||||||
|
const containers = document.querySelectorAll<HTMLElement>('.tooltip-container');
|
||||||
|
|
||||||
const handleMouseMove = () => {
|
containers.forEach(container => {
|
||||||
const now = Date.now();
|
const tooltip = container.querySelector<HTMLElement>('.tooltip');
|
||||||
if (now - lastExecution < throttleInterval) return;
|
if (!tooltip) return;
|
||||||
lastExecution = now;
|
|
||||||
|
|
||||||
const cardContent = cardContentRef.current;
|
// Only position visible tooltips
|
||||||
if (!cardContent) return;
|
if (getComputedStyle(tooltip).visibility === 'hidden') return;
|
||||||
|
|
||||||
// Get all visible tooltips
|
// Get container position
|
||||||
const visibleTooltips = document.querySelectorAll<HTMLElement>('.group:hover > div[class*="invisible group-hover:visible absolute"]');
|
const rect = container.getBoundingClientRect();
|
||||||
if (visibleTooltips.length === 0) return;
|
|
||||||
|
|
||||||
visibleTooltips.forEach(tooltip => {
|
// Position tooltip above the container
|
||||||
// Get the parent element that triggered the tooltip
|
tooltip.style.left = `${rect.left}px`;
|
||||||
const triggerElement = tooltip.parentElement;
|
tooltip.style.top = `${rect.top - 5}px`;
|
||||||
if (!triggerElement) return;
|
tooltip.style.transform = 'translateY(-100%)';
|
||||||
|
|
||||||
const triggerRect = triggerElement.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Use fixed positioning for all tooltips
|
|
||||||
tooltip.classList.add('tooltip-fixed');
|
|
||||||
|
|
||||||
// Calculate position based on trigger element
|
|
||||||
const tooltipHeight = tooltip.offsetHeight;
|
|
||||||
const viewportHeight = window.innerHeight;
|
|
||||||
|
|
||||||
// Check if tooltip would go off the bottom of the viewport
|
|
||||||
const wouldOverflowBottom = triggerRect.bottom + tooltipHeight + 5 > viewportHeight;
|
|
||||||
|
|
||||||
if (wouldOverflowBottom) {
|
|
||||||
// Position above the trigger
|
|
||||||
tooltip.style.top = `${triggerRect.top - tooltipHeight - 5}px`;
|
|
||||||
tooltip.style.bottom = 'auto';
|
|
||||||
} else {
|
|
||||||
// Position below the trigger
|
|
||||||
tooltip.style.top = `${triggerRect.bottom + 5}px`;
|
|
||||||
tooltip.style.bottom = 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Horizontal positioning
|
|
||||||
tooltip.style.left = `${triggerRect.left}px`;
|
|
||||||
tooltip.style.maxWidth = '600px';
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add mouse move listener to the document
|
// Set up event listeners
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
const handleMouseOver = (e: MouseEvent) => {
|
||||||
|
// Check if target or its parent is a tooltip container
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const container = target.closest('.tooltip-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Small delay to ensure tooltip is visible before positioning
|
||||||
|
setTimeout(positionTooltips, 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mouseover', handleMouseOver);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener('mouseover', handleMouseOver);
|
||||||
};
|
};
|
||||||
}, [docs]);
|
}, [docs]);
|
||||||
|
|
||||||
@@ -268,6 +324,11 @@ export default function DocumentManager() {
|
|||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [health, fetchDocuments, t, currentTab])
|
}, [health, fetchDocuments, t, currentTab])
|
||||||
|
|
||||||
|
// Add dependency on sort state to re-render when sort changes
|
||||||
|
useEffect(() => {
|
||||||
|
// This effect ensures the component re-renders when sort state changes
|
||||||
|
}, [sortField, sortDirection]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="!rounded-none !overflow-hidden flex flex-col h-full min-h-0">
|
<Card className="!rounded-none !overflow-hidden flex flex-col h-full min-h-0">
|
||||||
<CardHeader className="py-2 px-6">
|
<CardHeader className="py-2 px-6">
|
||||||
@@ -340,22 +401,61 @@ export default function DocumentManager() {
|
|||||||
)}
|
)}
|
||||||
{docs && (
|
{docs && (
|
||||||
<div className="absolute inset-0 flex flex-col p-0">
|
<div className="absolute inset-0 flex flex-col p-0">
|
||||||
<div className="w-full h-full flex flex-col rounded-lg border border-gray-200 dark:border-gray-700 overflow-auto">
|
<div className="w-full h-full flex flex-col border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||||
<Table className="w-full">
|
<Table className="w-full">
|
||||||
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
|
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
|
||||||
<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)]">
|
<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)]">
|
||||||
<TableHead>{t('documentPanel.documentManager.columns.id')}</TableHead>
|
<TableHead
|
||||||
|
onClick={() => handleSort('id')}
|
||||||
|
className="cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-800 select-none"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{t('documentPanel.documentManager.columns.id')}
|
||||||
|
{sortField === 'id' && (
|
||||||
|
<span className="ml-1">
|
||||||
|
{sortDirection === 'asc' ? <ArrowUpIcon size={14} /> : <ArrowDownIcon size={14} />}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableHead>
|
||||||
<TableHead>{t('documentPanel.documentManager.columns.summary')}</TableHead>
|
<TableHead>{t('documentPanel.documentManager.columns.summary')}</TableHead>
|
||||||
<TableHead>{t('documentPanel.documentManager.columns.status')}</TableHead>
|
<TableHead>{t('documentPanel.documentManager.columns.status')}</TableHead>
|
||||||
<TableHead>{t('documentPanel.documentManager.columns.length')}</TableHead>
|
<TableHead>{t('documentPanel.documentManager.columns.length')}</TableHead>
|
||||||
<TableHead>{t('documentPanel.documentManager.columns.chunks')}</TableHead>
|
<TableHead>{t('documentPanel.documentManager.columns.chunks')}</TableHead>
|
||||||
<TableHead>{t('documentPanel.documentManager.columns.created')}</TableHead>
|
<TableHead
|
||||||
<TableHead>{t('documentPanel.documentManager.columns.updated')}</TableHead>
|
onClick={() => handleSort('created_at')}
|
||||||
|
className="cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-800 select-none"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{t('documentPanel.documentManager.columns.created')}
|
||||||
|
{sortField === 'created_at' && (
|
||||||
|
<span className="ml-1">
|
||||||
|
{sortDirection === 'asc' ? <ArrowUpIcon size={14} /> : <ArrowDownIcon size={14} />}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead
|
||||||
|
onClick={() => handleSort('updated_at')}
|
||||||
|
className="cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-800 select-none"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{t('documentPanel.documentManager.columns.updated')}
|
||||||
|
{sortField === 'updated_at' && (
|
||||||
|
<span className="ml-1">
|
||||||
|
{sortDirection === 'asc' ? <ArrowUpIcon size={14} /> : <ArrowDownIcon size={14} />}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody className="text-sm overflow-auto">
|
<TableBody className="text-sm overflow-auto">
|
||||||
{Object.entries(docs.statuses).map(([status, documents]) =>
|
{Object.entries(docs.statuses).flatMap(([status, documents]) => {
|
||||||
documents.map((doc) => (
|
// Apply sorting to documents
|
||||||
|
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 ? (
|
||||||
@@ -364,7 +464,7 @@ export default function DocumentManager() {
|
|||||||
<div className="truncate">
|
<div className="truncate">
|
||||||
{getDisplayFileName(doc, 30)}
|
{getDisplayFileName(doc, 30)}
|
||||||
</div>
|
</div>
|
||||||
<div className="invisible group-hover:visible absolute z-[9999] mt-1 max-w-[600px] whitespace-normal break-all rounded-md bg-black/95 px-3 py-2 text-sm text-white shadow-lg dark:bg-white/95 dark:text-black">
|
<div className="invisible group-hover:visible tooltip">
|
||||||
{doc.file_path}
|
{doc.file_path}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -375,7 +475,7 @@ export default function DocumentManager() {
|
|||||||
<div className="truncate">
|
<div className="truncate">
|
||||||
{doc.id}
|
{doc.id}
|
||||||
</div>
|
</div>
|
||||||
<div className="invisible group-hover:visible absolute z-[9999] mt-1 max-w-[600px] whitespace-normal break-all rounded-md bg-black/95 px-3 py-2 text-sm text-white shadow-lg dark:bg-white/95 dark:text-black">
|
<div className="invisible group-hover:visible tooltip">
|
||||||
{doc.file_path}
|
{doc.file_path}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -386,7 +486,7 @@ export default function DocumentManager() {
|
|||||||
<div className="truncate">
|
<div className="truncate">
|
||||||
{doc.content_summary}
|
{doc.content_summary}
|
||||||
</div>
|
</div>
|
||||||
<div className="invisible group-hover:visible absolute z-[9999] mt-1 max-w-[600px] whitespace-normal break-all rounded-md bg-black/95 px-3 py-2 text-sm text-white shadow-lg dark:bg-white/95 dark:text-black">
|
<div className="invisible group-hover:visible tooltip">
|
||||||
{doc.content_summary}
|
{doc.content_summary}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -415,8 +515,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>
|
||||||
|
Reference in New Issue
Block a user