Merge pull request #1192 from danielaskdd/pipeline-status
Feat: Add Pipeline status to WebUI
This commit is contained in:
@@ -1 +1 @@
|
|||||||
__api_version__ = "1.2.6"
|
__api_version__ = "1.2.7"
|
||||||
|
@@ -421,6 +421,9 @@ def create_app(args):
|
|||||||
@app.get("/health", dependencies=[Depends(combined_auth)])
|
@app.get("/health", dependencies=[Depends(combined_auth)])
|
||||||
async def get_status():
|
async def get_status():
|
||||||
"""Get current system status"""
|
"""Get current system status"""
|
||||||
|
try:
|
||||||
|
pipeline_status = await get_namespace_data("pipeline_status")
|
||||||
|
|
||||||
username = os.getenv("AUTH_USERNAME")
|
username = os.getenv("AUTH_USERNAME")
|
||||||
password = os.getenv("AUTH_PASSWORD")
|
password = os.getenv("AUTH_PASSWORD")
|
||||||
if not (username and password):
|
if not (username and password):
|
||||||
@@ -451,7 +454,11 @@ def create_app(args):
|
|||||||
"core_version": core_version,
|
"core_version": core_version,
|
||||||
"api_version": __api_version__,
|
"api_version": __api_version__,
|
||||||
"auth_mode": auth_mode,
|
"auth_mode": auth_mode,
|
||||||
|
"pipeline_busy": pipeline_status.get("busy", False),
|
||||||
}
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting health status: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
# Custom StaticFiles class to prevent caching of HTML files
|
# Custom StaticFiles class to prevent caching of HTML files
|
||||||
class NoCacheStaticFiles(StaticFiles):
|
class NoCacheStaticFiles(StaticFiles):
|
||||||
|
@@ -164,61 +164,6 @@ def get_combined_auth_dependency(api_key: Optional[str] = None):
|
|||||||
return combined_dependency
|
return combined_dependency
|
||||||
|
|
||||||
|
|
||||||
def get_api_key_dependency(api_key: Optional[str]):
|
|
||||||
"""
|
|
||||||
Create an API key dependency for route protection.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
api_key (Optional[str]): The API key to validate against.
|
|
||||||
If None, no authentication is required.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Callable: A dependency function that validates the API key.
|
|
||||||
"""
|
|
||||||
# Use global whitelist_patterns and auth_configured variables
|
|
||||||
# whitelist_patterns and auth_configured are already initialized at module level
|
|
||||||
|
|
||||||
# Only calculate api_key_configured as it depends on the function parameter
|
|
||||||
api_key_configured = bool(api_key)
|
|
||||||
|
|
||||||
if not api_key_configured:
|
|
||||||
# If no API key is configured, return a dummy dependency that always succeeds
|
|
||||||
async def no_auth(request: Request = None, **kwargs):
|
|
||||||
return None
|
|
||||||
|
|
||||||
return no_auth
|
|
||||||
|
|
||||||
# If API key is configured, use proper authentication with Security for Swagger UI
|
|
||||||
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
|
||||||
|
|
||||||
async def api_key_auth(
|
|
||||||
request: Request,
|
|
||||||
api_key_header_value: Optional[str] = Security(
|
|
||||||
api_key_header, description="API Key for authentication"
|
|
||||||
),
|
|
||||||
):
|
|
||||||
# Check if request path is in whitelist
|
|
||||||
path = request.url.path
|
|
||||||
for pattern, is_prefix in whitelist_patterns:
|
|
||||||
if (is_prefix and path.startswith(pattern)) or (
|
|
||||||
not is_prefix and path == pattern
|
|
||||||
):
|
|
||||||
return # Whitelist path, allow access
|
|
||||||
|
|
||||||
# Non-whitelist path, validate API key
|
|
||||||
if not api_key_header_value:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTP_403_FORBIDDEN, detail="API Key required"
|
|
||||||
)
|
|
||||||
if api_key_header_value != api_key:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTP_403_FORBIDDEN, detail="Invalid API Key"
|
|
||||||
)
|
|
||||||
return api_key_header_value
|
|
||||||
|
|
||||||
return api_key_auth
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultRAGStorageConfig:
|
class DefaultRAGStorageConfig:
|
||||||
KV_STORAGE = "JsonKVStorage"
|
KV_STORAGE = "JsonKVStorage"
|
||||||
VECTOR_STORAGE = "NanoVectorDBStorage"
|
VECTOR_STORAGE = "NanoVectorDBStorage"
|
||||||
|
File diff suppressed because one or more lines are too long
1
lightrag/api/webui/assets/index-CR7GLPiB.css
generated
1
lightrag/api/webui/assets/index-CR7GLPiB.css
generated
File diff suppressed because one or more lines are too long
1
lightrag/api/webui/assets/index-CbzkrOyx.css
generated
Normal file
1
lightrag/api/webui/assets/index-CbzkrOyx.css
generated
Normal file
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-CSLDmoSD.js"></script>
|
<script type="module" crossorigin src="/webui/assets/index-BX3dHkLt.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/webui/assets/index-CR7GLPiB.css">
|
<link rel="stylesheet" crossorigin href="/webui/assets/index-CbzkrOyx.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
@@ -377,7 +377,7 @@ async def initialize_pipeline_status():
|
|||||||
{
|
{
|
||||||
"autoscanned": False, # Auto-scan started
|
"autoscanned": False, # Auto-scan started
|
||||||
"busy": False, # Control concurrent processes
|
"busy": False, # Control concurrent processes
|
||||||
"job_name": "Default Job", # Current job name (indexing files/indexing texts)
|
"job_name": "-", # Current job name (indexing files/indexing texts)
|
||||||
"job_start": None, # Job start time
|
"job_start": None, # Job start time
|
||||||
"docs": 0, # Total number of documents to be indexed
|
"docs": 0, # Total number of documents to be indexed
|
||||||
"batchs": 0, # Number of batches for processing documents
|
"batchs": 0, # Number of batches for processing documents
|
||||||
|
@@ -845,7 +845,7 @@ class LightRAG:
|
|||||||
pipeline_status.update(
|
pipeline_status.update(
|
||||||
{
|
{
|
||||||
"busy": True,
|
"busy": True,
|
||||||
"job_name": "indexing files",
|
"job_name": "Default Job",
|
||||||
"job_start": datetime.now().isoformat(),
|
"job_start": datetime.now().isoformat(),
|
||||||
"docs": 0,
|
"docs": 0,
|
||||||
"batchs": 0,
|
"batchs": 0,
|
||||||
@@ -884,11 +884,21 @@ class LightRAG:
|
|||||||
logger.info(log_message)
|
logger.info(log_message)
|
||||||
|
|
||||||
# Update pipeline status with current batch information
|
# Update pipeline status with current batch information
|
||||||
pipeline_status["docs"] += len(to_process_docs)
|
pipeline_status["docs"] = len(to_process_docs)
|
||||||
pipeline_status["batchs"] += len(docs_batches)
|
pipeline_status["batchs"] = len(docs_batches)
|
||||||
pipeline_status["latest_message"] = log_message
|
pipeline_status["latest_message"] = log_message
|
||||||
pipeline_status["history_messages"].append(log_message)
|
pipeline_status["history_messages"].append(log_message)
|
||||||
|
|
||||||
|
# Get first document's file path and total count for job name
|
||||||
|
first_doc_id, first_doc = next(iter(to_process_docs.items()))
|
||||||
|
first_doc_path = first_doc.file_path
|
||||||
|
path_prefix = first_doc_path[:20] + (
|
||||||
|
"..." if len(first_doc_path) > 20 else ""
|
||||||
|
)
|
||||||
|
total_files = len(to_process_docs)
|
||||||
|
job_name = f"{path_prefix}[{total_files} files]"
|
||||||
|
pipeline_status["job_name"] = job_name
|
||||||
|
|
||||||
async def process_document(
|
async def process_document(
|
||||||
doc_id: str,
|
doc_id: str,
|
||||||
status_doc: DocProcessingStatus,
|
status_doc: DocProcessingStatus,
|
||||||
|
@@ -45,6 +45,7 @@ export type LightragStatus = {
|
|||||||
core_version?: string
|
core_version?: string
|
||||||
api_version?: string
|
api_version?: string
|
||||||
auth_mode?: 'enabled' | 'disabled'
|
auth_mode?: 'enabled' | 'disabled'
|
||||||
|
pipeline_busy: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LightragDocumentsScanProgress = {
|
export type LightragDocumentsScanProgress = {
|
||||||
@@ -141,6 +142,20 @@ export type AuthStatusResponse = {
|
|||||||
api_version?: string
|
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 = {
|
export type LoginResponse = {
|
||||||
access_token: string
|
access_token: string
|
||||||
token_type: string
|
token_type: string
|
||||||
@@ -424,6 +439,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> => {
|
export const loginToServer = async (username: string, password: string): Promise<LoginResponse> => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('username', username);
|
formData.append('username', username);
|
||||||
|
@@ -31,7 +31,7 @@ export default function ClearDocumentsDialog() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(t('documentPanel.clearDocuments.error', { error: errorMessage(err) }))
|
toast.error(t('documentPanel.clearDocuments.error', { error: errorMessage(err) }))
|
||||||
}
|
}
|
||||||
}, [setOpen])
|
}, [setOpen, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
193
lightrag_webui/src/components/documents/PipelineStatusDialog.tsx
Normal file
193
lightrag_webui/src/components/documents/PipelineStatusDialog.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { AlignLeft, AlignCenter, AlignRight } from 'lucide-react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription
|
||||||
|
} from '@/components/ui/Dialog'
|
||||||
|
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 (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent
|
||||||
|
className={cn(
|
||||||
|
'sm:max-w-[600px] transition-all duration-200 fixed',
|
||||||
|
position === 'left' && '!left-[25%] !translate-x-[-50%] !mx-4',
|
||||||
|
position === 'center' && '!left-1/2 !-translate-x-1/2',
|
||||||
|
position === 'right' && '!left-[75%] !translate-x-[-50%] !mx-4'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
{status?.job_name
|
||||||
|
? `${t('documentPanel.pipelineStatus.jobName')}: ${status.job_name}, ${t('documentPanel.pipelineStatus.progress')}: ${status.cur_batch}/${status.batchs}`
|
||||||
|
: t('documentPanel.pipelineStatus.noActiveJob')
|
||||||
|
}
|
||||||
|
</DialogDescription>
|
||||||
|
<DialogHeader className="flex flex-row items-center">
|
||||||
|
<DialogTitle className="flex-1">
|
||||||
|
{t('documentPanel.pipelineStatus.title')}
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
{/* Position control buttons */}
|
||||||
|
<div className="flex items-center gap-2 mr-8">
|
||||||
|
<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>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* 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">{t('documentPanel.pipelineStatus.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">{t('documentPanel.pipelineStatus.requestPending')}:</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>{t('documentPanel.pipelineStatus.jobName')}: {status?.job_name || '-'}</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>{t('documentPanel.pipelineStatus.startTime')}: {status?.job_start ? new Date(status.job_start).toLocaleString() : '-'}</span>
|
||||||
|
<span>{t('documentPanel.pipelineStatus.progress')}: {status ? `${status.cur_batch}/${status.batchs} ${t('documentPanel.pipelineStatus.unit')}` : '-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Latest Message */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">{t('documentPanel.pipelineStatus.latestMessage')}:</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">{t('documentPanel.pipelineStatus.historyMessages')}:</div>
|
||||||
|
<div
|
||||||
|
ref={historyRef}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
className="font-mono text-sm rounded-md bg-zinc-800 text-zinc-100 p-3 overflow-y-auto min-h-[7.5em] max-h-[40vh]"
|
||||||
|
>
|
||||||
|
{status?.history_messages?.length ? (
|
||||||
|
status.history_messages.map((msg, idx) => (
|
||||||
|
<div key={idx}>{msg}</div>
|
||||||
|
))
|
||||||
|
) : '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
@@ -16,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<AlertDialogPrimitive.Overlay
|
<AlertDialogPrimitive.Overlay
|
||||||
className={cn(
|
className={cn(
|
||||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef<
|
|||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/30',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback, 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'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -20,8 +21,9 @@ 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 } from 'lucide-react'
|
import { RefreshCwIcon, ActivityIcon } from 'lucide-react'
|
||||||
import { DocStatusResponse } from '@/api/lightrag'
|
import { DocStatusResponse } from '@/api/lightrag'
|
||||||
|
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
|
||||||
|
|
||||||
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
|
||||||
@@ -44,19 +46,102 @@ const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): str
|
|||||||
: fileName;
|
: fileName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pulseStyle = `
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
background-color: rgb(255 0 0 / 0.1);
|
||||||
|
border-color: rgb(255 0 0 / 0.2);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: rgb(255 0 0 / 0.2);
|
||||||
|
border-color: rgb(255 0 0 / 0.4);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: rgb(255 0 0 / 0.1);
|
||||||
|
border-color: rgb(255 0 0 / 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .pipeline-busy {
|
||||||
|
animation: dark-pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dark-pulse {
|
||||||
|
0% {
|
||||||
|
background-color: rgb(255 0 0 / 0.2);
|
||||||
|
border-color: rgb(255 0 0 / 0.4);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: rgb(255 0 0 / 0.3);
|
||||||
|
border-color: rgb(255 0 0 / 0.6);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: rgb(255 0 0 / 0.2);
|
||||||
|
border-color: rgb(255 0 0 / 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline-busy {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export default function DocumentManager() {
|
export default function DocumentManager() {
|
||||||
|
const [showPipelineStatus, setShowPipelineStatus] = useState(false)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const health = useBackendState.use.health()
|
const health = useBackendState.use.health()
|
||||||
|
const pipelineBusy = useBackendState.use.pipelineBusy()
|
||||||
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
||||||
const currentTab = useSettingsStore.use.currentTab()
|
const currentTab = useSettingsStore.use.currentTab()
|
||||||
const showFileName = useSettingsStore.use.showFileName()
|
const showFileName = useSettingsStore.use.showFileName()
|
||||||
const setShowFileName = useSettingsStore.use.setShowFileName()
|
const setShowFileName = useSettingsStore.use.setShowFileName()
|
||||||
|
|
||||||
|
// Store previous status counts
|
||||||
|
const prevStatusCounts = useRef({
|
||||||
|
processed: 0,
|
||||||
|
processing: 0,
|
||||||
|
pending: 0,
|
||||||
|
failed: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add pulse style to document
|
||||||
|
useEffect(() => {
|
||||||
|
const style = document.createElement('style')
|
||||||
|
style.textContent = pulseStyle
|
||||||
|
document.head.appendChild(style)
|
||||||
|
return () => {
|
||||||
|
document.head.removeChild(style)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const fetchDocuments = useCallback(async () => {
|
const fetchDocuments = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const docs = await getDocuments()
|
const docs = await getDocuments()
|
||||||
|
|
||||||
|
// Get new status counts (treat null as all zeros)
|
||||||
|
const newStatusCounts = {
|
||||||
|
processed: docs?.statuses?.processed?.length || 0,
|
||||||
|
processing: docs?.statuses?.processing?.length || 0,
|
||||||
|
pending: docs?.statuses?.pending?.length || 0,
|
||||||
|
failed: docs?.statuses?.failed?.length || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any status count has changed
|
||||||
|
const hasStatusCountChange = (Object.keys(newStatusCounts) as Array<keyof typeof newStatusCounts>).some(
|
||||||
|
status => newStatusCounts[status] !== prevStatusCounts.current[status]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Trigger health check if changes detected
|
||||||
|
if (hasStatusCountChange) {
|
||||||
|
useBackendState.getState().check()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update previous status counts
|
||||||
|
prevStatusCounts.current = newStatusCounts
|
||||||
|
|
||||||
|
// Update docs state
|
||||||
if (docs && docs.statuses) {
|
if (docs && docs.statuses) {
|
||||||
// compose all documents count
|
|
||||||
const numDocuments = Object.values(docs.statuses).reduce(
|
const numDocuments = Object.values(docs.statuses).reduce(
|
||||||
(acc, status) => acc + status.length,
|
(acc, status) => acc + status.length,
|
||||||
0
|
0
|
||||||
@@ -113,6 +198,7 @@ export default function DocumentManager() {
|
|||||||
<CardTitle className="text-lg">{t('documentPanel.documentManager.title')}</CardTitle>
|
<CardTitle className="text-lg">{t('documentPanel.documentManager.title')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -123,9 +209,26 @@ export default function DocumentManager() {
|
|||||||
>
|
>
|
||||||
<RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')}
|
<RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowPipelineStatus(true)}
|
||||||
|
side="bottom"
|
||||||
|
tooltip={t('documentPanel.documentManager.pipelineStatusTooltip')}
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
pipelineBusy && 'pipeline-busy'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ActivityIcon /> {t('documentPanel.documentManager.pipelineStatusButton')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<ClearDocumentsDialog />
|
<ClearDocumentsDialog />
|
||||||
<UploadDocumentsDialog />
|
<UploadDocumentsDialog />
|
||||||
|
<PipelineStatusDialog
|
||||||
|
open={showPipelineStatus}
|
||||||
|
onOpenChange={setShowPipelineStatus}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
|
@@ -59,6 +59,8 @@
|
|||||||
"title": "إدارة المستندات",
|
"title": "إدارة المستندات",
|
||||||
"scanButton": "مسح ضوئي",
|
"scanButton": "مسح ضوئي",
|
||||||
"scanTooltip": "مسح المستندات ضوئيًا",
|
"scanTooltip": "مسح المستندات ضوئيًا",
|
||||||
|
"pipelineStatusButton": "حالة خط المعالجة",
|
||||||
|
"pipelineStatusTooltip": "عرض حالة خط المعالجة",
|
||||||
"uploadedTitle": "المستندات المرفوعة",
|
"uploadedTitle": "المستندات المرفوعة",
|
||||||
"uploadedDescription": "قائمة المستندات المرفوعة وحالاتها.",
|
"uploadedDescription": "قائمة المستندات المرفوعة وحالاتها.",
|
||||||
"emptyTitle": "لا توجد مستندات",
|
"emptyTitle": "لا توجد مستندات",
|
||||||
@@ -89,6 +91,20 @@
|
|||||||
"hideButton": "إخفاء",
|
"hideButton": "إخفاء",
|
||||||
"showFileNameTooltip": "عرض اسم الملف",
|
"showFileNameTooltip": "عرض اسم الملف",
|
||||||
"hideFileNameTooltip": "إخفاء اسم الملف"
|
"hideFileNameTooltip": "إخفاء اسم الملف"
|
||||||
|
},
|
||||||
|
"pipelineStatus": {
|
||||||
|
"title": "حالة خط المعالجة",
|
||||||
|
"busy": "خط المعالجة مشغول",
|
||||||
|
"requestPending": "الطلب معلق",
|
||||||
|
"jobName": "اسم المهمة",
|
||||||
|
"startTime": "وقت البدء",
|
||||||
|
"progress": "التقدم",
|
||||||
|
"unit": "دفعة",
|
||||||
|
"latestMessage": "آخر رسالة",
|
||||||
|
"historyMessages": "سجل الرسائل",
|
||||||
|
"errors": {
|
||||||
|
"fetchFailed": "فشل في جلب حالة خط المعالجة\n{{error}}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"graphPanel": {
|
"graphPanel": {
|
||||||
|
@@ -59,6 +59,8 @@
|
|||||||
"title": "Document Management",
|
"title": "Document Management",
|
||||||
"scanButton": "Scan",
|
"scanButton": "Scan",
|
||||||
"scanTooltip": "Scan documents",
|
"scanTooltip": "Scan documents",
|
||||||
|
"pipelineStatusButton": "Pipeline Status",
|
||||||
|
"pipelineStatusTooltip": "View pipeline status",
|
||||||
"uploadedTitle": "Uploaded Documents",
|
"uploadedTitle": "Uploaded Documents",
|
||||||
"uploadedDescription": "List of uploaded documents and their statuses.",
|
"uploadedDescription": "List of uploaded documents and their statuses.",
|
||||||
"emptyTitle": "No Documents",
|
"emptyTitle": "No Documents",
|
||||||
@@ -89,6 +91,20 @@
|
|||||||
"hideButton": "Hide",
|
"hideButton": "Hide",
|
||||||
"showFileNameTooltip": "Show file name",
|
"showFileNameTooltip": "Show file name",
|
||||||
"hideFileNameTooltip": "Hide file name"
|
"hideFileNameTooltip": "Hide file name"
|
||||||
|
},
|
||||||
|
"pipelineStatus": {
|
||||||
|
"title": "Pipeline Status",
|
||||||
|
"busy": "Pipeline Busy",
|
||||||
|
"requestPending": "Reques Pending",
|
||||||
|
"jobName": "Job Name",
|
||||||
|
"startTime": "Start Time",
|
||||||
|
"progress": "Progress",
|
||||||
|
"unit": "batch",
|
||||||
|
"latestMessage": "Latest Message",
|
||||||
|
"historyMessages": "History Message",
|
||||||
|
"errors": {
|
||||||
|
"fetchFailed": "Fail to get pipeline status\n{{error}}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"graphPanel": {
|
"graphPanel": {
|
||||||
@@ -113,7 +129,6 @@
|
|||||||
"save": "Save",
|
"save": "Save",
|
||||||
"refreshLayout": "Refresh Layout"
|
"refreshLayout": "Refresh Layout"
|
||||||
},
|
},
|
||||||
|
|
||||||
"zoomControl": {
|
"zoomControl": {
|
||||||
"zoomIn": "Zoom In",
|
"zoomIn": "Zoom In",
|
||||||
"zoomOut": "Zoom Out",
|
"zoomOut": "Zoom Out",
|
||||||
@@ -121,7 +136,6 @@
|
|||||||
"rotateCamera": "Clockwise Rotate",
|
"rotateCamera": "Clockwise Rotate",
|
||||||
"rotateCameraCounterClockwise": "Counter-Clockwise Rotate"
|
"rotateCameraCounterClockwise": "Counter-Clockwise Rotate"
|
||||||
},
|
},
|
||||||
|
|
||||||
"layoutsControl": {
|
"layoutsControl": {
|
||||||
"startAnimation": "Continue layout animation",
|
"startAnimation": "Continue layout animation",
|
||||||
"stopAnimation": "Stop layout animation",
|
"stopAnimation": "Stop layout animation",
|
||||||
@@ -135,7 +149,6 @@
|
|||||||
"Force Atlas": "Force Atlas"
|
"Force Atlas": "Force Atlas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"fullScreenControl": {
|
"fullScreenControl": {
|
||||||
"fullScreen": "Full Screen",
|
"fullScreen": "Full Screen",
|
||||||
"windowed": "Windowed"
|
"windowed": "Windowed"
|
||||||
@@ -224,7 +237,6 @@
|
|||||||
"querySettings": {
|
"querySettings": {
|
||||||
"parametersTitle": "Parameters",
|
"parametersTitle": "Parameters",
|
||||||
"parametersDescription": "Configure your query parameters",
|
"parametersDescription": "Configure your query parameters",
|
||||||
|
|
||||||
"queryMode": "Query Mode",
|
"queryMode": "Query Mode",
|
||||||
"queryModeTooltip": "Select the retrieval strategy:\n• Naive: Basic search without advanced techniques\n• Local: Context-dependent information retrieval\n• Global: Utilizes global knowledge base\n• Hybrid: Combines local and global retrieval\n• Mix: Integrates knowledge graph with vector retrieval",
|
"queryModeTooltip": "Select the retrieval strategy:\n• Naive: Basic search without advanced techniques\n• Local: Context-dependent information retrieval\n• Global: Utilizes global knowledge base\n• Hybrid: Combines local and global retrieval\n• Mix: Integrates knowledge graph with vector retrieval",
|
||||||
"queryModeOptions": {
|
"queryModeOptions": {
|
||||||
@@ -234,7 +246,6 @@
|
|||||||
"hybrid": "Hybrid",
|
"hybrid": "Hybrid",
|
||||||
"mix": "Mix"
|
"mix": "Mix"
|
||||||
},
|
},
|
||||||
|
|
||||||
"responseFormat": "Response Format",
|
"responseFormat": "Response Format",
|
||||||
"responseFormatTooltip": "Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points",
|
"responseFormatTooltip": "Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points",
|
||||||
"responseFormatOptions": {
|
"responseFormatOptions": {
|
||||||
@@ -242,37 +253,27 @@
|
|||||||
"singleParagraph": "Single Paragraph",
|
"singleParagraph": "Single Paragraph",
|
||||||
"bulletPoints": "Bullet Points"
|
"bulletPoints": "Bullet Points"
|
||||||
},
|
},
|
||||||
|
|
||||||
"topK": "Top K Results",
|
"topK": "Top K Results",
|
||||||
"topKTooltip": "Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode",
|
"topKTooltip": "Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode",
|
||||||
"topKPlaceholder": "Number of results",
|
"topKPlaceholder": "Number of results",
|
||||||
|
|
||||||
"maxTokensTextUnit": "Max Tokens for Text Unit",
|
"maxTokensTextUnit": "Max Tokens for Text Unit",
|
||||||
"maxTokensTextUnitTooltip": "Maximum number of tokens allowed for each retrieved text chunk",
|
"maxTokensTextUnitTooltip": "Maximum number of tokens allowed for each retrieved text chunk",
|
||||||
|
|
||||||
"maxTokensGlobalContext": "Max Tokens for Global Context",
|
"maxTokensGlobalContext": "Max Tokens for Global Context",
|
||||||
"maxTokensGlobalContextTooltip": "Maximum number of tokens allocated for relationship descriptions in global retrieval",
|
"maxTokensGlobalContextTooltip": "Maximum number of tokens allocated for relationship descriptions in global retrieval",
|
||||||
|
|
||||||
"maxTokensLocalContext": "Max Tokens for Local Context",
|
"maxTokensLocalContext": "Max Tokens for Local Context",
|
||||||
"maxTokensLocalContextTooltip": "Maximum number of tokens allocated for entity descriptions in local retrieval",
|
"maxTokensLocalContextTooltip": "Maximum number of tokens allocated for entity descriptions in local retrieval",
|
||||||
|
|
||||||
"historyTurns": "History Turns",
|
"historyTurns": "History Turns",
|
||||||
"historyTurnsTooltip": "Number of complete conversation turns (user-assistant pairs) to consider in the response context",
|
"historyTurnsTooltip": "Number of complete conversation turns (user-assistant pairs) to consider in the response context",
|
||||||
"historyTurnsPlaceholder": "Number of history turns",
|
"historyTurnsPlaceholder": "Number of history turns",
|
||||||
|
|
||||||
"hlKeywords": "High-Level Keywords",
|
"hlKeywords": "High-Level Keywords",
|
||||||
"hlKeywordsTooltip": "List of high-level keywords to prioritize in retrieval. Separate with commas",
|
"hlKeywordsTooltip": "List of high-level keywords to prioritize in retrieval. Separate with commas",
|
||||||
"hlkeywordsPlaceHolder": "Enter keywords",
|
"hlkeywordsPlaceHolder": "Enter keywords",
|
||||||
|
|
||||||
"llKeywords": "Low-Level Keywords",
|
"llKeywords": "Low-Level Keywords",
|
||||||
"llKeywordsTooltip": "List of low-level keywords to refine retrieval focus. Separate with commas",
|
"llKeywordsTooltip": "List of low-level keywords to refine retrieval focus. Separate with commas",
|
||||||
|
|
||||||
"onlyNeedContext": "Only Need Context",
|
"onlyNeedContext": "Only Need Context",
|
||||||
"onlyNeedContextTooltip": "If True, only returns the retrieved context without generating a response",
|
"onlyNeedContextTooltip": "If True, only returns the retrieved context without generating a response",
|
||||||
|
|
||||||
"onlyNeedPrompt": "Only Need Prompt",
|
"onlyNeedPrompt": "Only Need Prompt",
|
||||||
"onlyNeedPromptTooltip": "If True, only returns the generated prompt without producing a response",
|
"onlyNeedPromptTooltip": "If True, only returns the generated prompt without producing a response",
|
||||||
|
|
||||||
"streamResponse": "Stream Response",
|
"streamResponse": "Stream Response",
|
||||||
"streamResponseTooltip": "If True, enables streaming output for real-time responses"
|
"streamResponseTooltip": "If True, enables streaming output for real-time responses"
|
||||||
}
|
}
|
||||||
|
@@ -59,6 +59,8 @@
|
|||||||
"title": "Gestion des documents",
|
"title": "Gestion des documents",
|
||||||
"scanButton": "Scanner",
|
"scanButton": "Scanner",
|
||||||
"scanTooltip": "Scanner les documents",
|
"scanTooltip": "Scanner les documents",
|
||||||
|
"pipelineStatusButton": "État du Pipeline",
|
||||||
|
"pipelineStatusTooltip": "Voir l'état du pipeline",
|
||||||
"uploadedTitle": "Documents téléchargés",
|
"uploadedTitle": "Documents téléchargés",
|
||||||
"uploadedDescription": "Liste des documents téléchargés et leurs statuts.",
|
"uploadedDescription": "Liste des documents téléchargés et leurs statuts.",
|
||||||
"emptyTitle": "Aucun document",
|
"emptyTitle": "Aucun document",
|
||||||
@@ -83,6 +85,25 @@
|
|||||||
"loadFailed": "Échec du chargement des documents\n{{error}}",
|
"loadFailed": "Échec du chargement des documents\n{{error}}",
|
||||||
"scanFailed": "Échec de la numérisation des documents\n{{error}}",
|
"scanFailed": "Échec de la numérisation des documents\n{{error}}",
|
||||||
"scanProgressFailed": "Échec de l'obtention de la progression de la numérisation\n{{error}}"
|
"scanProgressFailed": "Échec de l'obtention de la progression de la numérisation\n{{error}}"
|
||||||
|
},
|
||||||
|
"fileNameLabel": "Nom du fichier",
|
||||||
|
"showButton": "Afficher",
|
||||||
|
"hideButton": "Masquer",
|
||||||
|
"showFileNameTooltip": "Afficher le nom du fichier",
|
||||||
|
"hideFileNameTooltip": "Masquer le nom du fichier"
|
||||||
|
},
|
||||||
|
"pipelineStatus": {
|
||||||
|
"title": "État du Pipeline",
|
||||||
|
"busy": "Pipeline occupé",
|
||||||
|
"requestPending": "Requête en attente",
|
||||||
|
"jobName": "Nom du travail",
|
||||||
|
"startTime": "Heure de début",
|
||||||
|
"progress": "Progression",
|
||||||
|
"unit": "lot",
|
||||||
|
"latestMessage": "Dernier message",
|
||||||
|
"historyMessages": "Historique des messages",
|
||||||
|
"errors": {
|
||||||
|
"fetchFailed": "Échec de la récupération de l'état du pipeline\n{{error}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -59,6 +59,8 @@
|
|||||||
"title": "文档管理",
|
"title": "文档管理",
|
||||||
"scanButton": "扫描",
|
"scanButton": "扫描",
|
||||||
"scanTooltip": "扫描文档",
|
"scanTooltip": "扫描文档",
|
||||||
|
"pipelineStatusButton": "流水线状态",
|
||||||
|
"pipelineStatusTooltip": "查看流水线状态",
|
||||||
"uploadedTitle": "已上传文档",
|
"uploadedTitle": "已上传文档",
|
||||||
"uploadedDescription": "已上传文档列表及其状态",
|
"uploadedDescription": "已上传文档列表及其状态",
|
||||||
"emptyTitle": "无文档",
|
"emptyTitle": "无文档",
|
||||||
@@ -89,6 +91,20 @@
|
|||||||
"hideButton": "隐藏",
|
"hideButton": "隐藏",
|
||||||
"showFileNameTooltip": "显示文件名",
|
"showFileNameTooltip": "显示文件名",
|
||||||
"hideFileNameTooltip": "隐藏文件名"
|
"hideFileNameTooltip": "隐藏文件名"
|
||||||
|
},
|
||||||
|
"pipelineStatus": {
|
||||||
|
"title": "流水线状态",
|
||||||
|
"busy": "流水线忙碌",
|
||||||
|
"requestPending": "待处理请求",
|
||||||
|
"jobName": "作业名称",
|
||||||
|
"startTime": "开始时间",
|
||||||
|
"progress": "进度",
|
||||||
|
"unit": "批",
|
||||||
|
"latestMessage": "最新消息",
|
||||||
|
"historyMessages": "历史消息",
|
||||||
|
"errors": {
|
||||||
|
"fetchFailed": "获取流水线状态失败\n{{error}}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"graphPanel": {
|
"graphPanel": {
|
||||||
|
@@ -6,14 +6,14 @@ interface BackendState {
|
|||||||
health: boolean
|
health: boolean
|
||||||
message: string | null
|
message: string | null
|
||||||
messageTitle: string | null
|
messageTitle: string | null
|
||||||
|
|
||||||
status: LightragStatus | null
|
status: LightragStatus | null
|
||||||
|
|
||||||
lastCheckTime: number
|
lastCheckTime: number
|
||||||
|
pipelineBusy: boolean
|
||||||
|
|
||||||
check: () => Promise<boolean>
|
check: () => Promise<boolean>
|
||||||
clear: () => void
|
clear: () => void
|
||||||
setErrorMessage: (message: string, messageTitle: string) => void
|
setErrorMessage: (message: string, messageTitle: string) => void
|
||||||
|
setPipelineBusy: (busy: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
@@ -34,6 +34,7 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
|
|||||||
messageTitle: null,
|
messageTitle: null,
|
||||||
lastCheckTime: Date.now(),
|
lastCheckTime: Date.now(),
|
||||||
status: null,
|
status: null,
|
||||||
|
pipelineBusy: false,
|
||||||
|
|
||||||
check: async () => {
|
check: async () => {
|
||||||
const health = await checkHealth()
|
const health = await checkHealth()
|
||||||
@@ -51,7 +52,8 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
|
|||||||
message: null,
|
message: null,
|
||||||
messageTitle: null,
|
messageTitle: null,
|
||||||
lastCheckTime: Date.now(),
|
lastCheckTime: Date.now(),
|
||||||
status: health
|
status: health,
|
||||||
|
pipelineBusy: health.pipeline_busy
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -71,6 +73,10 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
|
|||||||
|
|
||||||
setErrorMessage: (message: string, messageTitle: string) => {
|
setErrorMessage: (message: string, messageTitle: string) => {
|
||||||
set({ health: false, message, messageTitle })
|
set({ health: false, message, messageTitle })
|
||||||
|
},
|
||||||
|
|
||||||
|
setPipelineBusy: (busy: boolean) => {
|
||||||
|
set({ pipelineBusy: busy })
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user