Refactor routing logic in App component to conditionally use HashRouter or BrowserRouter based on the environment. Update AuthContext and related components to replace 'authenticated' with 'token' for improved authentication handling. Enhance NotesPanel and ObjectTable components to manage loading states and data fetching more effectively, ensuring better user experience and error handling.

This commit is contained in:
Tom Butcher 2025-07-20 20:20:30 +01:00
parent 66e137fac2
commit a20235a953
6 changed files with 102 additions and 61 deletions

View File

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import { import {
BrowserRouter as Router, HashRouter,
BrowserRouter,
Routes, Routes,
Route, Route,
Navigate Navigate
@ -70,8 +71,19 @@ import Hosts from './components/Dashboard/Management/Hosts.jsx'
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.js' import { ElectronProvider } from './components/Dashboard/context/ElectronContext.js'
import AuthCallback from './components/App/AuthCallback.jsx' import AuthCallback from './components/App/AuthCallback.jsx'
const getRouter = () => {
if (
typeof window !== 'undefined' &&
window.location.href.includes('index.html')
) {
return HashRouter
}
return BrowserRouter
}
const AppContent = () => { const AppContent = () => {
const { themeConfig } = useThemeContext() const { themeConfig } = useThemeContext()
const Router = getRouter()
return ( return (
<ConfigProvider theme={themeConfig}> <ConfigProvider theme={themeConfig}>

View File

@ -44,7 +44,7 @@ const EditObjectForm = ({ id, type, style, children }) => {
subscribeToObject, subscribeToObject,
subscribeToLock subscribeToLock
} = useContext(ApiServerContext) } = useContext(ApiServerContext)
const { authenticated } = useContext(AuthContext) const { token } = useContext(AuthContext)
// Validate form on change // Validate form on change
useEffect(() => { useEffect(() => {
form form
@ -92,11 +92,11 @@ const EditObjectForm = ({ id, type, style, children }) => {
}, []) }, [])
useEffect(() => { useEffect(() => {
if (!initialized && id && authenticated == true) { if (!initialized && id && token != null) {
setInitialized(true) setInitialized(true)
handleFetchObject() handleFetchObject()
} }
}, [id, initialized, handleFetchObject, authenticated]) }, [id, initialized, handleFetchObject, token])
useEffect(() => { useEffect(() => {
if (id && connected) { if (id && connected) {

View File

@ -264,6 +264,7 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
const [newNoteOpen, setNewNoteOpen] = useState(false) const [newNoteOpen, setNewNoteOpen] = useState(false)
const [showMarkdown, setShowMarkdown] = useState(false) const [showMarkdown, setShowMarkdown] = useState(false)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [initialized, setInitialized] = useState(false)
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const [newNoteFormLoading, setNewNoteFormLoading] = useState(false) const [newNoteFormLoading, setNewNoteFormLoading] = useState(false)
const [newNoteFormValues, setNewNoteFormValues] = useState({}) const [newNoteFormValues, setNewNoteFormValues] = useState({})
@ -290,19 +291,17 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
.catch(() => setDoneEnabled(false)) .catch(() => setDoneEnabled(false))
}, [newNoteForm, newNoteFormUpdateValues]) }, [newNoteForm, newNoteFormUpdateValues])
const { authenticated, userProfile } = useContext(AuthContext) const { token, userProfile } = useContext(AuthContext)
const { fetchNotes } = useContext(ApiServerContext) const { fetchNotes } = useContext(ApiServerContext)
const fetchData = useCallback( const fetchData = useCallback(
async (id) => { async (id) => {
try { try {
const newData = await fetchNotes(id) const newData = await fetchNotes(id)
setLoading(false)
return newData return newData
} catch (error) { } catch (error) {
setNotes([])
setError(error) setError(error)
setLoading(false) return null
} }
}, },
[fetchNotes] [fetchNotes]
@ -322,24 +321,27 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
const generateNotes = useCallback( const generateNotes = useCallback(
async (id) => { async (id) => {
const notesData = await fetchData(id) const notesData = await fetchData(id)
setLoading(false)
if (notesData == null) {
return null
}
if (notesData.length <= 0) { if (notesData.length <= 0) {
return ( return (
<Spin indicator={<LoadingOutlined />} spinning={loading}> <Card>
<Card> <Flex
<Flex justify='center'
justify='center' gap={'small'}
gap={'small'} style={{ height: '100%' }}
style={{ height: '100%' }} align='center'
align='center' >
> <Text type='secondary'>
<Text type='secondary'> <InfoCircleIcon />
<InfoCircleIcon /> </Text>
</Text> <Text type='secondary'>No notes added.</Text>
<Text type='secondary'>No notes added.</Text> </Flex>
</Flex> </Card>
</Card>
</Spin>
) )
} }
@ -458,10 +460,11 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
}, [_id, generateNotes]) }, [_id, generateNotes])
useEffect(() => { useEffect(() => {
if (authenticated) { if (token != null && !initialized) {
handleReloadData() handleReloadData()
setInitialized(true)
} }
}, [authenticated, handleReloadData]) }, [token, handleReloadData, initialized])
const handleModalOk = async () => { const handleModalOk = async () => {
try { try {

View File

@ -56,7 +56,7 @@ const ObjectTable = forwardRef(
}, },
ref ref
) => { ) => {
const { authenticated } = useContext(AuthContext) const { token } = useContext(AuthContext)
const { fetchObjects, connected, subscribeToObject, subscribeToType } = const { fetchObjects, connected, subscribeToObject, subscribeToType } =
useContext(ApiServerContext) useContext(ApiServerContext)
const isMobile = useMediaQuery({ maxWidth: 768 }) const isMobile = useMediaQuery({ maxWidth: 768 })
@ -163,7 +163,7 @@ const ObjectTable = forwardRef(
setLoading(false) setLoading(false)
setLazyLoading(false) setLazyLoading(false)
return result.data return result.data || []
} catch (error) { } catch (error) {
setPages((prev) => setPages((prev) =>
prev.map((page) => ({ prev.map((page) => ({
@ -302,18 +302,20 @@ const ObjectTable = forwardRef(
// Subscribe to each item in all pages // Subscribe to each item in all pages
pages.forEach((page) => { pages.forEach((page) => {
page.items.forEach((item) => { if (page?.items && page?.items?.length > 0) {
if (!item.isSkeleton) { page.items.forEach((item) => {
const unsubscribe = subscribeToObject( if (!item.isSkeleton) {
item._id, const unsubscribe = subscribeToObject(
type, item._id,
updateEventHandler type,
) updateEventHandler
if (unsubscribe) { )
unsubscribes.push(unsubscribe) if (unsubscribe) {
unsubscribes.push(unsubscribe)
}
} }
} })
}) }
}) })
return () => { return () => {
@ -378,11 +380,11 @@ const ObjectTable = forwardRef(
})) }))
useEffect(() => { useEffect(() => {
if (authenticated && !pages.includes(initialPage) && !initialized) { if (token != null && !pages.includes(initialPage) && !initialized) {
loadInitialPage() loadInitialPage()
setInitialized(true) setInitialized(true)
} }
}, [authenticated, loadInitialPage, initialPage, pages, initialized]) }, [token, loadInitialPage, initialPage, pages, initialized])
const getFilterDropdown = ({ const getFilterDropdown = ({
setSelectedKeys, setSelectedKeys,
@ -497,7 +499,7 @@ const ObjectTable = forwardRef(
fixed: fixed, fixed: fixed,
key: prop.name, key: prop.name,
render: (text, record) => { render: (text, record) => {
if (record.isSkeleton) { if (record?.isSkeleton) {
return ( return (
<Skeleton.Input active size='small' style={{ width: '100%' }} /> <Skeleton.Input active size='small' style={{ width: '100%' }} />
) )

View File

@ -23,7 +23,8 @@ logger.setLevel(config.logLevel)
const ApiServerContext = createContext() const ApiServerContext = createContext()
const ApiServerProvider = ({ children }) => { const ApiServerProvider = ({ children }) => {
const { token, userProfile, authenticated } = useContext(AuthContext) const { token, userProfile, authenticated, setUnauthenticated } =
useContext(AuthContext)
const socketRef = useRef(null) const socketRef = useRef(null)
const [connected, setConnected] = useState(false) const [connected, setConnected] = useState(false)
const [connecting, setConnecting] = useState(false) const [connecting, setConnecting] = useState(false)
@ -347,6 +348,11 @@ const ApiServerProvider = ({ children }) => {
) )
const showError = (error, callback = null) => { const showError = (error, callback = null) => {
const code = error.response.data.code || 'UNKNOWN'
if (code == 'UNAUTHORIZED') {
setUnauthenticated()
return
}
var content = `Error ${error.code} (${error.status}): ${error.message}` var content = `Error ${error.code} (${error.status}): ${error.message}`
if (error.response?.data?.error) { if (error.response?.data?.error) {
content = `${error.response?.data?.error} (${error.status})` content = `${error.response?.data?.error} (${error.status})`
@ -377,14 +383,13 @@ const ApiServerProvider = ({ children }) => {
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
} }
}) })
setFetchLoading(false)
return response.data return response.data
} catch (err) { } catch (err) {
showError(err, () => { showError(err, () => {
fetchObject(id, type) fetchObject(id, type)
}) })
return {} return {}
} finally {
setFetchLoading(false)
} }
} }
@ -397,7 +402,9 @@ const ApiServerProvider = ({ children }) => {
sorter = {}, sorter = {},
onDataChange onDataChange
} = params } = params
if (token == null) {
return []
}
logger.debug('Fetching table data from:', type, { logger.debug('Fetching table data from:', type, {
page, page,
limit, limit,
@ -449,6 +456,9 @@ const ApiServerProvider = ({ children }) => {
// Fetch table data with pagination, filtering, and sorting // Fetch table data with pagination, filtering, and sorting
const fetchObjectsByProperty = async (type, params = {}) => { const fetchObjectsByProperty = async (type, params = {}) => {
if (token == null) {
return []
}
const { filter = {}, properties = [] } = params const { filter = {}, properties = [] } = params
logger.debug('Fetching property object data from:', type, { logger.debug('Fetching property object data from:', type, {
@ -476,8 +486,9 @@ const ApiServerProvider = ({ children }) => {
return newData return newData
} catch (err) { } catch (err) {
showError(err, () => { showError(err, () => {
fetchObjectsByProperty(type, params) fetchObjects(type, params)
}) })
return [] return []
} }
} }
@ -604,7 +615,7 @@ const ApiServerProvider = ({ children }) => {
} }
}) })
const notesData = response.data const notesData = response.data || []
logger.debug('Fetched notes:', notesData.length) logger.debug('Fetched notes:', notesData.length)
return notesData return notesData
} catch (error) { } catch (error) {

View File

@ -15,7 +15,7 @@ import config from '../../../config'
import AppError from '../../App/AppError' import AppError from '../../App/AppError'
import loglevel from 'loglevel' import loglevel from 'loglevel'
import { ElectronContext } from './ElectronContext' import { ElectronContext } from './ElectronContext'
import { useLocation } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
const logger = loglevel.getLogger('ApiServerContext') const logger = loglevel.getLogger('ApiServerContext')
logger.setLevel(config.logLevel) logger.setLevel(config.logLevel)
@ -36,6 +36,7 @@ const AuthProvider = ({ children }) => {
const [authError, setAuthError] = useState(null) const [authError, setAuthError] = useState(null)
const { openExternalUrl, isElectron } = useContext(ElectronContext) const { openExternalUrl, isElectron } = useContext(ElectronContext)
const location = useLocation() const location = useLocation()
const navigate = useNavigate()
// Read token from session storage if present // Read token from session storage if present
useEffect(() => { useEffect(() => {
@ -147,6 +148,11 @@ const AuthProvider = ({ children }) => {
} }
}, [token]) }, [token])
const setUnauthenticated = () => {
setAuthenticated(false)
setShowUnauthorizedModal(true)
}
const refreshToken = useCallback(async () => { const refreshToken = useCallback(async () => {
try { try {
const response = await axios.get(`${config.backendUrl}/auth/refresh`, { const response = await axios.get(`${config.backendUrl}/auth/refresh`, {
@ -260,22 +266,28 @@ const AuthProvider = ({ children }) => {
new URLSearchParams(location.search).get('authCode') || null new URLSearchParams(location.search).get('authCode') || null
if (authCode != null) { if (authCode != null) {
getLoginToken(authCode) getLoginToken(authCode)
if (window && window.history && window.location) { const searchParams = new URLSearchParams(location.search)
const url = new URL(window.location.href) if (searchParams.has('authCode')) {
if (url.searchParams.has('authCode')) { searchParams.delete('authCode')
url.searchParams.delete('authCode') const newSearch = searchParams.toString()
window.history.replaceState( const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
{}, navigate(newPath, { replace: true })
document.title,
url.pathname + url.search
)
}
} }
setInitialized(true) } else if (token == null) {
return setShowUnauthorizedModal(true)
setAuthenticated(false)
} }
setInitialized(true)
} }
}, [checkAuthStatus, location.search, getLoginToken, initialized]) }, [
checkAuthStatus,
location.search,
getLoginToken,
initialized,
location.pathname,
navigate,
token
])
if (authError) { if (authError) {
return <AppError message={authError} showBack={false} /> return <AppError message={authError} showBack={false} />
@ -288,6 +300,7 @@ const AuthProvider = ({ children }) => {
<AuthContext.Provider <AuthContext.Provider
value={{ value={{
authenticated, authenticated,
setUnauthenticated,
loginWithSSO, loginWithSSO,
getLoginToken, getLoginToken,
token, token,