implement backend health check and alert system
This commit is contained in:
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -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 />
|
||||||
|
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 (
|
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}
|
||||||
|
@@ -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} />
|
||||||
) : (
|
) : (
|
||||||
|
@@ -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>
|
||||||
|
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 { 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
|
||||||
|
|
||||||
|
@@ -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'
|
||||||
|
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