From 4363f08f50f67b209f6236716a051f460b865eb6 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 21 Jun 2026 13:19:09 +0100 Subject: [PATCH] Implemented about page. --- Jenkinsfile | 6 +- assets/stylesheets/App.css | 6 + public/appupdate.js | 0 public/mainWindow.js | 6 + src/components/Dashboard/Management/About.jsx | 180 ++++++++++++++++ .../Dashboard/common/DashboardBreadcrumb.jsx | 3 +- .../Dashboard/context/ApiServerContext.jsx | 73 +++++-- .../Dashboard/context/ElectronContext.jsx | 8 +- src/components/Icons/sidebarIconMap.jsx | 2 + src/database/sidebars/management.js | 6 + src/routes/ManagementRoutes.jsx | 201 ++++++++++++++---- 11 files changed, 426 insertions(+), 65 deletions(-) create mode 100644 public/appupdate.js create mode 100644 src/components/Dashboard/Management/About.jsx diff --git a/Jenkinsfile b/Jenkinsfile index 6b5cf9c..04a1a07 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -20,7 +20,7 @@ def deploy() { stage('Build (Ubuntu)') { nodejs(nodeJSInstallationName: 'Node23') { - sh 'NODE_ENV=production pnpm build:cloudflare' + sh "VITE_BUILD_NUMBER=${env.BUILD_NUMBER} NODE_ENV=production pnpm build:cloudflare" } } @@ -70,9 +70,9 @@ def buildOnLabel(label, buildCommand) { stage("Build (${label})") { nodejs(nodeJSInstallationName: 'Node23') { if (isUnix()) { - sh "NODE_ENV=production ${buildCommand}" + sh "VITE_BUILD_NUMBER=${env.BUILD_NUMBER} NODE_ENV=production ${buildCommand}" } else { - bat "set NODE_ENV=production && ${buildCommand}" + bat "set VITE_BUILD_NUMBER=${env.BUILD_NUMBER} && set NODE_ENV=production && ${buildCommand}" } } } diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index f7ff927..96d4d41 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -596,3 +596,9 @@ body { .ant-table-wrapper .ant-table-filter-column { align-items: center; } + +span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { + width: 50px; + min-width: 0; + height: 20px; +} diff --git a/public/appupdate.js b/public/appupdate.js new file mode 100644 index 0000000..e69de29 diff --git a/public/mainWindow.js b/public/mainWindow.js index 28e5929..39f6d66 100644 --- a/public/mainWindow.js +++ b/public/mainWindow.js @@ -206,6 +206,10 @@ export function getWindow() { return win } +export function getElectronVersion() { + return process.versions.electron +} + export function setupMainWindowIPC() { // IPC handler to get window state ipcMain.handle('window-state', () => { @@ -267,6 +271,8 @@ export function setupMainWindowIPC() { applyApplicationMenu() return true }) + + ipcMain.handle('electron-version', () => getElectronVersion()) } export function setupMainWindowAppEvents(app) { diff --git a/src/components/Dashboard/Management/About.jsx b/src/components/Dashboard/Management/About.jsx new file mode 100644 index 0000000..78f95b3 --- /dev/null +++ b/src/components/Dashboard/Management/About.jsx @@ -0,0 +1,180 @@ +import { useContext, useEffect, useState } from 'react' +import { + Flex, + Typography, + Button, + Dropdown, + Skeleton, + Tag, + Divider +} from 'antd' +import useCollapseState from '../hooks/useCollapseState' +import InfoCollapse from '../common/InfoCollapse' +import InfoCircleIcon from '../../Icons/InfoCircleIcon' +import ReloadIcon from '../../Icons/ReloadIcon' +import DeveloperIcon from '../../Icons/DeveloperIcon' +import { version as appVersion } from '../../../../package.json' +import { ApiServerContext } from '../context/ApiServerContext' +import { AuthContext } from '../context/AuthContext' +import { ElectronContext } from '../context/ElectronContext' +import { useMediaQuery } from 'react-responsive' +const { Title, Text, Link } = Typography + +const About = () => { + const [collapseState, updateCollapseState] = useCollapseState('About', { + updater: true + }) + const { token } = useContext(AuthContext) + const actions = [ + { + label: 'Check for Updates', + icon: , + onClick: () => { + console.log('Check for Updates') + } + } + ] + + const buildNumber = import.meta.env.VITE_BUILD_NUMBER + ? 'b' + import.meta.env.VITE_BUILD_NUMBER + : 'dev' + const developmentMode = import.meta.env.MODE === 'development' + + const { fetchApiServerVersion, fetchWsServerVersion } = + useContext(ApiServerContext) + const { isElectron, getElectronVersion } = useContext(ElectronContext) + const isMobile = useMediaQuery({ maxWidth: 768 }) + + useEffect(() => { + if (token) { + fetchApiServerVersion().then((version) => { + setApiServerVersion(version) + }) + fetchWsServerVersion().then((version) => { + setWsServerVersion(version) + }) + } + }, [fetchApiServerVersion, fetchWsServerVersion, token]) + + useEffect(() => { + if (!isElectron) return + + getElectronVersion() + .then((version) => { + setElectronVersion(version || 'unknown') + }) + .catch(() => { + setElectronVersion('unknown') + }) + }, [getElectronVersion, isElectron]) + + const [apiServerVersion, setApiServerVersion] = useState(null) + const [wsServerVersion, setWsServerVersion] = useState(null) + const [electronVersion, setElectronVersion] = useState(null) + + const apiServerVersionText = apiServerVersion ? ( + + {`v${apiServerVersion.version}-${apiServerVersion.buildNumber}`} + + ) : ( + + ) + const wsServerVersionText = wsServerVersion ? ( + {`v${wsServerVersion.version}-${wsServerVersion.buildNumber}`} + ) : ( + + ) + const electronVersionText = electronVersion ? ( + {`v${electronVersion}`} + ) : ( + + ) + + return ( +
+ + + + + + + + } + canCollapse={false} + active={collapseState.purchaseOrderStats} + onToggle={(isActive) => + updateCollapseState('purchaseOrderStats', isActive) + } + className='no-t-padding-collapse' + collapseKey='purchaseOrderStats' + > + + Farm Control Logo + + + + Farm Control + + {developmentMode && !isMobile && ( + } + > + Development + + )} + + + 3D Printer ERP and Control Software. + + + + User Interface:{' '} + + v{appVersion}-{buildNumber} + + + {isElectron && ( + + Electron: {electronVersionText} + + )} + + REST API: {apiServerVersionText} + + Web Socket: {wsServerVersionText} + + + + {developmentMode && isMobile && ( + } + > + Development + + )} + + Jenkins + + + GitHub + + + + + + +
+ ) +} + +export default About diff --git a/src/components/Dashboard/common/DashboardBreadcrumb.jsx b/src/components/Dashboard/common/DashboardBreadcrumb.jsx index 73e96b5..b042a15 100644 --- a/src/components/Dashboard/common/DashboardBreadcrumb.jsx +++ b/src/components/Dashboard/common/DashboardBreadcrumb.jsx @@ -17,7 +17,8 @@ const breadcrumbNameMap = { info: 'Info', design: 'Design', control: 'Control', - preview: 'Preview' + preview: 'Preview', + about: 'About' } const mainSections = ['production', 'inventory', 'management', 'developer'] diff --git a/src/components/Dashboard/context/ApiServerContext.jsx b/src/components/Dashboard/context/ApiServerContext.jsx index 8132d9f..2b1036c 100644 --- a/src/components/Dashboard/context/ApiServerContext.jsx +++ b/src/components/Dashboard/context/ApiServerContext.jsx @@ -744,7 +744,7 @@ const ApiServerProvider = ({ children }) => { [offLockEvent] ) - const showError = (error, callback = null) => { + const showError = useCallback((error, callback = null) => { const code = error.response.data.code || 'UNKNOWN' if (code == 'UNAUTHORIZED') { setUnauthenticated() @@ -757,7 +757,7 @@ const ApiServerProvider = ({ children }) => { setErrorModalContent(content) setRetryCallback(() => callback) setShowErrorModal(true) - } + }, [setUnauthenticated]) const handleRetry = () => { setShowErrorModal(false) @@ -1678,15 +1678,55 @@ const ApiServerProvider = ({ children }) => { return response.data }, []) - const fetchAppUpdateBranches = useCallback(async () => { + const fetchApiServerVersion = useCallback(async () => { try { - const response = await axios.get(`${config.backendUrl}/appupdate/branches`, { + const response = await axios.get(`${config.backendUrl}/server/version`, { headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } }) - return Array.isArray(response.data?.branches) ? response.data.branches : [] + return response.data + } catch (err) { + console.error(err) + showError(err, () => { + fetchApiServerVersion() + }) + return null + } + }, [token, showError]) + + const fetchWsServerVersion = useCallback(async () => { + try { + if (socketRef.current && socketRef.current.connected) { + return await new Promise((resolve) => { + socketRef.current.emit('getServerVersion', {}, resolve) + }) + } + return null + } catch (err) { + console.error(err) + showError(err, () => { + fetchWsServerVersion() + }) + return null + } + }, [showError]) + + 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, () => { @@ -1694,18 +1734,21 @@ const ApiServerProvider = ({ children }) => { }) return [] } - }, [token]) + }, [token, showError]) const fetchAppUpdateCurrent = useCallback( async (branch) => { try { - const response = await axios.get(`${config.backendUrl}/appupdate/current`, { - params: { branch }, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${token}` + 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) @@ -1715,7 +1758,7 @@ const ApiServerProvider = ({ children }) => { return null } }, - [token] + [token, showError] ) const flushFile = async (id) => { @@ -1816,7 +1859,9 @@ const ApiServerProvider = ({ children }) => { completeAppLaunchSession, getAppLaunchSession, fetchAppUpdateBranches, - fetchAppUpdateCurrent + fetchAppUpdateCurrent, + fetchWsServerVersion, + fetchApiServerVersion }} > {contextHolder} diff --git a/src/components/Dashboard/context/ElectronContext.jsx b/src/components/Dashboard/context/ElectronContext.jsx index c58328b..31e7ff7 100644 --- a/src/components/Dashboard/context/ElectronContext.jsx +++ b/src/components/Dashboard/context/ElectronContext.jsx @@ -152,6 +152,11 @@ const ElectronProvider = ({ children }) => { [electronAvailable] ) + const getElectronVersion = useCallback(async () => { + if (!electronAvailable || !ipcRenderer) return null + return await ipcRenderer.invoke('electron-version') + }, [electronAvailable]) + return ( { getToken, setToken, resizeSpotlightWindow, - setSidebarViewMenu + setSidebarViewMenu, + getElectronVersion }} > {children} diff --git a/src/components/Icons/sidebarIconMap.jsx b/src/components/Icons/sidebarIconMap.jsx index a641cef..6f35de5 100644 --- a/src/components/Icons/sidebarIconMap.jsx +++ b/src/components/Icons/sidebarIconMap.jsx @@ -48,6 +48,7 @@ import CourierServiceIcon from './CourierServiceIcon' import TaxRateIcon from './TaxRateIcon' import TaxRecordIcon from './TaxRecordIcon' import AppPasswordIcon from './AppPasswordIcon' +import InfoCircleIcon from './InfoCircleIcon' const toEmoji = (emoji) => {emoji} @@ -102,6 +103,7 @@ const sidebarIconMap = { taxRate: , taxRecord: , appPassword: , + infoCircle: , sessionStorage: toEmoji('🗃️'), authDebug: toEmoji('🔐'), apiDebug: toEmoji('🌐') diff --git a/src/database/sidebars/management.js b/src/database/sidebars/management.js index d0d2e5d..0b53c3c 100644 --- a/src/database/sidebars/management.js +++ b/src/database/sidebars/management.js @@ -168,6 +168,12 @@ const managementSidebarItems = [ label: 'Developer', path: '/dashboard/developer/sessionstorage', devOnly: true + }, + { + key: 'about', + iconKey: 'infoCircle', + label: 'About', + path: '/dashboard/management/about' } ] diff --git a/src/routes/ManagementRoutes.jsx b/src/routes/ManagementRoutes.jsx index dcb7bab..e7c348a 100644 --- a/src/routes/ManagementRoutes.jsx +++ b/src/routes/ManagementRoutes.jsx @@ -1,55 +1,155 @@ import { lazy } from 'react' import { Route } from 'react-router-dom' -const Filaments = lazy(() => import('../components/Dashboard/Management/Filaments')) -const FilamentInfo = lazy(() => import('../components/Dashboard/Management/Filaments/FilamentInfo.jsx')) -const FilamentSkus = lazy(() => import('../components/Dashboard/Management/FilamentSkus.jsx')) -const FilamentSkuInfo = lazy(() => import('../components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx')) +const Filaments = lazy( + () => import('../components/Dashboard/Management/Filaments') +) +const FilamentInfo = lazy( + () => import('../components/Dashboard/Management/Filaments/FilamentInfo.jsx') +) +const FilamentSkus = lazy( + () => import('../components/Dashboard/Management/FilamentSkus.jsx') +) +const FilamentSkuInfo = lazy( + () => + import('../components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx') +) const Parts = lazy(() => import('../components/Dashboard/Management/Parts.jsx')) -const PartInfo = lazy(() => import('../components/Dashboard/Management/Parts/PartInfo.jsx')) -const PartSkus = lazy(() => import('../components/Dashboard/Management/PartSkus.jsx')) -const PartSkuInfo = lazy(() => import('../components/Dashboard/Management/PartSkus/PartSkuInfo.jsx')) -const Products = lazy(() => import('../components/Dashboard/Management/Products.jsx')) -const ProductInfo = lazy(() => import('../components/Dashboard/Management/Products/ProductInfo.jsx')) -const ProductCategories = lazy(() => import('../components/Dashboard/Management/ProductCategories.jsx')) -const ProductCategoryInfo = lazy(() => import('../components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx')) -const ProductSkus = lazy(() => import('../components/Dashboard/Management/ProductSkus.jsx')) -const ProductSkuInfo = lazy(() => import('../components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx')) +const PartInfo = lazy( + () => import('../components/Dashboard/Management/Parts/PartInfo.jsx') +) +const PartSkus = lazy( + () => import('../components/Dashboard/Management/PartSkus.jsx') +) +const PartSkuInfo = lazy( + () => import('../components/Dashboard/Management/PartSkus/PartSkuInfo.jsx') +) +const Products = lazy( + () => import('../components/Dashboard/Management/Products.jsx') +) +const ProductInfo = lazy( + () => import('../components/Dashboard/Management/Products/ProductInfo.jsx') +) +const ProductCategories = lazy( + () => import('../components/Dashboard/Management/ProductCategories.jsx') +) +const ProductCategoryInfo = lazy( + () => + import('../components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx') +) +const ProductSkus = lazy( + () => import('../components/Dashboard/Management/ProductSkus.jsx') +) +const ProductSkuInfo = lazy( + () => + import('../components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx') +) const Vendors = lazy(() => import('../components/Dashboard/Management/Vendors')) -const VendorInfo = lazy(() => import('../components/Dashboard/Management/Vendors/VendorInfo')) -const Materials = lazy(() => import('../components/Dashboard/Management/Materials')) -const MaterialInfo = lazy(() => import('../components/Dashboard/Management/Materials/MaterialInfo.jsx')) -const Couriers = lazy(() => import('../components/Dashboard/Management/Couriers')) -const CourierInfo = lazy(() => import('../components/Dashboard/Management/Couriers/CourierInfo.jsx')) -const CourierServices = lazy(() => import('../components/Dashboard/Management/CourierServices')) -const CourierServiceInfo = lazy(() => import('../components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx')) -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 NoteTypes = lazy(() => import('../components/Dashboard/Management/NoteTypes.jsx')) -const NoteTypeInfo = lazy(() => import('../components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx')) -const NoteInfo = lazy(() => import('../components/Dashboard/Management/Notes/NoteInfo.jsx')) +const VendorInfo = lazy( + () => import('../components/Dashboard/Management/Vendors/VendorInfo') +) +const Materials = lazy( + () => import('../components/Dashboard/Management/Materials') +) +const MaterialInfo = lazy( + () => import('../components/Dashboard/Management/Materials/MaterialInfo.jsx') +) +const Couriers = lazy( + () => import('../components/Dashboard/Management/Couriers') +) +const CourierInfo = lazy( + () => import('../components/Dashboard/Management/Couriers/CourierInfo.jsx') +) +const CourierServices = lazy( + () => import('../components/Dashboard/Management/CourierServices') +) +const CourierServiceInfo = lazy( + () => + import('../components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx') +) +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 NoteTypes = lazy( + () => import('../components/Dashboard/Management/NoteTypes.jsx') +) +const NoteTypeInfo = lazy( + () => import('../components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx') +) +const NoteInfo = lazy( + () => import('../components/Dashboard/Management/Notes/NoteInfo.jsx') +) const Users = lazy(() => import('../components/Dashboard/Management/Users.jsx')) -const UserInfo = lazy(() => import('../components/Dashboard/Management/Users/UserInfo.jsx')) -const AppPasswords = lazy(() => import('../components/Dashboard/Management/AppPasswords.jsx')) -const AppPasswordInfo = lazy(() => import('../components/Dashboard/Management/AppPasswords/AppPasswordInfo.jsx')) +const UserInfo = lazy( + () => import('../components/Dashboard/Management/Users/UserInfo.jsx') +) +const AppPasswords = lazy( + () => import('../components/Dashboard/Management/AppPasswords.jsx') +) +const AppPasswordInfo = lazy( + () => + import('../components/Dashboard/Management/AppPasswords/AppPasswordInfo.jsx') +) const Hosts = lazy(() => import('../components/Dashboard/Management/Hosts.jsx')) -const HostInfo = lazy(() => import('../components/Dashboard/Management/Hosts/HostInfo.jsx')) -const DocumentSizes = lazy(() => import('../components/Dashboard/Management/DocumentSizes.jsx')) -const DocumentSizeInfo = lazy(() => import('../components/Dashboard/Management/DocumentSizes/DocumentSizeInfo.jsx')) -const DocumentTemplates = lazy(() => import('../components/Dashboard/Management/DocumentTemplates.jsx')) -const DocumentTemplateInfo = lazy(() => import('../components/Dashboard/Management/DocumentTemplates/DocumentTemplateInfo.jsx')) -const DocumentPrinters = lazy(() => import('../components/Dashboard/Management/DocumentPrinters.jsx')) -const DocumentPrinterInfo = lazy(() => import('../components/Dashboard/Management/DocumentPrinters/DocumentPrinterInfo.jsx')) -const DocumentJobs = lazy(() => import('../components/Dashboard/Management/DocumentJobs.jsx')) -const DocumentJobInfo = lazy(() => import('../components/Dashboard/Management/DocumentJobs/DocumentJobInfo.jsx')) -const DocumentTemplateDesign = lazy(() => import('../components/Dashboard/Management/DocumentTemplates/DocumentTemplateDesign.jsx')) +const HostInfo = lazy( + () => import('../components/Dashboard/Management/Hosts/HostInfo.jsx') +) +const DocumentSizes = lazy( + () => import('../components/Dashboard/Management/DocumentSizes.jsx') +) +const DocumentSizeInfo = lazy( + () => + import('../components/Dashboard/Management/DocumentSizes/DocumentSizeInfo.jsx') +) +const DocumentTemplates = lazy( + () => import('../components/Dashboard/Management/DocumentTemplates.jsx') +) +const DocumentTemplateInfo = lazy( + () => + import('../components/Dashboard/Management/DocumentTemplates/DocumentTemplateInfo.jsx') +) +const DocumentPrinters = lazy( + () => import('../components/Dashboard/Management/DocumentPrinters.jsx') +) +const DocumentPrinterInfo = lazy( + () => + import('../components/Dashboard/Management/DocumentPrinters/DocumentPrinterInfo.jsx') +) +const DocumentJobs = lazy( + () => import('../components/Dashboard/Management/DocumentJobs.jsx') +) +const DocumentJobInfo = lazy( + () => + import('../components/Dashboard/Management/DocumentJobs/DocumentJobInfo.jsx') +) +const DocumentTemplateDesign = lazy( + () => + import('../components/Dashboard/Management/DocumentTemplates/DocumentTemplateDesign.jsx') +) const Files = lazy(() => import('../components/Dashboard/Management/Files.jsx')) -const FileInfo = lazy(() => import('../components/Dashboard/Management/Files/FileInfo.jsx')) -const TaxRates = lazy(() => import('../components/Dashboard/Management/TaxRates.jsx')) -const TaxRateInfo = lazy(() => import('../components/Dashboard/Management/TaxRates/TaxRateInfo.jsx')) -const TaxRecords = lazy(() => import('../components/Dashboard/Management/TaxRecords.jsx')) -const TaxRecordInfo = lazy(() => import('../components/Dashboard/Management/TaxRecords/TaxRecordInfo.jsx')) +const FileInfo = lazy( + () => import('../components/Dashboard/Management/Files/FileInfo.jsx') +) +const TaxRates = lazy( + () => import('../components/Dashboard/Management/TaxRates.jsx') +) +const TaxRateInfo = lazy( + () => import('../components/Dashboard/Management/TaxRates/TaxRateInfo.jsx') +) +const TaxRecords = lazy( + () => import('../components/Dashboard/Management/TaxRecords.jsx') +) +const TaxRecordInfo = lazy( + () => + import('../components/Dashboard/Management/TaxRecords/TaxRecordInfo.jsx') +) +const About = lazy(() => import('../components/Dashboard/Management/About.jsx')) const ManagementRoutes = [ } />, @@ -58,7 +158,11 @@ const ManagementRoutes = [ path='management/filaments/info' element={} />, - } />, + } + />, } />, - } />, + } + />, , } />, } />, + } />, } />, } />,