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) {
|
if (isMountedRef.current) {
|
||||||
const status = await useBackendState.getState().check();
|
const status = await useBackendState.getState().check();
|
||||||
if (status && 'status' in status && status.status === 'healthy' && status.configuration) {
|
if (status && 'status' in status && status.status === 'healthy' && status.configuration) {
|
||||||
useSettingsStore.getState().setStorageConfig(status.configuration);
|
useSettingsStore.getState().setStorageConfig(status.configuration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
||||||
import { AbstractGraph } from 'graphology-types'
|
import { AbstractGraph } from 'graphology-types'
|
||||||
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
||||||
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
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 useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
|
||||||
import { EdgeType, NodeType } from '@/hooks/useLightragGraph'
|
import { EdgeType, NodeType } from '@/hooks/useLightragGraph'
|
||||||
|
|
@ -212,158 +212,191 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
}
|
}
|
||||||
}, [sigma, sigmaGraph, minEdgeSize, maxEdgeSize])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
// Check if dark mode is actually applied (handles both 'dark' theme and 'system' theme when OS is dark)
|
|
||||||
const isDarkTheme = theme === 'dark' ||
|
const isDarkTheme = theme === 'dark' ||
|
||||||
(theme === 'system' && window.document.documentElement.classList.contains('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({
|
setSettings({
|
||||||
// Update display settings
|
|
||||||
enableEdgeEvents,
|
enableEdgeEvents,
|
||||||
renderEdgeLabels,
|
renderEdgeLabels,
|
||||||
renderLabels,
|
renderLabels,
|
||||||
|
nodeReducer,
|
||||||
// Node reducer for node appearance
|
edgeReducer
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}, [
|
}, [setSettings, enableEdgeEvents, renderEdgeLabels, renderLabels, nodeReducer, edgeReducer])
|
||||||
selectedNode,
|
|
||||||
focusedNode,
|
|
||||||
selectedEdge,
|
|
||||||
focusedEdge,
|
|
||||||
setSettings,
|
|
||||||
sigma,
|
|
||||||
disableHoverEffect,
|
|
||||||
theme,
|
|
||||||
systemThemeIsDark,
|
|
||||||
hideUnselectedEdges,
|
|
||||||
enableEdgeEvents,
|
|
||||||
renderEdgeLabels,
|
|
||||||
renderLabels
|
|
||||||
])
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
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 {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ const StatusIndicator = ({ className }: { className?: string }) => {
|
||||||
}, [lastCheckTime])
|
}, [lastCheckTime])
|
||||||
|
|
||||||
return (
|
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
|
<div
|
||||||
className="flex cursor-pointer items-center gap-2"
|
className="flex cursor-pointer items-center gap-2"
|
||||||
onClick={() => setDialogOpen(true)}
|
onClick={() => setDialogOpen(true)}
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ export default function UserPromptInputWithHistory({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleInputClick}
|
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}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
|
|
@ -165,13 +165,13 @@ export default function UserPromptInputWithHistory({
|
||||||
|
|
||||||
{/* Dropdown */}
|
{/* Dropdown */}
|
||||||
{isOpen && history.length > 0 && (
|
{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) => (
|
{history.map((prompt, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
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',
|
'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',
|
'focus-within:bg-gray-100 dark:focus-within:bg-gray-700',
|
||||||
selectedIndex === index && 'bg-gray-100 dark:bg-gray-700'
|
selectedIndex === index && 'bg-gray-100 dark:bg-gray-700'
|
||||||
)}
|
)}
|
||||||
|
|
@ -188,7 +188,7 @@ export default function UserPromptInputWithHistory({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => handleDeleteHistoryItem(index, e)}
|
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"
|
title="Delete this history item"
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3 text-gray-400 hover:text-red-500" />
|
<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 [showPipelineStatus, setShowPipelineStatus] = useState(false)
|
||||||
const { t, i18n } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const health = useBackendState.use.health()
|
const health = useBackendState.use.health()
|
||||||
const pipelineBusy = useBackendState.use.pipelineBusy()
|
const pipelineBusy = useBackendState.use.pipelineBusy()
|
||||||
|
|
||||||
|
|
@ -484,32 +484,32 @@ export default function DocumentManager() {
|
||||||
label: t('documentPanel.documentManager.status.all'),
|
label: t('documentPanel.documentManager.status.all'),
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
count: statusCounts.all || documentCounts.all || 0,
|
count: statusCounts.all || documentCounts.all || 0,
|
||||||
color: "text-muted-foreground",
|
color: 'text-muted-foreground',
|
||||||
activeColor: "text-foreground"
|
activeColor: 'text-foreground'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'processed' as StatusFilter,
|
id: 'processed' as StatusFilter,
|
||||||
label: t('documentPanel.documentManager.status.completed'),
|
label: t('documentPanel.documentManager.status.completed'),
|
||||||
icon: CheckCircle2,
|
icon: CheckCircle2,
|
||||||
count: processedCount,
|
count: processedCount,
|
||||||
color: "text-emerald-500",
|
color: 'text-emerald-500',
|
||||||
activeColor: "text-emerald-600 dark:text-emerald-500"
|
activeColor: 'text-emerald-600 dark:text-emerald-500'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'preprocessed' as StatusFilter,
|
id: 'preprocessed' as StatusFilter,
|
||||||
label: t('documentPanel.documentManager.status.preprocessed'),
|
label: t('documentPanel.documentManager.status.preprocessed'),
|
||||||
icon: Brain,
|
icon: Brain,
|
||||||
count: preprocessedCount,
|
count: preprocessedCount,
|
||||||
color: "text-purple-500",
|
color: 'text-purple-500',
|
||||||
activeColor: "text-purple-600 dark:text-purple-500"
|
activeColor: 'text-purple-600 dark:text-purple-500'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'processing' as StatusFilter,
|
id: 'processing' as StatusFilter,
|
||||||
label: t('documentPanel.documentManager.status.processing'),
|
label: t('documentPanel.documentManager.status.processing'),
|
||||||
icon: Loader2,
|
icon: Loader2,
|
||||||
count: processingCount,
|
count: processingCount,
|
||||||
color: "text-blue-500",
|
color: 'text-blue-500',
|
||||||
activeColor: "text-blue-600 dark:text-blue-500",
|
activeColor: 'text-blue-600 dark:text-blue-500',
|
||||||
spin: true
|
spin: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -517,8 +517,8 @@ export default function DocumentManager() {
|
||||||
label: t('documentPanel.documentManager.status.pending'),
|
label: t('documentPanel.documentManager.status.pending'),
|
||||||
icon: Loader2,
|
icon: Loader2,
|
||||||
count: pendingCount,
|
count: pendingCount,
|
||||||
color: "text-amber-500",
|
color: 'text-amber-500',
|
||||||
activeColor: "text-amber-600 dark:text-amber-500",
|
activeColor: 'text-amber-600 dark:text-amber-500',
|
||||||
spin: true
|
spin: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -526,8 +526,8 @@ export default function DocumentManager() {
|
||||||
label: t('documentPanel.documentManager.status.failed'),
|
label: t('documentPanel.documentManager.status.failed'),
|
||||||
icon: AlertCircle,
|
icon: AlertCircle,
|
||||||
count: failedCount,
|
count: failedCount,
|
||||||
color: "text-red-500",
|
color: 'text-red-500',
|
||||||
activeColor: "text-red-600 dark:text-red-500"
|
activeColor: 'text-red-600 dark:text-red-500'
|
||||||
}
|
}
|
||||||
], [t, statusCounts, documentCounts, processedCount, preprocessedCount, processingCount, pendingCount, failedCount]);
|
], [t, statusCounts, documentCounts, processedCount, preprocessedCount, processingCount, pendingCount, failedCount]);
|
||||||
|
|
||||||
|
|
@ -941,63 +941,6 @@ export default function DocumentManager() {
|
||||||
setPagination(prev => ({ ...prev, page: 1, page_size: newPageSize }));
|
setPagination(prev => ({ ...prev, page: 1, page_size: newPageSize }));
|
||||||
}, [pagination.page_size, setDocumentsPageSize]);
|
}, [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
|
// Monitor pipelineBusy changes and trigger immediate refresh with timer reset
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Skip the first render when prevPipelineBusyRef is undefined
|
// Skip the first render when prevPipelineBusyRef is undefined
|
||||||
|
|
@ -1193,27 +1136,27 @@ export default function DocumentManager() {
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-pressed={isActive}
|
aria-pressed={isActive}
|
||||||
className={cn(
|
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
|
isActive
|
||||||
? "bg-background shadow-sm border-border ring-1 ring-black/5 dark:ring-white/5"
|
? '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/80 border-border/40 hover:bg-background hover:border-border/60'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<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}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
{showIcon && (
|
{showIcon && (
|
||||||
<Icon
|
<Icon
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-4 w-4",
|
'h-4 w-4',
|
||||||
isActive ? item.activeColor : item.color,
|
isActive ? item.activeColor : item.color,
|
||||||
item.spin && item.count > 0 && "animate-spin"
|
item.spin && item.count > 0 && 'animate-spin'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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}
|
{item.count}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1266,10 +1209,10 @@ export default function DocumentManager() {
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowFileName(!showFileName)}
|
onClick={() => setShowFileName(!showFileName)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-all border",
|
'transition-all border',
|
||||||
showFileName
|
showFileName
|
||||||
? "bg-primary text-primary-foreground hover:bg-primary/90 border-primary shadow-sm"
|
? '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"
|
: 'text-muted-foreground border-dashed border-border/60 hover:border-solid hover:text-foreground hover:bg-accent/50'
|
||||||
)}
|
)}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
tooltip={showFileName ? t('documentPanel.documentManager.hideButton') : t('documentPanel.documentManager.showButton')}
|
tooltip={showFileName ? t('documentPanel.documentManager.hideButton') : t('documentPanel.documentManager.showButton')}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,41 @@ const createSigmaSettings = (isDarkTheme: boolean): Partial<SigmaSettings> => ({
|
||||||
// labelFont: 'Lato, sans-serif'
|
// 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 GraphEvents = () => {
|
||||||
const registerEvents = useRegisterEvents()
|
const registerEvents = useRegisterEvents()
|
||||||
const sigma = useSigma()
|
const sigma = useSigma()
|
||||||
|
|
@ -113,9 +148,6 @@ const GraphViewer = () => {
|
||||||
const sigmaRef = useRef<any>(null)
|
const sigmaRef = useRef<any>(null)
|
||||||
const prevTheme = useRef<string>('')
|
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 isFetching = useGraphStore.use.isFetching()
|
||||||
|
|
||||||
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
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
|
// Always render SigmaContainer but control its visibility with CSS
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full overflow-hidden">
|
<div className="relative h-full w-full overflow-hidden">
|
||||||
|
|
@ -204,15 +230,14 @@ const GraphViewer = () => {
|
||||||
|
|
||||||
{enableNodeDrag && <GraphEvents />}
|
{enableNodeDrag && <GraphEvents />}
|
||||||
|
|
||||||
<FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
|
<FocusSync />
|
||||||
|
|
||||||
<div className="absolute top-2 left-2 flex items-start gap-2">
|
<div className="absolute top-2 left-2 flex items-start gap-2">
|
||||||
<GraphLabels />
|
<GraphLabels />
|
||||||
{showNodeSearchBar && !isThemeSwitching && (
|
{showNodeSearchBar && !isThemeSwitching && (
|
||||||
<GraphSearch
|
<GraphSearchWithSelection
|
||||||
value={searchInitSelectedNode}
|
|
||||||
onFocus={onSearchFocus}
|
onFocus={onSearchFocus}
|
||||||
onChange={onSearchSelect}
|
onSelect={onSearchSelect}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -145,9 +145,9 @@ const LoginPage = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<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>
|
</div>
|
||||||
<Card className="w-full max-w-[480px] shadow-lg mx-4">
|
<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">
|
<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 { cn } from '@/lib/utils'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { navigationService } from '@/services/navigation'
|
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'
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip'
|
||||||
|
|
||||||
interface NavigationTabProps {
|
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
|
// Always show for now - TODO: fix storageConfig state propagation from health check
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
@ -48,7 +48,6 @@ function shouldShowTableExplorer(storageConfig: any) {
|
||||||
|
|
||||||
function TabsNavigation() {
|
function TabsNavigation() {
|
||||||
const currentTab = useSettingsStore.use.currentTab()
|
const currentTab = useSettingsStore.use.currentTab()
|
||||||
const storageConfig = useSettingsStore.use.storageConfig()
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -66,7 +65,7 @@ function TabsNavigation() {
|
||||||
<NavigationTab value="api" currentTab={currentTab}>
|
<NavigationTab value="api" currentTab={currentTab}>
|
||||||
{t('header.api')}
|
{t('header.api')}
|
||||||
</NavigationTab>
|
</NavigationTab>
|
||||||
{shouldShowTableExplorer(storageConfig) && (
|
{shouldShowTableExplorer() && (
|
||||||
<NavigationTab value="table-explorer" currentTab={currentTab}>
|
<NavigationTab value="table-explorer" currentTab={currentTab}>
|
||||||
{t('header.tables')}
|
{t('header.tables')}
|
||||||
</NavigationTab>
|
</NavigationTab>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import Button from '@/components/ui/Button'
|
||||||
import { ChevronLeftIcon, ChevronRightIcon, RefreshCwIcon, CopyIcon, CheckIcon } from 'lucide-react'
|
import { ChevronLeftIcon, ChevronRightIcon, RefreshCwIcon, CopyIcon, CheckIcon } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
const HIDDEN_COLUMNS = ['meta']
|
||||||
|
|
||||||
// Truncate long values for display
|
// Truncate long values for display
|
||||||
function truncateValue(value: any, maxLength = 50): string {
|
function truncateValue(value: any, maxLength = 50): string {
|
||||||
if (value === null || value === undefined) return ''
|
if (value === null || value === undefined) return ''
|
||||||
|
|
@ -116,9 +118,7 @@ function RowDetailModal({
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
}) {
|
}) {
|
||||||
if (!row) return null
|
const entries = useMemo(() => (row ? Object.entries(row) : []), [row])
|
||||||
|
|
||||||
const entries = Object.entries(row)
|
|
||||||
const fullRowJson = useMemo(() => {
|
const fullRowJson = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(row, null, 2)
|
return JSON.stringify(row, null, 2)
|
||||||
|
|
@ -127,6 +127,8 @@ function RowDetailModal({
|
||||||
}
|
}
|
||||||
}, [row])
|
}, [row])
|
||||||
|
|
||||||
|
if (!row) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
|
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
|
|
@ -208,9 +210,6 @@ export default function TableExplorer() {
|
||||||
setModalOpen(true)
|
setModalOpen(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Columns to hide from UI (exist in schema but not populated)
|
|
||||||
const HIDDEN_COLUMNS = ['meta']
|
|
||||||
|
|
||||||
// Generate columns dynamically from data
|
// Generate columns dynamically from data
|
||||||
const columns = useMemo<ColumnDef<any>[]>(() => {
|
const columns = useMemo<ColumnDef<any>[]>(() => {
|
||||||
const cols: ColumnDef<any>[] = []
|
const cols: ColumnDef<any>[] = []
|
||||||
|
|
@ -260,7 +259,7 @@ export default function TableExplorer() {
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Select value={effectiveSelectedTable} onValueChange={handleTableChange}>
|
<Select value={effectiveSelectedTable} onValueChange={handleTableChange}>
|
||||||
<SelectTrigger className="w-[250px]">
|
<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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{tableList && tableList.length > 0 ? (
|
{tableList && tableList.length > 0 ? (
|
||||||
|
|
@ -284,39 +283,39 @@ export default function TableExplorer() {
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{schema && (
|
{schema && (
|
||||||
<CardContent className="pb-2">
|
<CardContent className="pb-2">
|
||||||
<details className="text-xs text-muted-foreground cursor-pointer">
|
<details className="text-xs text-muted-foreground cursor-pointer">
|
||||||
<summary>Show Schema (DDL)</summary>
|
<summary>Show Schema (DDL)</summary>
|
||||||
<pre className="mt-2 p-2 bg-muted rounded overflow-auto max-h-[200px] font-mono text-xs">
|
<pre className="mt-2 p-2 bg-muted rounded overflow-auto max-h-[200px] font-mono text-xs">
|
||||||
{schema.ddl}
|
{schema.ddl}
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="flex-1 overflow-hidden flex flex-col">
|
<Card className="flex-1 overflow-hidden flex flex-col">
|
||||||
<CardContent className="flex-1 p-0 overflow-auto">
|
<CardContent className="flex-1 p-0 overflow-auto">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
<RefreshCwIcon className="h-8 w-8 animate-spin text-muted-foreground" />
|
<RefreshCwIcon className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
) : isError ? (
|
) : isError ? (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-destructive gap-2">
|
<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="font-medium">Failed to load table data</p>
|
||||||
<p className="text-sm text-muted-foreground">{error instanceof Error ? error.message : 'Unknown error'}</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">
|
<Button variant="outline" size="sm" onClick={() => refetch()} className="mt-2">
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={tableData?.data || []}
|
data={tableData?.data || []}
|
||||||
onRowClick={handleRowClick}
|
onRowClick={handleRowClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<div className="border-t p-2 flex items-center justify-between bg-muted/20">
|
<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
|
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
|
// Handle node expansion
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -47,40 +47,40 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: hsl(240 10% 3.9%);
|
--background: hsl(220 8% 12%);
|
||||||
--foreground: hsl(0 0% 98%);
|
--foreground: hsl(0 0% 95%);
|
||||||
--card: hsl(240 10% 3.9%);
|
--card: hsl(220 8% 16%);
|
||||||
--card-foreground: hsl(0 0% 98%);
|
--card-foreground: hsl(0 0% 96%);
|
||||||
--popover: hsl(240 10% 3.9%);
|
--popover: hsl(220 8% 16%);
|
||||||
--popover-foreground: hsl(0 0% 98%);
|
--popover-foreground: hsl(0 0% 96%);
|
||||||
--primary: hsl(0 0% 98%);
|
--primary: hsl(0 0% 96%);
|
||||||
--primary-foreground: hsl(240 5.9% 10%);
|
--primary-foreground: hsl(220 8% 14%);
|
||||||
--secondary: hsl(240 3.7% 15.9%);
|
--secondary: hsl(220 7% 22%);
|
||||||
--secondary-foreground: hsl(0 0% 98%);
|
--secondary-foreground: hsl(0 0% 96%);
|
||||||
--muted: hsl(240 3.7% 15.9%);
|
--muted: hsl(220 6% 26%);
|
||||||
--muted-foreground: hsl(240 5% 64.9%);
|
--muted-foreground: hsl(220 12% 72%);
|
||||||
--accent: hsl(240 3.7% 15.9%);
|
--accent: hsl(220 7% 24%);
|
||||||
--accent-foreground: hsl(0 0% 98%);
|
--accent-foreground: hsl(0 0% 96%);
|
||||||
--destructive: hsl(0 62.8% 30.6%);
|
--destructive: hsl(0 65% 52%);
|
||||||
--destructive-foreground: hsl(0 0% 98%);
|
--destructive-foreground: hsl(0 0% 96%);
|
||||||
--border: hsl(240 3.7% 15.9%);
|
--border: hsl(220 6% 30%);
|
||||||
--input: hsl(240 3.7% 15.9%);
|
--input: hsl(220 6% 30%);
|
||||||
--ring: hsl(240 4.9% 83.9%);
|
--ring: hsl(220 15% 65%);
|
||||||
--chart-1: hsl(220 70% 50%);
|
--chart-1: hsl(220 70% 50%);
|
||||||
--chart-2: hsl(160 60% 45%);
|
--chart-2: hsl(160 60% 45%);
|
||||||
--chart-3: hsl(30 80% 55%);
|
--chart-3: hsl(30 80% 55%);
|
||||||
--chart-4: hsl(280 65% 60%);
|
--chart-4: hsl(280 65% 60%);
|
||||||
--chart-5: hsl(340 75% 55%);
|
--chart-5: hsl(340 75% 55%);
|
||||||
--plum: #9d5ba3;
|
--plum: #b06ab6;
|
||||||
--plum-foreground: hsl(0 0% 98%);
|
--plum-foreground: hsl(0 0% 96%);
|
||||||
--sidebar-background: hsl(240 5.9% 10%);
|
--sidebar-background: hsl(220 8% 13%);
|
||||||
--sidebar-foreground: hsl(240 4.8% 95.9%);
|
--sidebar-foreground: hsl(0 0% 94%);
|
||||||
--sidebar-primary: hsl(224.3 76.3% 48%);
|
--sidebar-primary: hsl(224.3 70% 62%);
|
||||||
--sidebar-primary-foreground: hsl(0 0% 100%);
|
--sidebar-primary-foreground: hsl(0 0% 98%);
|
||||||
--sidebar-accent: hsl(240 3.7% 15.9%);
|
--sidebar-accent: hsl(220 7% 21%);
|
||||||
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
|
--sidebar-accent-foreground: hsl(0 0% 94%);
|
||||||
--sidebar-border: hsl(240 3.7% 15.9%);
|
--sidebar-border: hsl(220 7% 28%);
|
||||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
--sidebar-ring: hsl(220 15% 62%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
|
|
@ -152,11 +152,11 @@
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: hsl(0 0% 90%);
|
background-color: hsl(0 0% 70%);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background-color: hsl(0 0% 0%);
|
background-color: hsl(220 8% 18%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue