implement backend health check and alert system

This commit is contained in:
ArnoChen
2025-02-10 23:33:51 +08:00
parent a08f59f663
commit 7d6ffbbd87
11 changed files with 241 additions and 7 deletions

View File

@@ -1,12 +1,18 @@
import ThemeProvider from '@/components/ThemeProvider' import ThemeProvider from '@/components/ThemeProvider'
import BackendMessageAlert from '@/components/BackendMessageAlert'
import { GraphViewer } from '@/GraphViewer' import { GraphViewer } from '@/GraphViewer'
import { cn } from '@/lib/utils'
import { useBackendState } from '@/stores/state'
function App() { function App() {
const health = useBackendState.use.health()
return ( return (
<ThemeProvider defaultTheme="system" storageKey="lightrag-viewer-webui-theme"> <ThemeProvider>
<div className="h-screen w-screen"> <div className={cn('h-screen w-screen', !health && 'pointer-events-none')}>
<GraphViewer /> <GraphViewer />
</div> </div>
{!health && <BackendMessageAlert />}
</ThemeProvider> </ThemeProvider>
) )
} }

View File

@@ -153,7 +153,7 @@ export const GraphViewer = () => {
/> />
</div> </div>
<div className="bg-background/20 absolute bottom-2 left-2 flex flex-col rounded-xl border-2 backdrop-blur-lg"> <div className="bg-background/60 absolute bottom-2 left-2 flex flex-col rounded-xl border-2 backdrop-blur-lg">
<Settings /> <Settings />
<ZoomControl /> <ZoomControl />
<LayoutsControl /> <LayoutsControl />

View File

@@ -0,0 +1,97 @@
import { backendBaseUrl } from '@/lib/constants'
export type LightragNodeType = {
id: string
labels: string[]
properties: Record<string, any>
}
export type LightragEdgeType = {
id: string
source: string
target: string
type: string
properties: Record<string, any>
}
export type LightragGraphType = {
nodes: LightragNodeType[]
edges: LightragEdgeType[]
}
export type LightragStatus = {
status: 'healthy'
working_directory: string
input_directory: string
indexed_files: string[]
indexed_files_count: number
configuration: {
llm_binding: string
llm_binding_host: string
llm_model: string
embedding_binding: string
embedding_binding_host: string
embedding_model: string
max_tokens: number
kv_storage: string
doc_status_storage: string
graph_storage: string
vector_storage: string
}
}
export type LightragDocumentsScanProgress = {
is_scanning: boolean
current_file: string
indexed_count: number
total_files: number
progress: number
}
const checkResponse = (response: Response) => {
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText} ${response.url}`)
}
}
export const queryGraphs = async (label: string): Promise<LightragGraphType> => {
const response = await fetch(backendBaseUrl + `/graphs?label=${label}`)
checkResponse(response)
return await response.json()
}
export const getGraphLabels = async (): Promise<string[]> => {
const response = await fetch(backendBaseUrl + '/graph/label/list')
checkResponse(response)
return await response.json()
}
export const checkHealth = async (): Promise<
LightragStatus | { status: 'error'; message: string }
> => {
try {
const response = await fetch(backendBaseUrl + '/health')
if (!response.ok) {
return {
status: 'error',
message: `Health check failed. Service is currently unavailable.\n${response.status} ${response.statusText} ${response.url}`
}
}
return await response.json()
} catch (e) {
return {
status: 'error',
message: `${e}`
}
}
}
export const getDocuments = async (): Promise<string[]> => {
const response = await fetch(backendBaseUrl + '/documents')
return await response.json()
}
export const getDocumentsScanProgress = async (): Promise<LightragDocumentsScanProgress> => {
const response = await fetch(backendBaseUrl + '/documents/scan-progress')
return await response.json()
}

View File

@@ -0,0 +1,22 @@
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert'
import { useBackendState } from '@/stores/state'
import { AlertCircle } from 'lucide-react'
const BackendMessageAlert = () => {
const health = useBackendState.use.health()
const message = useBackendState.use.message()
const messageTitle = useBackendState.use.messageTitle()
return (
<Alert
variant={health ? 'default' : 'destructive'}
className="absolute top-1/2 left-1/2 w-auto -translate-x-1/2 -translate-y-1/2 transform"
>
{!health && <AlertCircle className="h-4 w-4" />}
<AlertTitle>{messageTitle}</AlertTitle>
<AlertDescription>{message}</AlertDescription>
</Alert>
)
}
export default BackendMessageAlert

View File

@@ -70,7 +70,7 @@ export const GraphSearchInput = ({
return ( return (
<AsyncSelect <AsyncSelect
className="bg-background/20 w-52 rounded-xl border-1 opacity-60 backdrop-blur-lg transition-opacity hover:opacity-100" className="bg-background/60 w-52 rounded-xl border-1 opacity-60 backdrop-blur-lg transition-opacity hover:opacity-100"
fetcher={loadOptions} fetcher={loadOptions}
renderOption={OptionComponent} renderOption={OptionComponent}
getOptionValue={(item) => item.id} getOptionValue={(item) => item.id}

View File

@@ -59,7 +59,7 @@ const PropertiesView = () => {
return <></> return <></>
} }
return ( return (
<div className="bg-background/20 max-w-sm rounded-xl border-2 p-2 backdrop-blur-lg"> <div className="bg-background/80 max-w-sm rounded-xl border-2 p-2 backdrop-blur-lg">
{currentType == 'node' ? ( {currentType == 'node' ? (
<NodePropertiesView node={currentElement as any} /> <NodePropertiesView node={currentElement as any} />
) : ( ) : (

View File

@@ -7,6 +7,8 @@ import { useSettingsStore } from '@/stores/settings'
import { SettingsIcon } from 'lucide-react' import { SettingsIcon } from 'lucide-react'
import * as Api from '@/api/lightrag'
/** /**
* Component that displays a checkbox with a label. * Component that displays a checkbox with a label.
*/ */
@@ -95,6 +97,13 @@ export default function Settings() {
onCheckedChange={setShowEdgeLabel} onCheckedChange={setShowEdgeLabel}
label="Show Edge Label" label="Show Edge Label"
/> />
<Button
onClick={async () => {
console.log(Api.checkHealth())
}}
>
Test Api
</Button>
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>

View File

@@ -0,0 +1,49 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const alertVariants = cva(
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive'
}
},
defaultVariants: {
variant: 'default'
}
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
))
Alert.displayName = 'Alert'
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn('mb-1 leading-none font-medium tracking-tight', className)}
{...props}
/>
)
)
AlertTitle.displayName = 'AlertTitle'
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
))
AlertDescription.displayName = 'AlertDescription'
export { Alert, AlertTitle, AlertDescription }

View File

@@ -3,6 +3,8 @@ import { useCallback, useEffect, useState } from 'react'
import { randomColor } from '@/lib/utils' import { randomColor } from '@/lib/utils'
import * as Constants from '@/lib/constants' import * as Constants from '@/lib/constants'
import { useGraphStore, RawGraph } from '@/stores/graph' import { useGraphStore, RawGraph } from '@/stores/graph'
import { queryGraphs } from '@/api/lightrag'
import { useBackendState } from '@/stores/state'
const validateGraph = (graph: RawGraph) => { const validateGraph = (graph: RawGraph) => {
if (!graph) { if (!graph) {
@@ -46,8 +48,14 @@ export type NodeType = {
export type EdgeType = { label: string } export type EdgeType = { label: string }
const fetchGraph = async (label: string) => { const fetchGraph = async (label: string) => {
const response = await fetch(`/graphs?label=${label}`) let rawData: any = null
const rawData = await response.json()
try {
rawData = await queryGraphs(label)
} catch (e) {
useBackendState.getState().setErrorMessage(`${e}`, 'Query Graphs Error!')
return null
}
let rawGraph = null let rawGraph = null

View File

@@ -1,5 +1,7 @@
import { ButtonVariantType } from '@/components/ui/Button' import { ButtonVariantType } from '@/components/ui/Button'
export const backendBaseUrl = ''
export const controlButtonVariant: ButtonVariantType = 'ghost' export const controlButtonVariant: ButtonVariantType = 'ghost'
export const labelColorDarkTheme = '#B2EBF2' export const labelColorDarkTheme = '#B2EBF2'

View File

@@ -0,0 +1,41 @@
import { create } from 'zustand'
import { createSelectors } from '@/lib/utils'
import { checkHealth } from '@/api/lightrag'
interface BackendState {
health: boolean
message: string | null
messageTitle: string | null
check: () => Promise<boolean>
clear: () => void
setErrorMessage: (message: string, messageTitle: string) => void
}
const useBackendStateStoreBase = create<BackendState>()((set) => ({
health: true,
message: null,
messageTitle: null,
check: async () => {
const health = await checkHealth()
if (health.status === 'healthy') {
set({ health: true, message: null, messageTitle: null })
return true
}
set({ health: false, message: health.message, messageTitle: 'Backend Health Check Error!' })
return false
},
clear: () => {
set({ health: true, message: null, messageTitle: null })
},
setErrorMessage: (message: string, messageTitle: string) => {
set({ health: false, message, messageTitle })
}
}))
const useBackendState = createSelectors(useBackendStateStoreBase)
export { useBackendState }