Added better app loading and error handling

This commit is contained in:
Tom Butcher 2025-06-01 23:32:02 +01:00
parent 2ccf770525
commit 60f62df55a
32 changed files with 153 additions and 217 deletions

4
Jenkinsfile vendored
View File

@ -39,10 +39,10 @@ node {
remote.allowAnyHosts = true
// Copy the build directory to the remote server
sshPut remote: remote, from: 'build/*', into: '/srv/farmcontrol-server/'
sshPut remote: remote, from: 'build/*', into: '/srv/farmcontrol-ui/'
// Restart the service using sudo
sshCommand remote: remote, command: 'sudo /bin/systemctl restart farmcontrol-server.service'
sshCommand remote: remote, command: 'sudo /bin/systemctl restart nginx.service'
}
echo 'Pipeline completed successfully!'

View File

@ -6,7 +6,7 @@ import {
Navigate
} from 'react-router-dom'
import { App, ConfigProvider } from 'antd'
import AuthLayout from './components/Auth/AuthLayout.jsx'
import ProductionOverview from './components/Dashboard/Production/ProductionOverview'
import Printers from './components/Dashboard/Production/Printers'
@ -43,10 +43,9 @@ import StockAuditInfo from './components/Dashboard/Inventory/StockAudits/StockAu
import Dashboard from './components/Dashboard/common/Dashboard'
import PrivateRoute from './components/PrivateRoute'
import PublicRoute from './components/PublicRoute.jsx'
import './App.css'
import { SocketProvider } from './components/Dashboard/context/SocketContext.js'
import { AuthProvider } from './components/Auth/AuthContext.js'
import { AuthProvider } from './components/Dashboard/context/AuthContext.js'
import { SpotlightProvider } from './components/Dashboard/context/SpotlightContext.js'
import StockEvents from './components/Dashboard/Inventory/StockEvents.jsx'
import Settings from './components/Dashboard/Management/Settings'
@ -54,6 +53,7 @@ import {
ThemeProvider,
useThemeContext
} from './components/Dashboard/context/ThemeContext'
import AppError from './components/App/AppError'
const AppContent = () => {
const { themeConfig } = useThemeContext()
@ -79,11 +79,6 @@ const AppContent = () => {
/>
}
/>
<Route
path='login'
element={<PublicRoute component={() => <AuthLayout />} />}
/>
<Route
path='/dashboard'
element={<PrivateRoute component={() => <Dashboard />} />}
@ -175,6 +170,15 @@ const AppContent = () => {
/>
<Route path='management/settings' element={<Settings />} />
</Route>
<Route
path='*'
element={
<AppError
message='404! Page not found.'
showRefresh={false}
/>
}
/>
</Routes>
</Router>
</SpotlightProvider>

View File

@ -0,0 +1,65 @@
import React from 'react'
import { Flex, Card, Alert, Button } from 'antd'
import AuthParticles from './AppParticles'
import FarmControlLogo from '../Logos/FarmControlLogo'
import ExclamationOctagonIcon from '../Icons/ExclamationOctagonIcon'
import PropTypes from 'prop-types'
import ArrowLeftIcon from '../Icons/ArrowLeftIcon'
import ReloadIcon from '../Icons/ReloadIcon'
const AppError = ({
message = 'Error Message',
showBack = true,
showRefresh = true
}) => {
const handleBack = () => {
window.history.back()
}
const handleRefresh = () => {
window.location.reload()
}
return (
<>
<AuthParticles />
<Flex
align='center'
justify='center'
vertical
style={{ height: '100vh' }}
gap={'large'}
>
<Card>
<Flex vertical align='center'>
<FarmControlLogo style={{ fontSize: '350px', height: '31px' }} />
</Flex>
</Card>
<Alert
message={message}
icon={<ExclamationOctagonIcon />}
type={'error'}
showIcon
/>
{(showBack || showRefresh) && (
<Flex gap='middle'>
{showBack && (
<Button icon={<ArrowLeftIcon />} onClick={handleBack} />
)}
{showRefresh && (
<Button icon={<ReloadIcon />} onClick={handleRefresh} />
)}
</Flex>
)}
</Flex>
</>
)
}
AppError.propTypes = {
message: PropTypes.string,
showBack: PropTypes.bool,
showRefresh: PropTypes.bool
}
export default AppError

View File

@ -0,0 +1,33 @@
import React from 'react'
import { Flex, Card, Alert } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import AuthParticles from './AppParticles'
import FarmControlLogo from '../Logos/FarmControlLogo'
const AppLoading = () => {
return (
<>
<AuthParticles />
<Flex
align='center'
justify='center'
vertical
style={{ height: '100vh' }}
gap={'large'}
>
<Card>
<Flex vertical align='center'>
<FarmControlLogo style={{ fontSize: '350px', height: '31px' }} />
</Flex>
</Card>
<Alert
message='Loading Farm Control please wait...'
icon={<LoadingOutlined />}
showIcon
/>
</Flex>
</>
)
}
export default AppLoading

View File

@ -3,8 +3,6 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react'
import Particles, { initParticlesEngine } from '@tsparticles/react'
import { loadSlim } from '@tsparticles/slim'
import './Auth.css'
const ParticlesComponent = React.memo(({ options, particlesLoaded }) => {
return (
<Particles
@ -17,7 +15,7 @@ const ParticlesComponent = React.memo(({ options, particlesLoaded }) => {
ParticlesComponent.displayName = 'ParticlesComponent'
const AuthParticles = () => {
const AppParticles = () => {
const [init, setInit] = useState(false)
// this should be run only once per application lifetime
@ -120,4 +118,4 @@ ParticlesComponent.propTypes = {
particlesLoaded: PropTypes.func.isRequired
}
export default AuthParticles
export default AppParticles

View File

@ -1,29 +0,0 @@
/* AuthPage.css */
.auth-container {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-image: url('../../../public/wallpaper.webp');
background-size: cover;
background-position: center;
background-attachment: fixed;
}
.auth-form > h2 {
text-align: center;
}
.auth-form {
width: 300px;
border-radius: 8px;
}
.auth-form-button {
width: 100%;
}
.ant-spin-blur {
filter: blur(2.5px);
}

View File

@ -1,38 +0,0 @@
import PropTypes from 'prop-types'
import React, { useContext } from 'react'
import { Spin, Flex, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import { AuthContext } from './AuthContext'
import AuthParticles from './AuthParticles'
import './Auth.css'
const AuthLayout = ({ children }) => {
const { loading } = useContext(AuthContext)
return (
<>
<AuthParticles />
<Flex
horizontal='true'
align='center'
justify='center'
style={{ paddingTop: '35px' }}
>
<Card style={{ maxWidth: 350 }}>
<Spin
spinning={loading}
indicator={<LoadingOutlined spin />}
size='large'
>
{children}
</Spin>
</Card>
</Flex>
</>
)
}
AuthLayout.propTypes = {
children: PropTypes.node.isRequired
}
export default AuthLayout

View File

@ -1,52 +0,0 @@
import React, { useContext } from 'react'
//import { useNavigate } from 'react-router-dom'
import { Form, Button, Divider, Typography, Flex } from 'antd'
import { UserAddOutlined } from '@ant-design/icons'
import { AuthContext } from './AuthContext'
import AuthLayout from './AuthLayout'
import './Auth.css'
const { Text } = Typography
const LoginUser = () => {
//const [error] = useState('')
//const navigate = useNavigate()
const { loginWithSSO } = useContext(AuthContext)
const handleLogin = async () => {
loginWithSSO('/dashboard/production/overview')
}
return (
<AuthLayout>
<Flex vertical='true' align='center' style={{ marginBottom: 25 }}>
<img
src='/logo512@2x.png'
style={{ width: '100px' }}
alt='Farm Control Logo'
></img>
<h1 style={{ marginTop: 10, marginBottom: 10 }}>Farm Control</h1>
<Text style={{ textAlign: 'center' }}>Please sign in below.</Text>
</Flex>
<Form name='loginForm' className='login-form' onFinish={handleLogin}>
<Form.Item>
<Button
className='auth-form-button'
type='primary'
style={{ width: '250px' }}
htmlType='submit'
>
Login with auth.tombutcher.work
</Button>
</Form.Item>
</Form>
<Divider plain></Divider>
<Button className='auth-form-button' icon={<UserAddOutlined />}>
Register
</Button>
</AuthLayout>
)
}
export default LoginUser

View File

@ -1,56 +0,0 @@
import React, { useState, useContext } from 'react'
import { useNavigate } from 'react-router-dom'
import { Button, Typography, Flex } from 'antd'
import { LockOutlined } from '@ant-design/icons'
import { AuthContext } from './AuthContext'
import PassKeysIcon from '../Icons/PassKeysIcon' // Adjust the path if necessary
import './Auth.css'
import AuthLayout from './AuthLayout'
const { Text } = Typography
const RegisterPasskey = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const navigate = useNavigate()
const { registerPasskey } = useContext(AuthContext)
const [init, setInit] = useState(false)
const handleRegisterPasskey = async (e) => {
const result = await registerPasskey(email, password)
if (result.successful === true) {
setTimeout(() => {
navigate('/dashboard/overview')
}, 500)
} else {
}
}
return (
<AuthLayout>
<Flex vertical='true' align='center' style={{ marginBottom: 25 }}>
<PassKeysIcon style={{ fontSize: '64px' }} />
<h1 style={{ marginTop: 10, marginBottom: 10 }}>Register a Passkey</h1>
<Text style={{ textAlign: 'center' }}>
Please setup a passkey in order to continue. The passkey may use
another device for encryption.
</Text>
</Flex>
<Button
type='primary'
className='auth-form-button'
icon={<LockOutlined />}
onClick={() => {
handleRegisterPasskey()
}}
>
Continue
</Button>
</AuthLayout>
)
}
export default RegisterPasskey

View File

@ -16,7 +16,7 @@ import {
import { createStyles } from 'antd-style'
import { LoadingOutlined } from '@ant-design/icons'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext'
import { SocketContext } from '../context/SocketContext'
import NewFilamentStock from './FilamentStocks/NewFilamentStock'

View File

@ -16,7 +16,7 @@ import {
import { createStyles } from 'antd-style'
import { LoadingOutlined } from '@ant-design/icons'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext'
import NewPartStock from './PartStocks/NewPartStock'
import IdText from '../common/IdText'

View File

@ -6,7 +6,7 @@ import { Table, Button, Flex, Space, message, Dropdown, Typography } from 'antd'
import { createStyles } from 'antd-style'
import { LoadingOutlined } from '@ant-design/icons'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext'
import { SocketContext } from '../context/SocketContext'
import IdText from '../common/IdText'

View File

@ -17,7 +17,7 @@ import {
ClockCircleOutlined
} from '@ant-design/icons'
import { AuthContext } from '../../../Auth/AuthContext'
import { AuthContext } from '../../context/AuthContext'
import IdText from '../../common/IdText'
import TimeDisplay from '../../common/TimeDisplay'

View File

@ -17,7 +17,7 @@ import { createStyles } from 'antd-style'
import { LoadingOutlined, AuditOutlined } from '@ant-design/icons'
import moment from 'moment'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext'
import { SocketContext } from '../context/SocketContext'
import IdText from '../common/IdText'
import TimeDisplay from '../common/TimeDisplay'

View File

@ -21,7 +21,7 @@ import {
import { createStyles } from 'antd-style'
import { LoadingOutlined } from '@ant-design/icons'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext'
import NewFilament from './Filaments/NewFilament'
import IdText from '../common/IdText'
import FilamentIcon from '../../Icons/FilamentIcon'

View File

@ -16,7 +16,7 @@ import {
import { createStyles } from 'antd-style'
import { LoadingOutlined } from '@ant-design/icons'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext'
import NewMaterial from './Materials/NewMaterial'
import IdText from '../common/IdText'

View File

@ -21,7 +21,7 @@ import {
import { createStyles } from 'antd-style'
import { LoadingOutlined, DownloadOutlined } from '@ant-design/icons'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext'
import IdText from '../common/IdText'
import NewProduct from './Products/NewProduct'
import PartIcon from '../../Icons/PartIcon'

View File

@ -20,7 +20,7 @@ import {
import { createStyles } from 'antd-style'
import { LoadingOutlined, DownloadOutlined } from '@ant-design/icons'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext'
import IdText from '../common/IdText'
import TimeDisplay from '../common/TimeDisplay'

View File

@ -18,7 +18,7 @@ import {
InputNumber
} from 'antd'
import { DeleteOutlined, EyeOutlined } from '@ant-design/icons'
import { AuthContext } from '../../../Auth/AuthContext'
import { AuthContext } from '../../context/AuthContext'
import PartIcon from '../../../Icons/PartIcon'
import { StlViewer } from 'react-stl-viewer'
import VendorSelect from '../../common/VendorSelect'

View File

@ -17,7 +17,7 @@ import {
} from 'antd'
import { createStyles } from 'antd-style'
import { LoadingOutlined, ExportOutlined } from '@ant-design/icons'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext'
import IdText from '../common/IdText'
import NewVendor from './Vendors/NewVendor'
import CountryDisplay from '../common/CountryDisplay'

View File

@ -22,7 +22,7 @@ import {
import { createStyles } from 'antd-style'
import { LoadingOutlined, DownloadOutlined } from '@ant-design/icons'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext'
import NewGCodeFile from './GCodeFiles/NewGCodeFile'
import IdText from '../common/IdText'
import GCodeFileIcon from '../../Icons/GCodeFileIcon'

View File

@ -23,7 +23,7 @@ import {
} from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import { AuthContext } from '../../../Auth/AuthContext'
import { AuthContext } from '../../context/AuthContext.js'
import GCodeFileIcon from '../../../Icons/GCodeFileIcon'

View File

@ -21,7 +21,7 @@ import {
import { createStyles } from 'antd-style'
import { LoadingOutlined } from '@ant-design/icons'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext.js'
import { SocketContext } from '../context/SocketContext'
import NewPrintJob from './PrintJobs/NewPrintJob'
import JobState from '../common/JobState'

View File

@ -20,7 +20,7 @@ import {
import { createStyles } from 'antd-style'
import { LoadingOutlined } from '@ant-design/icons'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext'
import PrinterState from '../common/PrinterState'
import NewPrinter from './Printers/NewPrinter'
import IdText from '../common/IdText'

View File

@ -27,7 +27,7 @@ import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel'
import PrinterPositionPanel from '../../common/PrinterPositionPanel'
import PrinterMovementPanel from '../../common/PrinterMovementPanel'
import PrinterState from '../../common/PrinterState'
import { AuthContext } from '../../../Auth/AuthContext'
import { AuthContext } from '../../context/AuthContext'
import PrinterSubJobsTree from '../../common/PrinterJobsTree'
import IdText from '../../common/IdText'

View File

@ -22,7 +22,7 @@ import {
DisconnectOutlined,
MenuOutlined
} from '@ant-design/icons'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext'
import { SocketContext } from '../context/SocketContext'
import { SpotlightContext } from '../context/SpotlightContext'
import { useNavigate, useLocation } from 'react-router-dom'

View File

@ -4,7 +4,7 @@ import { TreeSelect, Badge, Flex, message, Typography } from 'antd'
import React, { useEffect, useState, useContext } from 'react'
import axios from 'axios'
import GCodeFileIcon from '../../Icons/GCodeFileIcon'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext'
import config from '../../../config'

View File

@ -4,7 +4,7 @@ import { TreeSelect, message, Tag } from 'antd'
import React, { useEffect, useState, useContext } from 'react'
import axios from 'axios'
import PrinterState from './PrinterState'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from '../context/AuthContext'
import config from '../../../config'
const PrinterSelect = ({ onChange, disabled, checkable, value = [] }) => {

View File

@ -3,9 +3,10 @@ import React, { createContext, useState, useCallback, useEffect } from 'react'
import axios from 'axios'
import { message, Modal, notification, Progress, Button, Space } from 'antd'
import PropTypes from 'prop-types'
import ExclamationOctogonIcon from '../Icons/ExclamationOctagonIcon'
import InfoCircleIcon from '../Icons/InfoCircleIcon'
import config from '../../config'
import ExclamationOctogonIcon from '../../Icons/ExclamationOctagonIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import config from '../../../config'
import AppError from '../../App/AppError'
const AuthContext = createContext()
@ -20,6 +21,7 @@ const AuthProvider = ({ children }) => {
const [userProfile, setUserProfile] = useState(null)
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false)
const [showUnauthorizedModal, setShowUnauthorizedModal] = useState(false)
const [authError, setAuthError] = useState(null)
const logout = useCallback((redirectUri = '/login') => {
setAuthenticated(false)
@ -40,6 +42,7 @@ const AuthProvider = ({ children }) => {
// Function to check if the user is logged in
const checkAuthStatus = useCallback(async () => {
setLoading(true)
setAuthError(null)
try {
// Make a call to your backend to check auth status
const response = await axios.get(`${config.backendUrl}/auth/user`, {
@ -54,11 +57,14 @@ const AuthProvider = ({ children }) => {
setUserProfile(response.data)
} else {
setAuthenticated(false)
setAuthError('Failed to authenticate user.')
}
} catch (error) {
console.log('Auth check failed', error)
if (error.response?.status === 401) {
setShowUnauthorizedModal(true)
} else {
setAuthError('Error connecting to authentication service.')
}
setAuthenticated(false)
} finally {
@ -179,6 +185,10 @@ const AuthProvider = ({ children }) => {
checkAuthStatus()
}, [checkAuthStatus])
if (authError) {
return <AppError message={authError} showBack={false} />
}
return (
<>
{contextHolder}

View File

@ -9,7 +9,7 @@ import React, {
import io from 'socket.io-client'
import { message, notification } from 'antd'
import PropTypes from 'prop-types'
import { AuthContext } from '../../Auth/AuthContext'
import { AuthContext } from './AuthContext'
import config from '../../../config'
const SocketContext = createContext()

View File

@ -1,8 +1,8 @@
// PrivateRoute.js
import PropTypes from 'prop-types'
import React, { useContext } from 'react'
//import { Navigate } from 'react-router-dom'
import { AuthContext } from './Auth/AuthContext'
import { AuthContext } from './Dashboard/context/AuthContext'
import AuthLoading from './App/AppLoading'
const PrivateRoute = ({ component: Component }) => {
const { authenticated, loading, showSessionExpiredModal } =
@ -10,7 +10,7 @@ const PrivateRoute = ({ component: Component }) => {
// Show loading state while auth state is being determined
if (loading) {
return <div>Loading...</div>
return <AuthLoading />
}
// Redirect to login if not authenticated

View File

@ -2,14 +2,15 @@
import PropTypes from 'prop-types'
import React, { useContext } from 'react'
import { Navigate } from 'react-router-dom'
import { AuthContext } from './Auth/AuthContext'
import { AuthContext } from './Dashboard/context/AuthContext'
import AuthLoading from './App/AppLoading'
const PublicRoute = ({ component: Component }) => {
const { authenticated, loading } = useContext(AuthContext)
// Show loading state while auth state is being determined
if (loading) {
return <div>Loading...</div>
return <AuthLoading />
}
// Redirect to login if not authenticated