feat: add pipeline status monitoring dialog
- Add pipeline status API and types - Create PipelineStatusDialog component with position control - Unify modal overlay style across components
This commit is contained in:
@@ -141,6 +141,20 @@ export type AuthStatusResponse = {
|
||||
api_version?: string
|
||||
}
|
||||
|
||||
export type PipelineStatusResponse = {
|
||||
autoscanned: boolean
|
||||
busy: boolean
|
||||
job_name: string
|
||||
job_start?: string
|
||||
docs: number
|
||||
batchs: number
|
||||
cur_batch: number
|
||||
request_pending: boolean
|
||||
latest_message: string
|
||||
history_messages?: string[]
|
||||
update_status?: Record<string, any>
|
||||
}
|
||||
|
||||
export type LoginResponse = {
|
||||
access_token: string
|
||||
token_type: string
|
||||
@@ -424,6 +438,11 @@ export const getAuthStatus = async (): Promise<AuthStatusResponse> => {
|
||||
}
|
||||
}
|
||||
|
||||
export const getPipelineStatus = async (): Promise<PipelineStatusResponse> => {
|
||||
const response = await axiosInstance.get('/documents/pipeline_status')
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const loginToServer = async (username: string, password: string): Promise<LoginResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append('username', username);
|
||||
|
@@ -5,7 +5,8 @@ import {
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
AlertDialogTitle,
|
||||
AlertDialogOverlay
|
||||
} from '@/components/ui/AlertDialog'
|
||||
import Button from '@/components/ui/Button'
|
||||
import Input from '@/components/ui/Input'
|
||||
@@ -50,6 +51,7 @@ const ApiKeyAlert = ({ open: opened, onOpenChange: setOpened }: ApiKeyAlertProps
|
||||
|
||||
return (
|
||||
<AlertDialog open={opened} onOpenChange={setOpened}>
|
||||
<AlertDialogOverlay className="bg-black/30" />
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t('apiKeyAlert.title')}</AlertDialogTitle>
|
||||
|
196
lightrag_webui/src/components/documents/PipelineStatusDialog.tsx
Normal file
196
lightrag_webui/src/components/documents/PipelineStatusDialog.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { X, AlignLeft, AlignCenter, AlignRight } from 'lucide-react'
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogOverlay
|
||||
} from '@/components/ui/AlertDialog'
|
||||
import Button from '@/components/ui/Button'
|
||||
import { getPipelineStatus, PipelineStatusResponse } from '@/api/lightrag'
|
||||
import { errorMessage } from '@/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type DialogPosition = 'left' | 'center' | 'right'
|
||||
|
||||
interface PipelineStatusDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export default function PipelineStatusDialog({
|
||||
open,
|
||||
onOpenChange
|
||||
}: PipelineStatusDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
const [status, setStatus] = useState<PipelineStatusResponse | null>(null)
|
||||
const [position, setPosition] = useState<DialogPosition>('center')
|
||||
const [isUserScrolled, setIsUserScrolled] = useState(false)
|
||||
const historyRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Reset position when dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setPosition('center')
|
||||
setIsUserScrolled(false)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
// Handle scroll position
|
||||
useEffect(() => {
|
||||
const container = historyRef.current
|
||||
if (!container || isUserScrolled) return
|
||||
|
||||
container.scrollTop = container.scrollHeight
|
||||
}, [status?.history_messages, isUserScrolled])
|
||||
|
||||
const handleScroll = () => {
|
||||
const container = historyRef.current
|
||||
if (!container) return
|
||||
|
||||
const isAtBottom = Math.abs(
|
||||
(container.scrollHeight - container.scrollTop) - container.clientHeight
|
||||
) < 1
|
||||
|
||||
if (isAtBottom) {
|
||||
setIsUserScrolled(false)
|
||||
} else {
|
||||
setIsUserScrolled(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh status every 2 seconds
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
|
||||
const fetchStatus = async () => {
|
||||
try {
|
||||
const data = await getPipelineStatus()
|
||||
setStatus(data)
|
||||
} catch (err) {
|
||||
toast.error(t('documentPanel.pipelineStatus.errors.fetchFailed', { error: errorMessage(err) }))
|
||||
}
|
||||
}
|
||||
|
||||
fetchStatus()
|
||||
const interval = setInterval(fetchStatus, 2000)
|
||||
return () => clearInterval(interval)
|
||||
}, [open, t])
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogOverlay className="bg-black/30" />
|
||||
<AlertDialogContent
|
||||
className={cn(
|
||||
'sm:max-w-[600px] transition-all duration-200',
|
||||
position === 'left' && '!left-4 !translate-x-0',
|
||||
position === 'center' && '!left-1/2 !-translate-x-1/2',
|
||||
position === 'right' && '!right-4 !left-auto !translate-x-0'
|
||||
)}
|
||||
>
|
||||
<AlertDialogHeader className="flex flex-row items-center justify-between">
|
||||
<AlertDialogTitle>
|
||||
{t('documentPanel.pipelineStatus.title')}
|
||||
</AlertDialogTitle>
|
||||
|
||||
{/* Position control buttons and close button */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'h-6 w-6',
|
||||
position === 'left' && 'bg-zinc-200 text-zinc-800 hover:bg-zinc-300 dark:bg-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-600'
|
||||
)}
|
||||
onClick={() => setPosition('left')}
|
||||
>
|
||||
<AlignLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'h-6 w-6',
|
||||
position === 'center' && 'bg-zinc-200 text-zinc-800 hover:bg-zinc-300 dark:bg-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-600'
|
||||
)}
|
||||
onClick={() => setPosition('center')}
|
||||
>
|
||||
<AlignCenter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'h-6 w-6',
|
||||
position === 'right' && 'bg-zinc-200 text-zinc-800 hover:bg-zinc-300 dark:bg-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-600'
|
||||
)}
|
||||
onClick={() => setPosition('right')}
|
||||
>
|
||||
<AlignRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDialogHeader>
|
||||
|
||||
{/* Status Content */}
|
||||
<div className="space-y-4 pt-4">
|
||||
{/* Pipeline Status */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm font-medium">Busy:</div>
|
||||
<div className={`h-2 w-2 rounded-full ${status?.busy ? 'bg-green-500' : 'bg-gray-300'}`} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm font-medium">Request Pending:</div>
|
||||
<div className={`h-2 w-2 rounded-full ${status?.request_pending ? 'bg-green-500' : 'bg-gray-300'}`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Job Information */}
|
||||
<div className="rounded-md border p-3 space-y-2">
|
||||
<div>Job Name: {status?.job_name || '-'}</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Start Time: {status?.job_start ? new Date(status.job_start).toLocaleString() : '-'}</span>
|
||||
<span>Progress: {status ? `${status.cur_batch}/${status.batchs}` : '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Latest Message */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Latest Message:</div>
|
||||
<div className="font-mono text-sm rounded-md bg-zinc-800 text-zinc-100 p-3">
|
||||
{status?.latest_message || '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* History Messages */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">History Messages:</div>
|
||||
<div
|
||||
ref={historyRef}
|
||||
onScroll={handleScroll}
|
||||
className="font-mono text-sm rounded-md bg-zinc-800 text-zinc-100 p-3 h-[20em] overflow-y-auto"
|
||||
>
|
||||
{status?.history_messages?.map((msg, idx) => (
|
||||
<div key={idx}>{msg}</div>
|
||||
)) || '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
@@ -30,7 +30,6 @@ const AlertDialogContent = React.forwardRef<
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
|
@@ -20,8 +20,9 @@ import { errorMessage } from '@/lib/utils'
|
||||
import { toast } from 'sonner'
|
||||
import { useBackendState } from '@/stores/state'
|
||||
|
||||
import { RefreshCwIcon } from 'lucide-react'
|
||||
import { RefreshCwIcon, ActivityIcon } from 'lucide-react'
|
||||
import { DocStatusResponse } from '@/api/lightrag'
|
||||
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
|
||||
|
||||
const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): string => {
|
||||
// Check if file_path exists and is a non-empty string
|
||||
@@ -45,6 +46,7 @@ const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): str
|
||||
};
|
||||
|
||||
export default function DocumentManager() {
|
||||
const [showPipelineStatus, setShowPipelineStatus] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const health = useBackendState.use.health()
|
||||
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
||||
@@ -114,18 +116,33 @@ export default function DocumentManager() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={scanDocuments}
|
||||
side="bottom"
|
||||
tooltip={t('documentPanel.documentManager.scanTooltip')}
|
||||
size="sm"
|
||||
>
|
||||
<RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')}
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={scanDocuments}
|
||||
side="bottom"
|
||||
tooltip={t('documentPanel.documentManager.scanTooltip')}
|
||||
size="sm"
|
||||
>
|
||||
<RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowPipelineStatus(true)}
|
||||
side="bottom"
|
||||
tooltip="View pipeline status"
|
||||
size="sm"
|
||||
>
|
||||
<ActivityIcon /> Pipeline Status
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
<ClearDocumentsDialog />
|
||||
<UploadDocumentsDialog />
|
||||
<PipelineStatusDialog
|
||||
open={showPipelineStatus}
|
||||
onOpenChange={setShowPipelineStatus}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
|
Reference in New Issue
Block a user