implement backend health check and alert system
This commit is contained in:
@@ -1,12 +1,18 @@
|
||||
import ThemeProvider from '@/components/ThemeProvider'
|
||||
import BackendMessageAlert from '@/components/BackendMessageAlert'
|
||||
import { GraphViewer } from '@/GraphViewer'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useBackendState } from '@/stores/state'
|
||||
|
||||
function App() {
|
||||
const health = useBackendState.use.health()
|
||||
|
||||
return (
|
||||
<ThemeProvider defaultTheme="system" storageKey="lightrag-viewer-webui-theme">
|
||||
<div className="h-screen w-screen">
|
||||
<ThemeProvider>
|
||||
<div className={cn('h-screen w-screen', !health && 'pointer-events-none')}>
|
||||
<GraphViewer />
|
||||
</div>
|
||||
{!health && <BackendMessageAlert />}
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
@@ -153,7 +153,7 @@ export const GraphViewer = () => {
|
||||
/>
|
||||
</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 />
|
||||
<ZoomControl />
|
||||
<LayoutsControl />
|
||||
|
97
lightrag/api/graph_viewer_webui/src/api/lightrag.ts
Normal file
97
lightrag/api/graph_viewer_webui/src/api/lightrag.ts
Normal 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()
|
||||
}
|
@@ -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
|
@@ -70,7 +70,7 @@ export const GraphSearchInput = ({
|
||||
|
||||
return (
|
||||
<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}
|
||||
renderOption={OptionComponent}
|
||||
getOptionValue={(item) => item.id}
|
||||
|
@@ -59,7 +59,7 @@ const PropertiesView = () => {
|
||||
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' ? (
|
||||
<NodePropertiesView node={currentElement as any} />
|
||||
) : (
|
||||
|
@@ -7,6 +7,8 @@ import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
import { SettingsIcon } from 'lucide-react'
|
||||
|
||||
import * as Api from '@/api/lightrag'
|
||||
|
||||
/**
|
||||
* Component that displays a checkbox with a label.
|
||||
*/
|
||||
@@ -95,6 +97,13 @@ export default function Settings() {
|
||||
onCheckedChange={setShowEdgeLabel}
|
||||
label="Show Edge Label"
|
||||
/>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log(Api.checkHealth())
|
||||
}}
|
||||
>
|
||||
Test Api
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
49
lightrag/api/graph_viewer_webui/src/components/ui/Alert.tsx
Normal file
49
lightrag/api/graph_viewer_webui/src/components/ui/Alert.tsx
Normal 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 }
|
@@ -3,6 +3,8 @@ import { useCallback, useEffect, useState } from 'react'
|
||||
import { randomColor } from '@/lib/utils'
|
||||
import * as Constants from '@/lib/constants'
|
||||
import { useGraphStore, RawGraph } from '@/stores/graph'
|
||||
import { queryGraphs } from '@/api/lightrag'
|
||||
import { useBackendState } from '@/stores/state'
|
||||
|
||||
const validateGraph = (graph: RawGraph) => {
|
||||
if (!graph) {
|
||||
@@ -46,8 +48,14 @@ export type NodeType = {
|
||||
export type EdgeType = { label: string }
|
||||
|
||||
const fetchGraph = async (label: string) => {
|
||||
const response = await fetch(`/graphs?label=${label}`)
|
||||
const rawData = await response.json()
|
||||
let rawData: any = null
|
||||
|
||||
try {
|
||||
rawData = await queryGraphs(label)
|
||||
} catch (e) {
|
||||
useBackendState.getState().setErrorMessage(`${e}`, 'Query Graphs Error!')
|
||||
return null
|
||||
}
|
||||
|
||||
let rawGraph = null
|
||||
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import { ButtonVariantType } from '@/components/ui/Button'
|
||||
|
||||
export const backendBaseUrl = ''
|
||||
|
||||
export const controlButtonVariant: ButtonVariantType = 'ghost'
|
||||
|
||||
export const labelColorDarkTheme = '#B2EBF2'
|
||||
|
41
lightrag/api/graph_viewer_webui/src/stores/state.ts
Normal file
41
lightrag/api/graph_viewer_webui/src/stores/state.ts
Normal 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 }
|
Reference in New Issue
Block a user