style(lightrag_webui): fix indentation, color palette, and component optimization
- Fix inconsistent indentation in App.tsx (66 → 68 chars) - Refactor GraphControl reducer logic: cache selection/theme in refs to prevent expensive re-renders on every hover/selection change; extract nodeReducer and edgeReducer to useCallback with stable dependencies - Improve GraphViewer performance: extract FocusSync and GraphSearchWithSelection components to prevent re-renders from unrelated store updates - Remove unused imports (X icon, ZapIcon, i18n) - Remove unused function parameter (storageConfig) - Standardize dark theme colors: improve contrast and visual hierarchy (hsl values); update scrollbar colors for better visibility - Normalize quote style: double quotes → single quotes in className attributes - Fix form element styling: improve dark mode button hover states (gray-800/900 → gray-700/800, red-900 → red-800) - Optimize dropdown menu colors: dark mode backgrounds (gray-900/gray-800) - Relocate HIDDEN_COLUMNS constant to module level in TableExplorer - Optimize RowDetailModal: move entries computation to useMemo for perf - Fix useLightragGraph dependency array: add missing minDegree and includeOrphans dependencies
This commit is contained in:
parent
9f5948650e
commit
4e58da3583
12 changed files with 314 additions and 315 deletions
|
|
@ -63,7 +63,7 @@ function App() {
|
|||
if (isMountedRef.current) {
|
||||
const status = await useBackendState.getState().check();
|
||||
if (status && 'status' in status && status.status === 'healthy' && status.configuration) {
|
||||
useSettingsStore.getState().setStorageConfig(status.configuration);
|
||||
useSettingsStore.getState().setStorageConfig(status.configuration);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
|||
import { AbstractGraph } from 'graphology-types'
|
||||
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
||||
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||
|
||||
// import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
|
||||
import { EdgeType, NodeType } from '@/hooks/useLightragGraph'
|
||||
|
|
@ -212,158 +212,191 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|||
}
|
||||
}, [sigma, sigmaGraph, minEdgeSize, maxEdgeSize])
|
||||
|
||||
// Cache selection state in refs so we don't trigger expensive reducer recreation on hover/selection changes
|
||||
const selectionRef = useRef({
|
||||
selectedNode: null as string | null,
|
||||
focusedNode: null as string | null,
|
||||
selectedEdge: null as string | null,
|
||||
focusedEdge: null as string | null,
|
||||
hideUnselectedEdges
|
||||
})
|
||||
|
||||
const focusedNeighborsRef = useRef<{ key: string | null; neighbors: Set<string> | null }>(
|
||||
{ key: null, neighbors: null }
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
selectionRef.current = {
|
||||
selectedNode,
|
||||
focusedNode,
|
||||
selectedEdge,
|
||||
focusedEdge,
|
||||
hideUnselectedEdges
|
||||
}
|
||||
|
||||
// Invalidate neighbor cache when focused node changes
|
||||
if (focusedNeighborsRef.current.key !== (focusedNode || null)) {
|
||||
focusedNeighborsRef.current = { key: focusedNode || null, neighbors: null }
|
||||
}
|
||||
}, [selectedNode, focusedNode, selectedEdge, focusedEdge, hideUnselectedEdges])
|
||||
|
||||
// Theme values used inside reducers; kept in refs to avoid re-creating reducer functions
|
||||
const themeRef = useRef({
|
||||
isDarkTheme: false,
|
||||
labelColor: undefined as string | undefined,
|
||||
edgeColor: undefined as string | undefined
|
||||
})
|
||||
|
||||
/**
|
||||
* When component mount or hovered node change
|
||||
* => Setting the sigma reducers
|
||||
*/
|
||||
useEffect(() => {
|
||||
// Check if dark mode is actually applied (handles both 'dark' theme and 'system' theme when OS is dark)
|
||||
const isDarkTheme = theme === 'dark' ||
|
||||
(theme === 'system' && window.document.documentElement.classList.contains('dark'))
|
||||
const labelColor = isDarkTheme ? Constants.labelColorDarkTheme : undefined
|
||||
const edgeColor = isDarkTheme ? Constants.edgeColorDarkTheme : undefined
|
||||
|
||||
// Update all dynamic settings directly without recreating the sigma container
|
||||
themeRef.current = {
|
||||
isDarkTheme,
|
||||
labelColor: isDarkTheme ? Constants.labelColorDarkTheme : undefined,
|
||||
edgeColor: isDarkTheme ? Constants.edgeColorDarkTheme : undefined
|
||||
}
|
||||
}, [theme, systemThemeIsDark])
|
||||
|
||||
// Helper to lazily compute focused node neighbors and reuse across reducer calls
|
||||
const getFocusedNeighbors = useCallback((graph: AbstractGraph, nodeId: string): Set<string> => {
|
||||
if (focusedNeighborsRef.current.key === nodeId && focusedNeighborsRef.current.neighbors) {
|
||||
return focusedNeighborsRef.current.neighbors
|
||||
}
|
||||
const neighbors = new Set(graph.neighbors(nodeId))
|
||||
focusedNeighborsRef.current = { key: nodeId, neighbors }
|
||||
return neighbors
|
||||
}, [])
|
||||
|
||||
const nodeReducer = useCallback((node: string, data: NodeType) => {
|
||||
const graph = sigma.getGraph()
|
||||
const { selectedNode, focusedNode, selectedEdge, focusedEdge } = selectionRef.current
|
||||
const { isDarkTheme, labelColor } = themeRef.current
|
||||
|
||||
if (!graph.hasNode(node)) {
|
||||
return { ...data, highlighted: false, labelColor }
|
||||
}
|
||||
|
||||
const newData: NodeType & { labelColor?: string; borderColor?: string; borderSize?: number } = {
|
||||
...data,
|
||||
highlighted: data.highlighted || false,
|
||||
labelColor
|
||||
}
|
||||
|
||||
// Hidden connections indicator
|
||||
const dbDegree = graph.getNodeAttribute(node, 'db_degree') || 0
|
||||
const visualDegree = graph.degree(node)
|
||||
if (dbDegree > visualDegree) {
|
||||
newData.borderColor = Constants.nodeBorderColorHiddenConnections
|
||||
newData.borderSize = 1.5
|
||||
}
|
||||
|
||||
if (disableHoverEffect) {
|
||||
return newData
|
||||
}
|
||||
|
||||
const targetNode = focusedNode || selectedNode
|
||||
const targetEdge = focusedEdge || selectedEdge
|
||||
|
||||
if (targetNode && graph.hasNode(targetNode)) {
|
||||
try {
|
||||
const neighbors = getFocusedNeighbors(graph, targetNode)
|
||||
if (node === targetNode || neighbors.has(node)) {
|
||||
newData.highlighted = true
|
||||
if (node === selectedNode) {
|
||||
newData.borderColor = Constants.nodeBorderColorSelected
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in nodeReducer:', error)
|
||||
return { ...data, highlighted: false, labelColor }
|
||||
}
|
||||
} else if (targetEdge && graph.hasEdge(targetEdge)) {
|
||||
try {
|
||||
if (graph.extremities(targetEdge).includes(node)) {
|
||||
newData.highlighted = true
|
||||
newData.size = 3
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error accessing edge extremities in nodeReducer:', error)
|
||||
return { ...data, highlighted: false, labelColor }
|
||||
}
|
||||
}
|
||||
|
||||
if (newData.highlighted) {
|
||||
if (isDarkTheme) {
|
||||
newData.labelColor = Constants.LabelColorHighlightedDarkTheme
|
||||
}
|
||||
} else {
|
||||
newData.color = Constants.nodeColorDisabled
|
||||
}
|
||||
|
||||
return newData
|
||||
}, [sigma, disableHoverEffect, getFocusedNeighbors])
|
||||
|
||||
const edgeReducer = useCallback((edge: string, data: EdgeType) => {
|
||||
const graph = sigma.getGraph()
|
||||
const { selectedNode, focusedNode, selectedEdge, focusedEdge, hideUnselectedEdges } = selectionRef.current
|
||||
const { isDarkTheme, labelColor, edgeColor } = themeRef.current
|
||||
|
||||
if (!graph.hasEdge(edge)) {
|
||||
return { ...data, hidden: false, labelColor, color: edgeColor }
|
||||
}
|
||||
|
||||
const newData = { ...data, hidden: false, labelColor, color: edgeColor }
|
||||
|
||||
if (disableHoverEffect) {
|
||||
return newData
|
||||
}
|
||||
|
||||
const targetNode = focusedNode || selectedNode
|
||||
const edgeHighlightColor = isDarkTheme
|
||||
? Constants.edgeColorHighlightedDarkTheme
|
||||
: Constants.edgeColorHighlightedLightTheme
|
||||
|
||||
if (targetNode && graph.hasNode(targetNode)) {
|
||||
try {
|
||||
const includesNode = graph.extremities(edge).includes(targetNode)
|
||||
if (hideUnselectedEdges && !includesNode) {
|
||||
newData.hidden = true
|
||||
} else if (includesNode) {
|
||||
newData.color = edgeHighlightColor
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in edgeReducer:', error)
|
||||
return { ...data, hidden: false, labelColor, color: edgeColor }
|
||||
}
|
||||
} else {
|
||||
const _selectedEdge = selectedEdge && graph.hasEdge(selectedEdge) ? selectedEdge : null
|
||||
const _focusedEdge = focusedEdge && graph.hasEdge(focusedEdge) ? focusedEdge : null
|
||||
|
||||
if (_selectedEdge || _focusedEdge) {
|
||||
if (edge === _selectedEdge) {
|
||||
newData.color = Constants.edgeColorSelected
|
||||
} else if (edge === _focusedEdge) {
|
||||
newData.color = edgeHighlightColor
|
||||
} else if (hideUnselectedEdges) {
|
||||
newData.hidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newData
|
||||
}, [sigma, disableHoverEffect])
|
||||
|
||||
/**
|
||||
* Keep sigma reducers stable; selection/theme changes are read from refs to avoid
|
||||
* re-registering reducers on every hover and maintain frame budget.
|
||||
*/
|
||||
useEffect(() => {
|
||||
setSettings({
|
||||
// Update display settings
|
||||
enableEdgeEvents,
|
||||
renderEdgeLabels,
|
||||
renderLabels,
|
||||
|
||||
// Node reducer for node appearance
|
||||
nodeReducer: (node, data) => {
|
||||
const graph = sigma.getGraph()
|
||||
|
||||
// Add defensive check for node existence during theme switching
|
||||
if (!graph.hasNode(node)) {
|
||||
console.warn(`Node ${node} not found in graph during theme switch, returning default data`)
|
||||
return { ...data, highlighted: false, labelColor }
|
||||
}
|
||||
|
||||
const newData: NodeType & {
|
||||
labelColor?: string
|
||||
borderColor?: string
|
||||
borderSize?: number
|
||||
} = { ...data, highlighted: data.highlighted || false, labelColor }
|
||||
|
||||
// Check for hidden connections (db_degree > visual_degree)
|
||||
const dbDegree = graph.getNodeAttribute(node, 'db_degree') || 0
|
||||
const visualDegree = graph.degree(node)
|
||||
if (dbDegree > visualDegree) {
|
||||
newData.borderColor = Constants.nodeBorderColorHiddenConnections
|
||||
newData.borderSize = 1.5
|
||||
}
|
||||
|
||||
if (!disableHoverEffect) {
|
||||
newData.highlighted = false
|
||||
const _focusedNode = focusedNode || selectedNode
|
||||
const _focusedEdge = focusedEdge || selectedEdge
|
||||
|
||||
if (_focusedNode && graph.hasNode(_focusedNode)) {
|
||||
try {
|
||||
if (node === _focusedNode || graph.neighbors(_focusedNode).includes(node)) {
|
||||
newData.highlighted = true
|
||||
if (node === selectedNode) {
|
||||
newData.borderColor = Constants.nodeBorderColorSelected
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in nodeReducer:', error);
|
||||
return { ...data, highlighted: false, labelColor }
|
||||
}
|
||||
} else if (_focusedEdge && graph.hasEdge(_focusedEdge)) {
|
||||
try {
|
||||
if (graph.extremities(_focusedEdge).includes(node)) {
|
||||
newData.highlighted = true
|
||||
newData.size = 3
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error accessing edge extremities in nodeReducer:', error);
|
||||
return { ...data, highlighted: false, labelColor }
|
||||
}
|
||||
} else {
|
||||
return newData
|
||||
}
|
||||
|
||||
if (newData.highlighted) {
|
||||
if (isDarkTheme) {
|
||||
newData.labelColor = Constants.LabelColorHighlightedDarkTheme
|
||||
}
|
||||
} else {
|
||||
newData.color = Constants.nodeColorDisabled
|
||||
}
|
||||
}
|
||||
return newData
|
||||
},
|
||||
|
||||
// Edge reducer for edge appearance
|
||||
edgeReducer: (edge, data) => {
|
||||
const graph = sigma.getGraph()
|
||||
|
||||
// Add defensive check for edge existence during theme switching
|
||||
if (!graph.hasEdge(edge)) {
|
||||
console.warn(`Edge ${edge} not found in graph during theme switch, returning default data`)
|
||||
return { ...data, hidden: false, labelColor, color: edgeColor }
|
||||
}
|
||||
|
||||
const newData = { ...data, hidden: false, labelColor, color: edgeColor }
|
||||
|
||||
if (!disableHoverEffect) {
|
||||
const _focusedNode = focusedNode || selectedNode
|
||||
// Choose edge highlight color based on theme
|
||||
const edgeHighlightColor = isDarkTheme
|
||||
? Constants.edgeColorHighlightedDarkTheme
|
||||
: Constants.edgeColorHighlightedLightTheme
|
||||
|
||||
if (_focusedNode && graph.hasNode(_focusedNode)) {
|
||||
try {
|
||||
if (hideUnselectedEdges) {
|
||||
if (!graph.extremities(edge).includes(_focusedNode)) {
|
||||
newData.hidden = true
|
||||
}
|
||||
} else {
|
||||
if (graph.extremities(edge).includes(_focusedNode)) {
|
||||
newData.color = edgeHighlightColor
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in edgeReducer:', error);
|
||||
return { ...data, hidden: false, labelColor, color: edgeColor }
|
||||
}
|
||||
} else {
|
||||
const _selectedEdge = selectedEdge && graph.hasEdge(selectedEdge) ? selectedEdge : null;
|
||||
const _focusedEdge = focusedEdge && graph.hasEdge(focusedEdge) ? focusedEdge : null;
|
||||
|
||||
if (_selectedEdge || _focusedEdge) {
|
||||
if (edge === _selectedEdge) {
|
||||
newData.color = Constants.edgeColorSelected
|
||||
} else if (edge === _focusedEdge) {
|
||||
newData.color = edgeHighlightColor
|
||||
} else if (hideUnselectedEdges) {
|
||||
newData.hidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return newData
|
||||
}
|
||||
nodeReducer,
|
||||
edgeReducer
|
||||
})
|
||||
}, [
|
||||
selectedNode,
|
||||
focusedNode,
|
||||
selectedEdge,
|
||||
focusedEdge,
|
||||
setSettings,
|
||||
sigma,
|
||||
disableHoverEffect,
|
||||
theme,
|
||||
systemThemeIsDark,
|
||||
hideUnselectedEdges,
|
||||
enableEdgeEvents,
|
||||
renderEdgeLabels,
|
||||
renderLabels
|
||||
])
|
||||
}, [setSettings, enableEdgeEvents, renderEdgeLabels, renderLabels, nodeReducer, edgeReducer])
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { AlignLeft, AlignCenter, AlignRight, Link, Loader2, CheckCircle2, AlertCircle, X, Play, Square } from 'lucide-react'
|
||||
import { AlignLeft, AlignCenter, AlignRight, Link, Loader2, CheckCircle2, AlertCircle, Play, Square } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const StatusIndicator = ({ className }: { className?: string }) => {
|
|||
}, [lastCheckTime])
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2 opacity-80 select-none", className)}>
|
||||
<div className={cn('flex items-center gap-2 opacity-80 select-none', className)}>
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ export default function UserPromptInputWithHistory({
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleInputClick}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-0 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-0 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ChevronDown
|
||||
|
|
@ -165,13 +165,13 @@ export default function UserPromptInputWithHistory({
|
|||
|
||||
{/* Dropdown */}
|
||||
{isOpen && history.length > 0 && (
|
||||
<div className="absolute top-full left-0 right-0 z-50 mt-0.5 bg-gray-100 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-md shadow-lg max-h-96 overflow-auto min-w-0">
|
||||
<div className="absolute top-full left-0 right-0 z-50 mt-0.5 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-lg max-h-96 overflow-auto min-w-0">
|
||||
{history.map((prompt, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'flex items-center justify-between pl-3 pr-1 py-2 text-sm hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors',
|
||||
'border-b border-gray-100 dark:border-gray-700 last:border-b-0',
|
||||
'border-b border-gray-100 dark:border-gray-600 last:border-b-0',
|
||||
'focus-within:bg-gray-100 dark:focus-within:bg-gray-700',
|
||||
selectedIndex === index && 'bg-gray-100 dark:bg-gray-700'
|
||||
)}
|
||||
|
|
@ -188,7 +188,7 @@ export default function UserPromptInputWithHistory({
|
|||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleDeleteHistoryItem(index, e)}
|
||||
className="flex-shrink-0 p-0 rounded hover:bg-red-100 dark:hover:bg-red-900 transition-colors focus:outline-none ml-auto"
|
||||
className="flex-shrink-0 p-0 rounded hover:bg-red-100 dark:hover:bg-red-800 transition-colors focus:outline-none ml-auto"
|
||||
title="Delete this history item"
|
||||
>
|
||||
<X className="h-3 w-3 text-gray-400 hover:text-red-500" />
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ export default function DocumentManager() {
|
|||
}, []);
|
||||
|
||||
const [showPipelineStatus, setShowPipelineStatus] = useState(false)
|
||||
const { t, i18n } = useTranslation()
|
||||
const { t } = useTranslation()
|
||||
const health = useBackendState.use.health()
|
||||
const pipelineBusy = useBackendState.use.pipelineBusy()
|
||||
|
||||
|
|
@ -484,32 +484,32 @@ export default function DocumentManager() {
|
|||
label: t('documentPanel.documentManager.status.all'),
|
||||
icon: FileText,
|
||||
count: statusCounts.all || documentCounts.all || 0,
|
||||
color: "text-muted-foreground",
|
||||
activeColor: "text-foreground"
|
||||
color: 'text-muted-foreground',
|
||||
activeColor: 'text-foreground'
|
||||
},
|
||||
{
|
||||
id: 'processed' as StatusFilter,
|
||||
label: t('documentPanel.documentManager.status.completed'),
|
||||
icon: CheckCircle2,
|
||||
count: processedCount,
|
||||
color: "text-emerald-500",
|
||||
activeColor: "text-emerald-600 dark:text-emerald-500"
|
||||
color: 'text-emerald-500',
|
||||
activeColor: 'text-emerald-600 dark:text-emerald-500'
|
||||
},
|
||||
{
|
||||
id: 'preprocessed' as StatusFilter,
|
||||
label: t('documentPanel.documentManager.status.preprocessed'),
|
||||
icon: Brain,
|
||||
count: preprocessedCount,
|
||||
color: "text-purple-500",
|
||||
activeColor: "text-purple-600 dark:text-purple-500"
|
||||
color: 'text-purple-500',
|
||||
activeColor: 'text-purple-600 dark:text-purple-500'
|
||||
},
|
||||
{
|
||||
id: 'processing' as StatusFilter,
|
||||
label: t('documentPanel.documentManager.status.processing'),
|
||||
icon: Loader2,
|
||||
count: processingCount,
|
||||
color: "text-blue-500",
|
||||
activeColor: "text-blue-600 dark:text-blue-500",
|
||||
color: 'text-blue-500',
|
||||
activeColor: 'text-blue-600 dark:text-blue-500',
|
||||
spin: true
|
||||
},
|
||||
{
|
||||
|
|
@ -517,8 +517,8 @@ export default function DocumentManager() {
|
|||
label: t('documentPanel.documentManager.status.pending'),
|
||||
icon: Loader2,
|
||||
count: pendingCount,
|
||||
color: "text-amber-500",
|
||||
activeColor: "text-amber-600 dark:text-amber-500",
|
||||
color: 'text-amber-500',
|
||||
activeColor: 'text-amber-600 dark:text-amber-500',
|
||||
spin: true
|
||||
},
|
||||
{
|
||||
|
|
@ -526,8 +526,8 @@ export default function DocumentManager() {
|
|||
label: t('documentPanel.documentManager.status.failed'),
|
||||
icon: AlertCircle,
|
||||
count: failedCount,
|
||||
color: "text-red-500",
|
||||
activeColor: "text-red-600 dark:text-red-500"
|
||||
color: 'text-red-500',
|
||||
activeColor: 'text-red-600 dark:text-red-500'
|
||||
}
|
||||
], [t, statusCounts, documentCounts, processedCount, preprocessedCount, processingCount, pendingCount, failedCount]);
|
||||
|
||||
|
|
@ -941,63 +941,6 @@ export default function DocumentManager() {
|
|||
setPagination(prev => ({ ...prev, page: 1, page_size: newPageSize }));
|
||||
}, [pagination.page_size, setDocumentsPageSize]);
|
||||
|
||||
// Handle manual refresh with pagination reset logic
|
||||
const handleManualRefresh = useCallback(async () => {
|
||||
try {
|
||||
setIsRefreshing(true);
|
||||
|
||||
// Fetch documents from the first page
|
||||
const request: DocumentsRequest = {
|
||||
status_filter: statusFilter === 'all' ? null : statusFilter,
|
||||
page: 1,
|
||||
page_size: pagination.page_size,
|
||||
sort_field: sortField,
|
||||
sort_direction: sortDirection
|
||||
};
|
||||
|
||||
const response = await getDocumentsPaginated(request);
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// Check if total count is less than current page size and page size is not already 10
|
||||
if (response.pagination.total_count < pagination.page_size && pagination.page_size !== 10) {
|
||||
// Reset page size to 10 which will trigger a new fetch
|
||||
handlePageSizeChange(10);
|
||||
} else {
|
||||
// Update pagination state
|
||||
setPagination(response.pagination);
|
||||
setCurrentPageDocs(response.documents);
|
||||
setStatusCounts(response.status_counts);
|
||||
|
||||
// Update legacy docs state for backward compatibility
|
||||
const legacyDocs: DocsStatusesResponse = {
|
||||
statuses: {
|
||||
processed: response.documents.filter(doc => doc.status === 'processed'),
|
||||
preprocessed: response.documents.filter(doc => doc.status === 'preprocessed'),
|
||||
processing: response.documents.filter(doc => doc.status === 'processing'),
|
||||
pending: response.documents.filter(doc => doc.status === 'pending'),
|
||||
failed: response.documents.filter(doc => doc.status === 'failed')
|
||||
}
|
||||
};
|
||||
|
||||
if (response.pagination.total_count > 0) {
|
||||
setDocs(legacyDocs);
|
||||
} else {
|
||||
setDocs(null);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
if (isMountedRef.current) {
|
||||
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }));
|
||||
}
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}
|
||||
}, [statusFilter, pagination.page_size, sortField, sortDirection, handlePageSizeChange, t]);
|
||||
|
||||
// Monitor pipelineBusy changes and trigger immediate refresh with timer reset
|
||||
useEffect(() => {
|
||||
// Skip the first render when prevPipelineBusyRef is undefined
|
||||
|
|
@ -1193,27 +1136,27 @@ export default function DocumentManager() {
|
|||
tabIndex={0}
|
||||
aria-pressed={isActive}
|
||||
className={cn(
|
||||
"flex-1 min-w-[140px] flex flex-col gap-1 p-3 rounded-lg transition-all cursor-pointer border select-none",
|
||||
'flex-1 min-w-[140px] flex flex-col gap-1 p-3 rounded-lg transition-all cursor-pointer border select-none',
|
||||
isActive
|
||||
? "bg-background shadow-sm border-border ring-1 ring-black/5 dark:ring-white/5"
|
||||
: "bg-background/80 border-border/40 hover:bg-background hover:border-border/60"
|
||||
? 'bg-background shadow-sm border-border ring-1 ring-black/5 dark:ring-white/5'
|
||||
: 'bg-background/80 border-border/40 hover:bg-background hover:border-border/60'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={cn("text-xs font-medium", isActive ? "text-foreground" : "text-muted-foreground")}>
|
||||
<span className={cn('text-xs font-medium', isActive ? 'text-foreground' : 'text-muted-foreground')}>
|
||||
{item.label}
|
||||
</span>
|
||||
{showIcon && (
|
||||
<Icon
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
'h-4 w-4',
|
||||
isActive ? item.activeColor : item.color,
|
||||
item.spin && item.count > 0 && "animate-spin"
|
||||
item.spin && item.count > 0 && 'animate-spin'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn("text-2xl font-bold", isActive ? "text-foreground" : "text-muted-foreground/80")}>
|
||||
<div className={cn('text-2xl font-bold', isActive ? 'text-foreground' : 'text-muted-foreground/80')}>
|
||||
{item.count}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1266,10 +1209,10 @@ export default function DocumentManager() {
|
|||
size="sm"
|
||||
onClick={() => setShowFileName(!showFileName)}
|
||||
className={cn(
|
||||
"transition-all border",
|
||||
'transition-all border',
|
||||
showFileName
|
||||
? "bg-primary text-primary-foreground hover:bg-primary/90 border-primary shadow-sm"
|
||||
: "text-muted-foreground border-dashed border-border/60 hover:border-solid hover:text-foreground hover:bg-accent/50"
|
||||
? 'bg-primary text-primary-foreground hover:bg-primary/90 border-primary shadow-sm'
|
||||
: 'text-muted-foreground border-dashed border-border/60 hover:border-solid hover:text-foreground hover:bg-accent/50'
|
||||
)}
|
||||
side="bottom"
|
||||
tooltip={showFileName ? t('documentPanel.documentManager.hideButton') : t('documentPanel.documentManager.showButton')}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,41 @@ const createSigmaSettings = (isDarkTheme: boolean): Partial<SigmaSettings> => ({
|
|||
// labelFont: 'Lato, sans-serif'
|
||||
})
|
||||
|
||||
// Keep focus logic isolated to avoid re-rendering the whole viewer during hover/selection churn
|
||||
const FocusSync = () => {
|
||||
const selectedNode = useGraphStore.use.selectedNode()
|
||||
const focusedNode = useGraphStore.use.focusedNode()
|
||||
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
|
||||
|
||||
const autoFocusedNode = useMemo(() => focusedNode ?? selectedNode, [focusedNode, selectedNode])
|
||||
|
||||
return <FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
|
||||
}
|
||||
|
||||
// Keep GraphSearch value derivation local to avoid bubbling re-renders
|
||||
const GraphSearchWithSelection = ({
|
||||
onFocus,
|
||||
onSelect
|
||||
}: {
|
||||
onFocus: (value: GraphSearchOption | null) => void
|
||||
onSelect: (value: GraphSearchOption | null) => void
|
||||
}) => {
|
||||
const selectedNode = useGraphStore.use.selectedNode()
|
||||
|
||||
const searchInitSelectedNode = useMemo(
|
||||
(): OptionItem | null => (selectedNode ? { type: 'nodes', id: selectedNode } : null),
|
||||
[selectedNode]
|
||||
)
|
||||
|
||||
return (
|
||||
<GraphSearch
|
||||
value={searchInitSelectedNode}
|
||||
onFocus={onFocus}
|
||||
onChange={onSelect}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const GraphEvents = () => {
|
||||
const registerEvents = useRegisterEvents()
|
||||
const sigma = useSigma()
|
||||
|
|
@ -113,9 +148,6 @@ const GraphViewer = () => {
|
|||
const sigmaRef = useRef<any>(null)
|
||||
const prevTheme = useRef<string>('')
|
||||
|
||||
const selectedNode = useGraphStore.use.selectedNode()
|
||||
const focusedNode = useGraphStore.use.focusedNode()
|
||||
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
|
||||
const isFetching = useGraphStore.use.isFetching()
|
||||
|
||||
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
||||
|
|
@ -186,12 +218,6 @@ const GraphViewer = () => {
|
|||
}
|
||||
}, [])
|
||||
|
||||
const autoFocusedNode = useMemo(() => focusedNode ?? selectedNode, [focusedNode, selectedNode])
|
||||
const searchInitSelectedNode = useMemo(
|
||||
(): OptionItem | null => (selectedNode ? { type: 'nodes', id: selectedNode } : null),
|
||||
[selectedNode]
|
||||
)
|
||||
|
||||
// Always render SigmaContainer but control its visibility with CSS
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
|
|
@ -204,15 +230,14 @@ const GraphViewer = () => {
|
|||
|
||||
{enableNodeDrag && <GraphEvents />}
|
||||
|
||||
<FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
|
||||
<FocusSync />
|
||||
|
||||
<div className="absolute top-2 left-2 flex items-start gap-2">
|
||||
<GraphLabels />
|
||||
{showNodeSearchBar && !isThemeSwitching && (
|
||||
<GraphSearch
|
||||
value={searchInitSelectedNode}
|
||||
<GraphSearchWithSelection
|
||||
onFocus={onSearchFocus}
|
||||
onChange={onSearchSelect}
|
||||
onSelect={onSearchSelect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -145,9 +145,9 @@ const LoginPage = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-gradient-to-br from-fuchsia-50 to-purple-100 dark:from-gray-900 dark:to-gray-800">
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-gradient-to-br from-fuchsia-50 to-purple-100 dark:from-gray-800 dark:to-gray-700">
|
||||
<div className="absolute top-4 right-4 flex items-center gap-2">
|
||||
<AppSettings className="bg-white/30 dark:bg-gray-800/30 backdrop-blur-sm rounded-md" />
|
||||
<AppSettings className="bg-white/30 dark:bg-gray-700/30 backdrop-blur-sm rounded-md" />
|
||||
</div>
|
||||
<Card className="w-full max-w-[480px] shadow-lg mx-4">
|
||||
<CardHeader className="flex items-center justify-center space-y-2 pb-8 pt-6">
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { useAuthStore } from '@/stores/state'
|
|||
import { cn } from '@/lib/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { navigationService } from '@/services/navigation'
|
||||
import { ZapIcon, LogOutIcon, BrainIcon } from 'lucide-react'
|
||||
import { LogOutIcon, BrainIcon } from 'lucide-react'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip'
|
||||
|
||||
interface NavigationTabProps {
|
||||
|
|
@ -31,7 +31,7 @@ function NavigationTab({ value, currentTab, children }: NavigationTabProps) {
|
|||
)
|
||||
}
|
||||
|
||||
function shouldShowTableExplorer(storageConfig: any) {
|
||||
function shouldShowTableExplorer() {
|
||||
// Always show for now - TODO: fix storageConfig state propagation from health check
|
||||
return true
|
||||
|
||||
|
|
@ -48,7 +48,6 @@ function shouldShowTableExplorer(storageConfig: any) {
|
|||
|
||||
function TabsNavigation() {
|
||||
const currentTab = useSettingsStore.use.currentTab()
|
||||
const storageConfig = useSettingsStore.use.storageConfig()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
|
|
@ -66,7 +65,7 @@ function TabsNavigation() {
|
|||
<NavigationTab value="api" currentTab={currentTab}>
|
||||
{t('header.api')}
|
||||
</NavigationTab>
|
||||
{shouldShowTableExplorer(storageConfig) && (
|
||||
{shouldShowTableExplorer() && (
|
||||
<NavigationTab value="table-explorer" currentTab={currentTab}>
|
||||
{t('header.tables')}
|
||||
</NavigationTab>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import Button from '@/components/ui/Button'
|
|||
import { ChevronLeftIcon, ChevronRightIcon, RefreshCwIcon, CopyIcon, CheckIcon } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
const HIDDEN_COLUMNS = ['meta']
|
||||
|
||||
// Truncate long values for display
|
||||
function truncateValue(value: any, maxLength = 50): string {
|
||||
if (value === null || value === undefined) return ''
|
||||
|
|
@ -116,9 +118,7 @@ function RowDetailModal({
|
|||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}) {
|
||||
if (!row) return null
|
||||
|
||||
const entries = Object.entries(row)
|
||||
const entries = useMemo(() => (row ? Object.entries(row) : []), [row])
|
||||
const fullRowJson = useMemo(() => {
|
||||
try {
|
||||
return JSON.stringify(row, null, 2)
|
||||
|
|
@ -127,6 +127,8 @@ function RowDetailModal({
|
|||
}
|
||||
}, [row])
|
||||
|
||||
if (!row) return null
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
|
|
@ -208,9 +210,6 @@ export default function TableExplorer() {
|
|||
setModalOpen(true)
|
||||
}, [])
|
||||
|
||||
// Columns to hide from UI (exist in schema but not populated)
|
||||
const HIDDEN_COLUMNS = ['meta']
|
||||
|
||||
// Generate columns dynamically from data
|
||||
const columns = useMemo<ColumnDef<any>[]>(() => {
|
||||
const cols: ColumnDef<any>[] = []
|
||||
|
|
@ -260,7 +259,7 @@ export default function TableExplorer() {
|
|||
<div className="flex items-center gap-2">
|
||||
<Select value={effectiveSelectedTable} onValueChange={handleTableChange}>
|
||||
<SelectTrigger className="w-[250px]">
|
||||
<SelectValue placeholder={tableList && tableList.length > 0 ? "Select a table" : "No tables available"} />
|
||||
<SelectValue placeholder={tableList && tableList.length > 0 ? 'Select a table' : 'No tables available'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{tableList && tableList.length > 0 ? (
|
||||
|
|
@ -284,39 +283,39 @@ export default function TableExplorer() {
|
|||
</CardHeader>
|
||||
{schema && (
|
||||
<CardContent className="pb-2">
|
||||
<details className="text-xs text-muted-foreground cursor-pointer">
|
||||
<summary>Show Schema (DDL)</summary>
|
||||
<pre className="mt-2 p-2 bg-muted rounded overflow-auto max-h-[200px] font-mono text-xs">
|
||||
{schema.ddl}
|
||||
</pre>
|
||||
</details>
|
||||
<details className="text-xs text-muted-foreground cursor-pointer">
|
||||
<summary>Show Schema (DDL)</summary>
|
||||
<pre className="mt-2 p-2 bg-muted rounded overflow-auto max-h-[200px] font-mono text-xs">
|
||||
{schema.ddl}
|
||||
</pre>
|
||||
</details>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card className="flex-1 overflow-hidden flex flex-col">
|
||||
<CardContent className="flex-1 p-0 overflow-auto">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<RefreshCwIcon className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : isError ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-destructive gap-2">
|
||||
<p className="font-medium">Failed to load table data</p>
|
||||
<p className="text-sm text-muted-foreground">{error instanceof Error ? error.message : 'Unknown error'}</p>
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()} className="mt-2">
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={tableData?.data || []}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<RefreshCwIcon className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : isError ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-destructive gap-2">
|
||||
<p className="font-medium">Failed to load table data</p>
|
||||
<p className="text-sm text-muted-foreground">{error instanceof Error ? error.message : 'Unknown error'}</p>
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()} className="mt-2">
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={tableData?.data || []}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<div className="border-t p-2 flex items-center justify-between bg-muted/20">
|
||||
|
|
|
|||
|
|
@ -461,7 +461,7 @@ const useLightrangeGraph = () => {
|
|||
state.setLastSuccessfulQueryLabel('') // Clear last successful query label on error
|
||||
})
|
||||
}
|
||||
}, [queryLabel, maxQueryDepth, maxNodes, isFetching, t, graphDataVersion])
|
||||
}, [queryLabel, maxQueryDepth, maxNodes, minDegree, includeOrphans, isFetching, t, graphDataVersion])
|
||||
|
||||
// Handle node expansion
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -47,40 +47,40 @@
|
|||
}
|
||||
|
||||
.dark {
|
||||
--background: hsl(240 10% 3.9%);
|
||||
--foreground: hsl(0 0% 98%);
|
||||
--card: hsl(240 10% 3.9%);
|
||||
--card-foreground: hsl(0 0% 98%);
|
||||
--popover: hsl(240 10% 3.9%);
|
||||
--popover-foreground: hsl(0 0% 98%);
|
||||
--primary: hsl(0 0% 98%);
|
||||
--primary-foreground: hsl(240 5.9% 10%);
|
||||
--secondary: hsl(240 3.7% 15.9%);
|
||||
--secondary-foreground: hsl(0 0% 98%);
|
||||
--muted: hsl(240 3.7% 15.9%);
|
||||
--muted-foreground: hsl(240 5% 64.9%);
|
||||
--accent: hsl(240 3.7% 15.9%);
|
||||
--accent-foreground: hsl(0 0% 98%);
|
||||
--destructive: hsl(0 62.8% 30.6%);
|
||||
--destructive-foreground: hsl(0 0% 98%);
|
||||
--border: hsl(240 3.7% 15.9%);
|
||||
--input: hsl(240 3.7% 15.9%);
|
||||
--ring: hsl(240 4.9% 83.9%);
|
||||
--background: hsl(220 8% 12%);
|
||||
--foreground: hsl(0 0% 95%);
|
||||
--card: hsl(220 8% 16%);
|
||||
--card-foreground: hsl(0 0% 96%);
|
||||
--popover: hsl(220 8% 16%);
|
||||
--popover-foreground: hsl(0 0% 96%);
|
||||
--primary: hsl(0 0% 96%);
|
||||
--primary-foreground: hsl(220 8% 14%);
|
||||
--secondary: hsl(220 7% 22%);
|
||||
--secondary-foreground: hsl(0 0% 96%);
|
||||
--muted: hsl(220 6% 26%);
|
||||
--muted-foreground: hsl(220 12% 72%);
|
||||
--accent: hsl(220 7% 24%);
|
||||
--accent-foreground: hsl(0 0% 96%);
|
||||
--destructive: hsl(0 65% 52%);
|
||||
--destructive-foreground: hsl(0 0% 96%);
|
||||
--border: hsl(220 6% 30%);
|
||||
--input: hsl(220 6% 30%);
|
||||
--ring: hsl(220 15% 65%);
|
||||
--chart-1: hsl(220 70% 50%);
|
||||
--chart-2: hsl(160 60% 45%);
|
||||
--chart-3: hsl(30 80% 55%);
|
||||
--chart-4: hsl(280 65% 60%);
|
||||
--chart-5: hsl(340 75% 55%);
|
||||
--plum: #9d5ba3;
|
||||
--plum-foreground: hsl(0 0% 98%);
|
||||
--sidebar-background: hsl(240 5.9% 10%);
|
||||
--sidebar-foreground: hsl(240 4.8% 95.9%);
|
||||
--sidebar-primary: hsl(224.3 76.3% 48%);
|
||||
--sidebar-primary-foreground: hsl(0 0% 100%);
|
||||
--sidebar-accent: hsl(240 3.7% 15.9%);
|
||||
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
|
||||
--sidebar-border: hsl(240 3.7% 15.9%);
|
||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||
--plum: #b06ab6;
|
||||
--plum-foreground: hsl(0 0% 96%);
|
||||
--sidebar-background: hsl(220 8% 13%);
|
||||
--sidebar-foreground: hsl(0 0% 94%);
|
||||
--sidebar-primary: hsl(224.3 70% 62%);
|
||||
--sidebar-primary-foreground: hsl(0 0% 98%);
|
||||
--sidebar-accent: hsl(220 7% 21%);
|
||||
--sidebar-accent-foreground: hsl(0 0% 94%);
|
||||
--sidebar-border: hsl(220 7% 28%);
|
||||
--sidebar-ring: hsl(220 15% 62%);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
|
|
@ -152,11 +152,11 @@
|
|||
|
||||
.dark {
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: hsl(0 0% 90%);
|
||||
background-color: hsl(0 0% 70%);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: hsl(0 0% 0%);
|
||||
background-color: hsl(220 8% 18%);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue