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 {
BrowserRouter as Router,
HashRouter,
BrowserRouter,
Routes,
Route,
Navigate
@ -70,8 +71,19 @@ import Hosts from './components/Dashboard/Management/Hosts.jsx'
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.js'
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 { themeConfig } = useThemeContext()
const Router = getRouter()
return (
<ConfigProvider theme={themeConfig}>

View File

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

View File

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

View File

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

View File

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

View File

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