Started app update implementation.
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

This commit is contained in:
Tom Butcher 2026-06-21 01:59:03 +01:00
parent c581705cdd
commit afbab60ab9
4 changed files with 237 additions and 1 deletions

View File

@ -0,0 +1,186 @@
import { useContext, useEffect, useMemo, useState } from 'react'
import {
Button,
Collapse,
Descriptions,
Empty,
Flex,
Select,
Space,
Typography
} from 'antd'
import { CaretLeftOutlined } from '@ant-design/icons'
import { ApiServerContext } from '../context/ApiServerContext'
import useCollapseState from '../hooks/useCollapseState'
const { Title, Text, Link } = Typography
const { Option } = Select
const AppUpdate = () => {
const { fetchAppUpdateBranches, fetchAppUpdateCurrent } =
useContext(ApiServerContext)
const [collapseState, updateCollapseState] = useCollapseState('AppUpdate', {
updater: true
})
const [branches, setBranches] = useState([])
const [selectedBranch, setSelectedBranch] = useState(undefined)
const [branchLoading, setBranchLoading] = useState(false)
const [checking, setChecking] = useState(false)
const [currentUpdate, setCurrentUpdate] = useState(null)
useEffect(() => {
const loadBranches = async () => {
setBranchLoading(true)
const availableBranches = await fetchAppUpdateBranches()
setBranches(availableBranches)
if (availableBranches.length > 0) {
setSelectedBranch((previous) =>
previous && availableBranches.includes(previous)
? previous
: availableBranches[0]
)
}
setBranchLoading(false)
}
loadBranches()
}, [fetchAppUpdateBranches])
const branchOptions = useMemo(
() =>
branches.map((branch) => (
<Option key={branch} value={branch}>
{branch}
</Option>
)),
[branches]
)
const handleCheckForUpdates = async () => {
if (!selectedBranch) return
setChecking(true)
const updateData = await fetchAppUpdateCurrent(selectedBranch)
setCurrentUpdate(updateData)
setChecking(false)
}
const buildTimestamp = currentUpdate?.buildTimestamp
? new Date(currentUpdate.buildTimestamp).toLocaleString()
: 'Unknown'
return (
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
<Flex vertical gap='large'>
<Collapse
ghost
expandIconPosition='end'
activeKey={collapseState.updater ? ['1'] : []}
onChange={(keys) => updateCollapseState('updater', keys.length > 0)}
expandIcon={({ isActive }) => (
<CaretLeftOutlined
rotate={isActive ? 90 : 0}
style={{ paddingTop: '9px' }}
/>
)}
className='no-h-padding-collapse'
>
<Collapse.Panel
header={
<Flex
align='center'
justify='space-between'
style={{ width: '100%' }}
>
<Title level={5} style={{ margin: 0 }}>
Application Updater
</Title>
</Flex>
}
key='1'
>
<Descriptions bordered column={1}>
<Descriptions.Item label='Branch'>
<Select
value={selectedBranch}
onChange={setSelectedBranch}
style={{ width: '100%' }}
loading={branchLoading}
placeholder='Select a branch'
>
{branchOptions}
</Select>
</Descriptions.Item>
<Descriptions.Item label='Actions'>
<Button
type='primary'
onClick={handleCheckForUpdates}
loading={checking}
disabled={!selectedBranch}
>
Check for Updates
</Button>
</Descriptions.Item>
</Descriptions>
<div style={{ marginTop: 16 }}>
{currentUpdate ? (
<Descriptions bordered column={1} title='Latest Build'>
<Descriptions.Item label='Branch'>
{currentUpdate.branch || selectedBranch}
</Descriptions.Item>
<Descriptions.Item label='Build Number'>
{currentUpdate.buildNumber || 'Unknown'}
</Descriptions.Item>
<Descriptions.Item label='Build Source'>
{currentUpdate.buildSource || 'Unknown'}
</Descriptions.Item>
<Descriptions.Item label='Build Status'>
{currentUpdate.buildResult || 'Unknown'}
</Descriptions.Item>
<Descriptions.Item label='Build Time'>
{buildTimestamp}
</Descriptions.Item>
<Descriptions.Item label='Build URL'>
{currentUpdate.buildUrl ? (
<Link href={currentUpdate.buildUrl} target='_blank'>
Open Jenkins Build
</Link>
) : (
<Text type='secondary'>No build URL available</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Artifacts'>
{Array.isArray(currentUpdate.artifacts) &&
currentUpdate.artifacts.length > 0 ? (
<Space direction='vertical'>
{currentUpdate.artifacts.map((artifact) => (
<Link
key={artifact.url}
href={artifact.url}
target='_blank'
>
{artifact.fileName || artifact.relativePath}
</Link>
))}
</Space>
) : (
<Text type='secondary'>No artifacts published</Text>
)}
</Descriptions.Item>
</Descriptions>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description='No update check has been run yet'
/>
)}
</div>
</Collapse.Panel>
</Collapse>
</Flex>
</div>
)
}
export default AppUpdate

View File

@ -1678,6 +1678,46 @@ const ApiServerProvider = ({ children }) => {
return response.data return response.data
}, []) }, [])
const fetchAppUpdateBranches = useCallback(async () => {
try {
const response = await axios.get(`${config.backendUrl}/appupdate/branches`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
return Array.isArray(response.data?.branches) ? response.data.branches : []
} catch (err) {
console.error(err)
showError(err, () => {
fetchAppUpdateBranches()
})
return []
}
}, [token])
const fetchAppUpdateCurrent = useCallback(
async (branch) => {
try {
const response = await axios.get(`${config.backendUrl}/appupdate/current`, {
params: { branch },
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
fetchAppUpdateCurrent(branch)
})
return null
}
},
[token]
)
const flushFile = async (id) => { const flushFile = async (id) => {
logger.debug('Flushing file...') logger.debug('Flushing file...')
try { try {
@ -1774,7 +1814,9 @@ const ApiServerProvider = ({ children }) => {
getMarketplaceAuthUrl, getMarketplaceAuthUrl,
refreshMarketplaceAuth, refreshMarketplaceAuth,
completeAppLaunchSession, completeAppLaunchSession,
getAppLaunchSession getAppLaunchSession,
fetchAppUpdateBranches,
fetchAppUpdateCurrent
}} }}
> >
{contextHolder} {contextHolder}

View File

@ -143,6 +143,12 @@ const managementSidebarItems = [
label: 'Settings', label: 'Settings',
path: '/dashboard/management/settings' path: '/dashboard/management/settings'
}, },
{
key: 'appUpdate',
iconKey: 'settings',
label: 'App Update',
path: '/dashboard/management/appupdate'
},
{ {
key: 'files', key: 'files',
iconKey: 'file', iconKey: 'file',

View File

@ -24,6 +24,7 @@ const CourierInfo = lazy(() => import('../components/Dashboard/Management/Courie
const CourierServices = lazy(() => import('../components/Dashboard/Management/CourierServices')) const CourierServices = lazy(() => import('../components/Dashboard/Management/CourierServices'))
const CourierServiceInfo = lazy(() => import('../components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx')) const CourierServiceInfo = lazy(() => import('../components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx'))
const Settings = lazy(() => import('../components/Dashboard/Management/Settings')) const Settings = lazy(() => import('../components/Dashboard/Management/Settings'))
const AppUpdate = lazy(() => import('../components/Dashboard/Management/AppUpdate'))
const AuditLogs = lazy(() => import('../components/Dashboard/Management/AuditLogs.jsx')) const AuditLogs = lazy(() => import('../components/Dashboard/Management/AuditLogs.jsx'))
const NoteTypes = lazy(() => import('../components/Dashboard/Management/NoteTypes.jsx')) const NoteTypes = lazy(() => import('../components/Dashboard/Management/NoteTypes.jsx'))
const NoteTypeInfo = lazy(() => import('../components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx')) const NoteTypeInfo = lazy(() => import('../components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx'))
@ -206,6 +207,7 @@ const ManagementRoutes = [
element={<AppPasswordInfo />} element={<AppPasswordInfo />}
/>, />,
<Route key='settings' path='management/settings' element={<Settings />} />, <Route key='settings' path='management/settings' element={<Settings />} />,
<Route key='appupdate' path='management/appupdate' element={<AppUpdate />} />,
<Route key='auditlogs' path='management/auditlogs' element={<AuditLogs />} />, <Route key='auditlogs' path='management/auditlogs' element={<AuditLogs />} />,
<Route key='taxrates' path='management/taxrates' element={<TaxRates />} />, <Route key='taxrates' path='management/taxrates' element={<TaxRates />} />,
<Route <Route