Merge pull request #1124 from danielaskdd/improve-login-flow
Improve login flow
This commit is contained in:
1
lightrag/api/webui/assets/index-BE_O4IWQ.css
Normal file
1
lightrag/api/webui/assets/index-BE_O4IWQ.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -8,8 +8,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="logo.png" />
|
<link rel="icon" type="image/svg+xml" href="logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Lightrag</title>
|
<title>Lightrag</title>
|
||||||
<script type="module" crossorigin src="/webui/assets/index-CSrxfS-k.js"></script>
|
<script type="module" crossorigin src="/webui/assets/index-C56SCRGK.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/webui/assets/index-mPRIIErN.css">
|
<link rel="stylesheet" crossorigin href="/webui/assets/index-BE_O4IWQ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
@@ -5,11 +5,9 @@ import MessageAlert from '@/components/MessageAlert'
|
|||||||
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
||||||
import StatusIndicator from '@/components/graph/StatusIndicator'
|
import StatusIndicator from '@/components/graph/StatusIndicator'
|
||||||
import { healthCheckInterval } from '@/lib/constants'
|
import { healthCheckInterval } from '@/lib/constants'
|
||||||
import { useBackendState, useAuthStore } from '@/stores/state'
|
import { useBackendState } from '@/stores/state'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { navigationService } from '@/services/navigation'
|
|
||||||
import SiteHeader from '@/features/SiteHeader'
|
import SiteHeader from '@/features/SiteHeader'
|
||||||
import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
|
import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
|
||||||
|
|
||||||
@@ -21,22 +19,13 @@ import ApiSite from '@/features/ApiSite'
|
|||||||
import { Tabs, TabsContent } from '@/components/ui/Tabs'
|
import { Tabs, TabsContent } from '@/components/ui/Tabs'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const message = useBackendState.use.message()
|
const message = useBackendState.use.message()
|
||||||
|
|
||||||
// Initialize navigation service
|
|
||||||
useEffect(() => {
|
|
||||||
navigationService.setNavigate(navigate);
|
|
||||||
}, [navigate]);
|
|
||||||
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
||||||
const currentTab = useSettingsStore.use.currentTab()
|
const currentTab = useSettingsStore.use.currentTab()
|
||||||
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
|
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { isAuthenticated } = useAuthStore.getState();
|
|
||||||
if (!enableHealthCheck || !isAuthenticated) return
|
|
||||||
|
|
||||||
// Check immediately
|
// Check immediately
|
||||||
useBackendState.getState().check()
|
useBackendState.getState().check()
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { HashRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
|
import { HashRouter as Router, Routes, Route, useNavigate } from 'react-router-dom'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useAuthStore } from '@/stores/state'
|
import { useAuthStore } from '@/stores/state'
|
||||||
|
import { navigationService } from '@/services/navigation'
|
||||||
import { getAuthStatus } from '@/api/lightrag'
|
import { getAuthStatus } from '@/api/lightrag'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Toaster } from 'sonner'
|
import { Toaster } from 'sonner'
|
||||||
@@ -15,6 +16,12 @@ interface ProtectedRouteProps {
|
|||||||
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
||||||
const { isAuthenticated } = useAuthStore()
|
const { isAuthenticated } = useAuthStore()
|
||||||
const [isChecking, setIsChecking] = useState(true)
|
const [isChecking, setIsChecking] = useState(true)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
// Set navigate function for navigation service
|
||||||
|
useEffect(() => {
|
||||||
|
navigationService.setNavigate(navigate)
|
||||||
|
}, [navigate])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true; // Flag to prevent state updates after unmount
|
let isMounted = true; // Flag to prevent state updates after unmount
|
||||||
@@ -65,17 +72,38 @@ const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// After checking, if still not authenticated, redirect to login
|
// After checking, if still not authenticated
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return <Navigate to="/login" replace />
|
// Get current path and check if it's a direct access
|
||||||
|
const currentPath = window.location.hash.slice(1); // Remove the '#' from hash
|
||||||
|
const isLoginPage = currentPath === '/login';
|
||||||
|
|
||||||
|
// Skip redirect if already on login page
|
||||||
|
if (isLoginPage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-login pages, handle state reset and navigation
|
||||||
|
if (!isLoginPage) {
|
||||||
|
// Use navigation service for redirection
|
||||||
|
console.log('Not authenticated, redirecting to login');
|
||||||
|
navigationService.navigateToLogin();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>
|
return <>{children}</>
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppRouter = () => {
|
const AppContent = () => {
|
||||||
const [initializing, setInitializing] = useState(true)
|
const [initializing, setInitializing] = useState(true)
|
||||||
const { isAuthenticated } = useAuthStore()
|
const { isAuthenticated } = useAuthStore()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
// Set navigate function for navigation service
|
||||||
|
useEffect(() => {
|
||||||
|
navigationService.setNavigate(navigate)
|
||||||
|
}, [navigate])
|
||||||
|
|
||||||
// Check token validity and auth configuration on app initialization
|
// Check token validity and auth configuration on app initialization
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -135,8 +163,6 @@ const AppRouter = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
|
||||||
<Router>
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route
|
<Route
|
||||||
@@ -148,7 +174,15 @@ const AppRouter = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
<Toaster position="top-center" />
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppRouter = () => {
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<Router>
|
||||||
|
<AppContent />
|
||||||
|
<Toaster position="bottom-center" />
|
||||||
</Router>
|
</Router>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
|
@@ -2,7 +2,6 @@ import axios, { AxiosError } from 'axios'
|
|||||||
import { backendBaseUrl } from '@/lib/constants'
|
import { backendBaseUrl } from '@/lib/constants'
|
||||||
import { errorMessage } from '@/lib/utils'
|
import { errorMessage } from '@/lib/utils'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { useAuthStore } from '@/stores/state'
|
|
||||||
import { navigationService } from '@/services/navigation'
|
import { navigationService } from '@/services/navigation'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
@@ -174,13 +173,12 @@ axiosInstance.interceptors.response.use(
|
|||||||
(error: AxiosError) => {
|
(error: AxiosError) => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
localStorage.removeItem('LIGHTRAG-API-TOKEN');
|
// For login API, throw error directly
|
||||||
sessionStorage.clear();
|
if (error.config?.url?.includes('/login')) {
|
||||||
useAuthStore.getState().logout();
|
throw error;
|
||||||
|
}
|
||||||
// Use navigation service to handle redirection
|
// For other APIs, navigate to login page
|
||||||
navigationService.navigateToLogin();
|
navigationService.navigateToLogin();
|
||||||
|
|
||||||
// Return a never-resolving promise to prevent further execution
|
// Return a never-resolving promise to prevent further execution
|
||||||
return new Promise(() => {});
|
return new Promise(() => {});
|
||||||
}
|
}
|
||||||
|
@@ -32,7 +32,7 @@ export default function AppSettings({ className }: AppSettingsProps) {
|
|||||||
return (
|
return (
|
||||||
<Popover open={opened} onOpenChange={setOpened}>
|
<Popover open={opened} onOpenChange={setOpened}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className={cn("h-9 w-9", className)}>
|
<Button variant="ghost" size="icon" className={cn('h-9 w-9', className)}>
|
||||||
<PaletteIcon className="h-5 w-5" />
|
<PaletteIcon className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
@@ -29,8 +29,6 @@ const GraphLabels = () => {
|
|||||||
// Set global flag to indicate we've attempted to fetch in this session
|
// Set global flag to indicate we've attempted to fetch in this session
|
||||||
useGraphStore.getState().setLabelsFetchAttempted(true)
|
useGraphStore.getState().setLabelsFetchAttempted(true)
|
||||||
|
|
||||||
console.log('Fetching graph labels (once per session)...')
|
|
||||||
|
|
||||||
useGraphStore.getState().fetchAllDatabaseLabels()
|
useGraphStore.getState().fetchAllDatabaseLabels()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
labelsLoadedRef.current = true
|
labelsLoadedRef.current = true
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -27,9 +27,7 @@ export default function DocumentManager() {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const health = useBackendState.use.health()
|
const health = useBackendState.use.health()
|
||||||
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
||||||
const { isTabVisible } = useTabVisibility()
|
const currentTab = useSettingsStore.use.currentTab()
|
||||||
const isDocumentsTabVisible = isTabVisible('documents')
|
|
||||||
const initialLoadRef = useRef(false)
|
|
||||||
|
|
||||||
const fetchDocuments = useCallback(async () => {
|
const fetchDocuments = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -53,13 +51,12 @@ export default function DocumentManager() {
|
|||||||
}
|
}
|
||||||
}, [setDocs, t])
|
}, [setDocs, t])
|
||||||
|
|
||||||
// Only fetch documents when the tab becomes visible for the first time
|
// Fetch documents when the tab becomes visible
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDocumentsTabVisible && !initialLoadRef.current) {
|
if (currentTab === 'documents') {
|
||||||
fetchDocuments()
|
fetchDocuments()
|
||||||
initialLoadRef.current = true
|
|
||||||
}
|
}
|
||||||
}, [isDocumentsTabVisible, fetchDocuments])
|
}, [currentTab, fetchDocuments])
|
||||||
|
|
||||||
const scanDocuments = useCallback(async () => {
|
const scanDocuments = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -70,9 +67,9 @@ export default function DocumentManager() {
|
|||||||
}
|
}
|
||||||
}, [t])
|
}, [t])
|
||||||
|
|
||||||
// Only set up polling when the tab is visible and health is good
|
// Set up polling when the documents tab is active and health is good
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDocumentsTabVisible || !health) {
|
if (currentTab !== 'documents' || !health) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +82,7 @@ export default function DocumentManager() {
|
|||||||
}, 5000)
|
}, 5000)
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [health, fetchDocuments, t, isDocumentsTabVisible])
|
}, [health, fetchDocuments, t, currentTab])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="!size-full !rounded-none !border-none">
|
<Card className="!size-full !rounded-none !border-none">
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
|
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
|
||||||
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
|
||||||
// import { MiniMap } from '@react-sigma/minimap'
|
// import { MiniMap } from '@react-sigma/minimap'
|
||||||
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
|
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
|
||||||
import { Settings as SigmaSettings } from 'sigma/settings'
|
import { Settings as SigmaSettings } from 'sigma/settings'
|
||||||
@@ -108,52 +107,39 @@ const GraphEvents = () => {
|
|||||||
const GraphViewer = () => {
|
const GraphViewer = () => {
|
||||||
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
||||||
const sigmaRef = useRef<any>(null)
|
const sigmaRef = useRef<any>(null)
|
||||||
const initAttemptedRef = useRef(false)
|
|
||||||
|
|
||||||
const selectedNode = useGraphStore.use.selectedNode()
|
const selectedNode = useGraphStore.use.selectedNode()
|
||||||
const focusedNode = useGraphStore.use.focusedNode()
|
const focusedNode = useGraphStore.use.focusedNode()
|
||||||
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
|
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
|
||||||
const isFetching = useGraphStore.use.isFetching()
|
const isFetching = useGraphStore.use.isFetching()
|
||||||
const shouldRender = useGraphStore.use.shouldRender() // Rendering control state
|
|
||||||
|
|
||||||
// Get tab visibility
|
|
||||||
const { isTabVisible } = useTabVisibility()
|
|
||||||
const isGraphTabVisible = isTabVisible('knowledge-graph')
|
|
||||||
|
|
||||||
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
||||||
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
||||||
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
||||||
|
|
||||||
// Handle component mount/unmount and tab visibility
|
|
||||||
useEffect(() => {
|
|
||||||
// When component mounts or tab becomes visible
|
|
||||||
if (isGraphTabVisible && !shouldRender && !isFetching && !initAttemptedRef.current) {
|
|
||||||
// If tab is visible but graph is not rendering, try to enable rendering
|
|
||||||
useGraphStore.getState().setShouldRender(true)
|
|
||||||
initAttemptedRef.current = true
|
|
||||||
console.log('Graph viewer initialized')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup function when component unmounts
|
|
||||||
return () => {
|
|
||||||
// Only log cleanup, don't actually clean up the WebGL context
|
|
||||||
// This allows the WebGL context to persist across tab switches
|
|
||||||
console.log('Graph viewer cleanup')
|
|
||||||
}
|
|
||||||
}, [isGraphTabVisible, shouldRender, isFetching])
|
|
||||||
|
|
||||||
// Initialize sigma settings once on component mount
|
// Initialize sigma settings once on component mount
|
||||||
// All dynamic settings will be updated in GraphControl using useSetSettings
|
// All dynamic settings will be updated in GraphControl using useSetSettings
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSigmaSettings(defaultSigmaSettings)
|
setSigmaSettings(defaultSigmaSettings)
|
||||||
|
console.log('Initialized sigma settings')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Clean up sigma instance when component unmounts
|
// Clean up sigma instance when component unmounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
// Clear the sigma instance when component unmounts
|
// TAB is mount twice in vite dev mode, this is a workaround
|
||||||
|
|
||||||
|
const sigma = useGraphStore.getState().sigmaInstance;
|
||||||
|
if (sigma) {
|
||||||
|
try {
|
||||||
|
// Destroy sigma,and clear WebGL context
|
||||||
|
sigma.kill();
|
||||||
useGraphStore.getState().setSigmaInstance(null);
|
useGraphStore.getState().setSigmaInstance(null);
|
||||||
console.log('Cleared sigma instance on unmount');
|
console.log('Cleared sigma instance on Graphviewer unmount');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cleaning up sigma instance:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@@ -4,7 +4,6 @@ import { useAuthStore } from '@/stores/state'
|
|||||||
import { loginToServer, getAuthStatus } from '@/api/lightrag'
|
import { loginToServer, getAuthStatus } from '@/api/lightrag'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader } from '@/components/ui/Card'
|
import { Card, CardContent, CardHeader } from '@/components/ui/Card'
|
||||||
import Input from '@/components/ui/Input'
|
import Input from '@/components/ui/Input'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
@@ -20,7 +19,11 @@ const LoginPage = () => {
|
|||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [checkingAuth, setCheckingAuth] = useState(true)
|
const [checkingAuth, setCheckingAuth] = useState(true)
|
||||||
|
|
||||||
// Check if authentication is configured
|
useEffect(() => {
|
||||||
|
console.log('LoginPage mounted')
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Check if authentication is configured, skip login if not
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true; // Flag to prevent state updates after unmount
|
let isMounted = true; // Flag to prevent state updates after unmount
|
||||||
|
|
||||||
@@ -45,7 +48,7 @@ const LoginPage = () => {
|
|||||||
toast.info(status.message)
|
toast.info(status.message)
|
||||||
}
|
}
|
||||||
navigate('/')
|
navigate('/')
|
||||||
return; // Exit early, no need to set checkingAuth to false
|
return // Exit early, no need to set checkingAuth to false
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to check auth configuration:', error)
|
console.error('Failed to check auth configuration:', error)
|
||||||
@@ -93,10 +96,16 @@ const LoginPage = () => {
|
|||||||
toast.success(t('login.successMessage'))
|
toast.success(t('login.successMessage'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigate to home page after successful login
|
||||||
navigate('/')
|
navigate('/')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login failed...', error)
|
console.error('Login failed...', error)
|
||||||
toast.error(t('login.errorInvalidCredentials'))
|
toast.error(t('login.errorInvalidCredentials'))
|
||||||
|
|
||||||
|
// Clear any existing auth state
|
||||||
|
useAuthStore.getState().logout()
|
||||||
|
// Clear local storage
|
||||||
|
localStorage.removeItem('LIGHTRAG-API-TOKEN')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
@@ -6,8 +6,7 @@ import { useSettingsStore } from '@/stores/settings'
|
|||||||
import { useAuthStore } from '@/stores/state'
|
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 { useNavigate } from 'react-router-dom'
|
import { navigationService } from '@/services/navigation'
|
||||||
|
|
||||||
import { ZapIcon, GithubIcon, LogOutIcon } from 'lucide-react'
|
import { ZapIcon, GithubIcon, LogOutIcon } from 'lucide-react'
|
||||||
|
|
||||||
interface NavigationTabProps {
|
interface NavigationTabProps {
|
||||||
@@ -56,12 +55,10 @@ function TabsNavigation() {
|
|||||||
|
|
||||||
export default function SiteHeader() {
|
export default function SiteHeader() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const { isGuestMode } = useAuthStore()
|
||||||
const { logout, isGuestMode } = useAuthStore()
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
logout()
|
navigationService.navigateToLogin();
|
||||||
navigate('/login')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -8,7 +8,6 @@ import { toast } from 'sonner'
|
|||||||
import { queryGraphs } from '@/api/lightrag'
|
import { queryGraphs } from '@/api/lightrag'
|
||||||
import { useBackendState } from '@/stores/state'
|
import { useBackendState } from '@/stores/state'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
|
||||||
|
|
||||||
import seedrandom from 'seedrandom'
|
import seedrandom from 'seedrandom'
|
||||||
|
|
||||||
@@ -190,10 +189,6 @@ const useLightrangeGraph = () => {
|
|||||||
const nodeToExpand = useGraphStore.use.nodeToExpand()
|
const nodeToExpand = useGraphStore.use.nodeToExpand()
|
||||||
const nodeToPrune = useGraphStore.use.nodeToPrune()
|
const nodeToPrune = useGraphStore.use.nodeToPrune()
|
||||||
|
|
||||||
// Get tab visibility
|
|
||||||
const { isTabVisible } = useTabVisibility()
|
|
||||||
const isGraphTabVisible = isTabVisible('knowledge-graph')
|
|
||||||
|
|
||||||
// Track previous parameters to detect actual changes
|
// Track previous parameters to detect actual changes
|
||||||
const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree })
|
const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree })
|
||||||
|
|
||||||
@@ -248,19 +243,12 @@ const useLightrangeGraph = () => {
|
|||||||
if (!isFetching && !fetchInProgressRef.current &&
|
if (!isFetching && !fetchInProgressRef.current &&
|
||||||
(paramsChanged || !useGraphStore.getState().graphDataFetchAttempted)) {
|
(paramsChanged || !useGraphStore.getState().graphDataFetchAttempted)) {
|
||||||
|
|
||||||
// Only fetch data if the Graph tab is visible and we haven't attempted a fetch yet
|
|
||||||
if (!isGraphTabVisible) {
|
|
||||||
console.log('Graph tab not visible, skipping data fetch');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set flags
|
// Set flags
|
||||||
fetchInProgressRef.current = true
|
fetchInProgressRef.current = true
|
||||||
useGraphStore.getState().setGraphDataFetchAttempted(true)
|
useGraphStore.getState().setGraphDataFetchAttempted(true)
|
||||||
|
|
||||||
const state = useGraphStore.getState()
|
const state = useGraphStore.getState()
|
||||||
state.setIsFetching(true)
|
state.setIsFetching(true)
|
||||||
state.setShouldRender(false) // Disable rendering during data loading
|
|
||||||
|
|
||||||
// Clear selection and highlighted nodes before fetching new graph
|
// Clear selection and highlighted nodes before fetching new graph
|
||||||
state.clearSelection()
|
state.clearSelection()
|
||||||
@@ -305,8 +293,6 @@ const useLightrangeGraph = () => {
|
|||||||
// Reset camera view
|
// Reset camera view
|
||||||
state.setMoveToSelectedNode(true)
|
state.setMoveToSelectedNode(true)
|
||||||
|
|
||||||
// Enable rendering if the tab is visible
|
|
||||||
state.setShouldRender(isGraphTabVisible)
|
|
||||||
state.setIsFetching(false)
|
state.setIsFetching(false)
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error('Error fetching graph data:', error)
|
console.error('Error fetching graph data:', error)
|
||||||
@@ -314,29 +300,12 @@ const useLightrangeGraph = () => {
|
|||||||
// Reset state on error
|
// Reset state on error
|
||||||
const state = useGraphStore.getState()
|
const state = useGraphStore.getState()
|
||||||
state.setIsFetching(false)
|
state.setIsFetching(false)
|
||||||
state.setShouldRender(isGraphTabVisible)
|
|
||||||
dataLoadedRef.current = false
|
dataLoadedRef.current = false
|
||||||
fetchInProgressRef.current = false
|
fetchInProgressRef.current = false
|
||||||
state.setGraphDataFetchAttempted(false)
|
state.setGraphDataFetchAttempted(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [queryLabel, maxQueryDepth, minDegree, isFetching, paramsChanged, isGraphTabVisible, rawGraph, sigmaGraph])
|
}, [queryLabel, maxQueryDepth, minDegree, isFetching, paramsChanged, rawGraph, sigmaGraph])
|
||||||
|
|
||||||
// Update rendering state and handle tab visibility changes
|
|
||||||
useEffect(() => {
|
|
||||||
// When tab becomes visible
|
|
||||||
if (isGraphTabVisible) {
|
|
||||||
// If we have data, enable rendering
|
|
||||||
if (rawGraph) {
|
|
||||||
useGraphStore.getState().setShouldRender(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We no longer reset the fetch attempted flag here to prevent continuous API calls
|
|
||||||
} else {
|
|
||||||
// When tab becomes invisible, disable rendering
|
|
||||||
useGraphStore.getState().setShouldRender(false)
|
|
||||||
}
|
|
||||||
}, [isGraphTabVisible, rawGraph])
|
|
||||||
|
|
||||||
// Handle node expansion
|
// Handle node expansion
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
import { NavigateFunction } from 'react-router-dom';
|
import { NavigateFunction } from 'react-router-dom';
|
||||||
|
import { useAuthStore, useBackendState } from '@/stores/state';
|
||||||
|
import { useGraphStore } from '@/stores/graph';
|
||||||
|
import { useSettingsStore } from '@/stores/settings';
|
||||||
|
|
||||||
class NavigationService {
|
class NavigationService {
|
||||||
private navigate: NavigateFunction | null = null;
|
private navigate: NavigateFunction | null = null;
|
||||||
@@ -7,11 +10,80 @@ class NavigationService {
|
|||||||
this.navigate = navigate;
|
this.navigate = navigate;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigateToLogin() {
|
/**
|
||||||
if (this.navigate) {
|
* Reset all application state to ensure a clean environment.
|
||||||
this.navigate('/login');
|
* This function should be called when:
|
||||||
|
* 1. User logs out
|
||||||
|
* 2. Authentication token expires
|
||||||
|
* 3. Direct access to login page
|
||||||
|
*/
|
||||||
|
resetAllApplicationState() {
|
||||||
|
console.log('Resetting all application state...');
|
||||||
|
|
||||||
|
// Reset graph state
|
||||||
|
const graphStore = useGraphStore.getState();
|
||||||
|
const sigma = graphStore.sigmaInstance;
|
||||||
|
graphStore.reset();
|
||||||
|
graphStore.setGraphDataFetchAttempted(false);
|
||||||
|
graphStore.setLabelsFetchAttempted(false);
|
||||||
|
graphStore.setSigmaInstance(null);
|
||||||
|
|
||||||
|
// Reset backend state
|
||||||
|
useBackendState.getState().clear();
|
||||||
|
|
||||||
|
// Reset retrieval history while preserving other user preferences
|
||||||
|
useSettingsStore.getState().setRetrievalHistory([]);
|
||||||
|
|
||||||
|
// Clear authentication state
|
||||||
|
sessionStorage.clear();
|
||||||
|
|
||||||
|
if (sigma) {
|
||||||
|
sigma.getGraph().clear();
|
||||||
|
sigma.kill();
|
||||||
|
useGraphStore.getState().setSigmaInstance(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle direct access to login page
|
||||||
|
* @returns true if it's a direct access, false if navigated from another page
|
||||||
|
*/
|
||||||
|
handleDirectLoginAccess() {
|
||||||
|
const isDirectAccess = !document.referrer;
|
||||||
|
if (isDirectAccess) {
|
||||||
|
this.resetAllApplicationState();
|
||||||
|
}
|
||||||
|
return isDirectAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to login page and reset application state
|
||||||
|
* @param skipReset whether to skip state reset (used for direct access scenario where reset is already handled)
|
||||||
|
*/
|
||||||
|
navigateToLogin() {
|
||||||
|
if (!this.navigate) {
|
||||||
|
console.error('Navigation function not set');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First navigate to login page
|
||||||
|
this.navigate('/login');
|
||||||
|
|
||||||
|
// Then reset state after navigation
|
||||||
|
setTimeout(() => {
|
||||||
|
this.resetAllApplicationState();
|
||||||
|
useAuthStore.getState().logout();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToHome() {
|
||||||
|
if (!this.navigate) {
|
||||||
|
console.error('Navigation function not set');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.navigate('/');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const navigationService = new NavigationService();
|
export const navigationService = new NavigationService();
|
||||||
|
@@ -74,7 +74,6 @@ interface GraphState {
|
|||||||
|
|
||||||
moveToSelectedNode: boolean
|
moveToSelectedNode: boolean
|
||||||
isFetching: boolean
|
isFetching: boolean
|
||||||
shouldRender: boolean
|
|
||||||
|
|
||||||
// Global flags to track data fetching attempts
|
// Global flags to track data fetching attempts
|
||||||
graphDataFetchAttempted: boolean
|
graphDataFetchAttempted: boolean
|
||||||
@@ -95,7 +94,6 @@ interface GraphState {
|
|||||||
setAllDatabaseLabels: (labels: string[]) => void
|
setAllDatabaseLabels: (labels: string[]) => void
|
||||||
fetchAllDatabaseLabels: () => Promise<void>
|
fetchAllDatabaseLabels: () => Promise<void>
|
||||||
setIsFetching: (isFetching: boolean) => void
|
setIsFetching: (isFetching: boolean) => void
|
||||||
setShouldRender: (shouldRender: boolean) => void
|
|
||||||
|
|
||||||
// 搜索引擎方法
|
// 搜索引擎方法
|
||||||
setSearchEngine: (engine: MiniSearch | null) => void
|
setSearchEngine: (engine: MiniSearch | null) => void
|
||||||
@@ -122,7 +120,6 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
|||||||
|
|
||||||
moveToSelectedNode: false,
|
moveToSelectedNode: false,
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
shouldRender: false,
|
|
||||||
|
|
||||||
// Initialize global flags
|
// Initialize global flags
|
||||||
graphDataFetchAttempted: false,
|
graphDataFetchAttempted: false,
|
||||||
@@ -137,7 +134,6 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
|||||||
|
|
||||||
|
|
||||||
setIsFetching: (isFetching: boolean) => set({ isFetching }),
|
setIsFetching: (isFetching: boolean) => set({ isFetching }),
|
||||||
setShouldRender: (shouldRender: boolean) => set({ shouldRender }),
|
|
||||||
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
|
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
|
||||||
set({ selectedNode: nodeId, moveToSelectedNode }),
|
set({ selectedNode: nodeId, moveToSelectedNode }),
|
||||||
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
|
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
|
||||||
@@ -159,8 +155,7 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
|||||||
rawGraph: null,
|
rawGraph: null,
|
||||||
sigmaGraph: null, // to avoid other components from acccessing graph objects
|
sigmaGraph: null, // to avoid other components from acccessing graph objects
|
||||||
searchEngine: null,
|
searchEngine: null,
|
||||||
moveToSelectedNode: false,
|
moveToSelectedNode: false
|
||||||
shouldRender: false
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -18,11 +18,9 @@ interface BackendState {
|
|||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
showLoginModal: boolean;
|
|
||||||
isGuestMode: boolean; // Add guest mode flag
|
isGuestMode: boolean; // Add guest mode flag
|
||||||
login: (token: string, isGuest?: boolean) => void;
|
login: (token: string, isGuest?: boolean) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
setShowLoginModal: (show: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const useBackendStateStoreBase = create<BackendState>()((set) => ({
|
const useBackendStateStoreBase = create<BackendState>()((set) => ({
|
||||||
@@ -104,14 +102,12 @@ export const useAuthStore = create<AuthState>(set => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isAuthenticated: initialState.isAuthenticated,
|
isAuthenticated: initialState.isAuthenticated,
|
||||||
showLoginModal: false,
|
|
||||||
isGuestMode: initialState.isGuestMode,
|
isGuestMode: initialState.isGuestMode,
|
||||||
|
|
||||||
login: (token, isGuest = false) => {
|
login: (token, isGuest = false) => {
|
||||||
localStorage.setItem('LIGHTRAG-API-TOKEN', token);
|
localStorage.setItem('LIGHTRAG-API-TOKEN', token);
|
||||||
set({
|
set({
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
showLoginModal: false,
|
|
||||||
isGuestMode: isGuest
|
isGuestMode: isGuest
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -122,8 +118,6 @@ export const useAuthStore = create<AuthState>(set => {
|
|||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
isGuestMode: false
|
isGuestMode: false
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
setShowLoginModal: (show) => set({ showLoginModal: show })
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user