Compare commits

..

9 Commits

Author SHA1 Message Date
50abb22054 Update FilamentStock and StockEvent models for improved data handling
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Reformatted the filters in FilamentStock for better readability.
- Changed the 'required' property of the stock location in FilamentStock to true, ensuring that stock location is mandatory.
- Added 'updatedAt' column to StockEvent for enhanced tracking of stock events.
2026-07-26 18:37:47 +01:00
4c19df717b Enhance SlicerIntegration layout for better responsiveness
- Wrapped the ObjectProperty component in a div with defined width properties to improve layout consistency and responsiveness within the SlicerIntegration component.
- Ensured that the display adapts better to varying screen sizes, enhancing user experience.
2026-07-26 18:37:42 +01:00
6ec1e9d3c0 Refactor ProductionOverview to replace HistoryDisplay with ModelHistoryDisplay
- Updated the ProductionOverview component to utilize ModelHistoryDisplay instead of HistoryDisplay for better clarity in displaying historical data for printers and jobs.
- Simplified the ObjectTable component's masterFilter for queued jobs, enhancing code readability.
2026-07-26 18:37:36 +01:00
2a849cea57 Enhance StateDisplay component to improve progress calculation
- Introduced a new progressValue calculation to round the currentState.progress for better precision in the displayed progress percentage.
- Updated the Progress component to utilize the new progressValue, ensuring accurate representation of loading progress.
2026-07-26 18:37:24 +01:00
4d9db747a2 Enhance Inventory Components with History Display Feature
- Added a HistoryDisplay component to FilamentStockInfo, PartStockInfo, and ProductStockInfo, allowing users to view historical data for filament, part, and product stocks.
- Introduced a new GraphIcon for visual representation in the collapsible history sections.
- Updated the state management to include history data, improving the overall functionality and user experience in inventory tracking.
2026-07-26 18:37:18 +01:00
fed7de8663 Update JobInfo component to pass complete object data to DeployJob component
- Modified the objectData prop in DeployJob to spread the properties from objectFormState, ensuring all relevant data is passed for deployment actions.
2026-07-26 16:10:14 +01:00
31e6356d9c Add additional hidden properties to ControlPrinter component 2026-07-26 16:10:06 +01:00
d122dfa83f Update ControlPrinter to consistently render AlertsDisplay component
- Moved the AlertsDisplay component to ensure it is always rendered within the ControlPrinter, improving visibility of alerts.
- Enhanced layout consistency by maintaining the structure of the component within the Flex container.
2026-07-26 15:00:58 +01:00
c588196580 Refactor AlertsDisplay component to simplify layout and remove mobile-specific logic
- Removed the useMediaQuery hook and associated mobile rendering logic, streamlining the component's structure.
- Updated the return statement to consistently render alerts within a Flex container, enhancing layout consistency.
2026-07-26 15:00:50 +01:00
15 changed files with 768 additions and 300 deletions

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.989624,0,0,0.989624,3,3.012667)">
<path d="M32.232,55.642C32.232,57.251 30.901,58.583 29.291,58.583C27.682,58.583 26.351,57.251 26.351,55.642C26.351,54.032 27.682,52.701 29.291,52.701C30.901,52.701 32.232,54.032 32.232,55.642ZM32.232,46.867C32.232,48.476 30.901,49.807 29.291,49.807C27.682,49.807 26.351,48.476 26.351,46.867C26.351,45.258 27.682,43.926 29.291,43.926C30.901,43.926 32.232,45.258 32.232,46.867ZM32.232,38.066C32.232,39.676 30.901,41.007 29.291,41.007C28.398,41.007 27.591,40.597 27.05,39.956C27.916,38.466 28.763,36.862 29.604,35.143C31.07,35.301 32.232,36.562 32.232,38.066ZM58.608,29.291C58.608,30.901 57.277,32.232 55.667,32.232C54.058,32.232 52.727,30.901 52.727,29.291C52.727,27.682 54.058,26.351 55.667,26.351C57.277,26.351 58.608,27.682 58.608,29.291ZM5.881,29.291C5.881,30.901 4.55,32.232 2.941,32.232C1.331,32.232 0,30.901 0,29.291C0,27.682 1.331,26.351 2.941,26.351C4.55,26.351 5.881,27.682 5.881,29.291ZM14.656,29.291C14.656,30.901 13.351,32.232 11.716,32.232C10.106,32.232 8.775,30.901 8.775,29.291C8.775,27.682 10.106,26.351 11.716,26.351C13.351,26.351 14.656,27.682 14.656,29.291ZM23.457,29.291C23.457,30.901 22.126,32.232 20.516,32.232C18.907,32.232 17.576,30.901 17.576,29.291C17.576,27.682 18.907,26.351 20.516,26.351C22.126,26.351 23.457,27.682 23.457,29.291ZM41.032,29.291C41.032,30.901 39.701,32.232 38.092,32.232C36.483,32.232 35.151,30.901 35.151,29.291C35.151,27.682 36.483,26.351 38.092,26.351C39.701,26.351 41.032,27.682 41.032,29.291ZM49.807,29.291C49.807,30.901 48.476,32.232 46.867,32.232C45.258,32.232 43.926,30.901 43.926,29.291C43.926,27.682 45.258,26.351 46.867,26.351C48.476,26.351 49.807,27.682 49.807,29.291ZM31.442,31.281C31.58,30.996 31.718,30.691 31.857,30.384C31.977,30.118 32.096,29.855 32.214,29.607C32.145,30.248 31.864,30.83 31.442,31.281ZM26.771,28.065L26.377,28.924C26.451,28.32 26.715,27.77 27.108,27.338L26.771,28.065ZM31.523,18.59C30.658,20.084 29.813,21.691 28.972,23.413C27.51,23.255 26.351,22.016 26.351,20.491C26.351,18.881 27.682,17.55 29.291,17.55C30.179,17.55 30.983,17.955 31.523,18.59ZM32.232,11.716C32.232,13.325 30.901,14.656 29.291,14.656C27.682,14.656 26.351,13.325 26.351,11.716C26.351,10.106 27.682,8.775 29.291,8.775C30.901,8.775 32.232,10.106 32.232,11.716ZM32.232,2.941C32.232,4.55 30.901,5.881 29.291,5.881C27.682,5.881 26.351,4.55 26.351,2.941C26.351,1.331 27.682,0 29.291,0C30.901,0 32.232,1.331 32.232,2.941Z" style="fill-rule:nonzero;"/>
<path d="M2.803,52.396C1.307,52.643 0.237,54.108 0.504,55.597C0.751,57.093 2.216,58.169 3.711,57.928C17.696,55.598 24.556,46.57 31.857,30.384C38.113,16.508 43.608,8.067 55.805,6.052C57.295,5.806 58.371,4.341 58.129,2.851C57.851,1.356 56.392,0.28 54.897,0.521C40.932,2.825 34.078,11.879 26.771,28.065C20.515,41.941 15,50.382 2.803,52.396Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -16,10 +16,12 @@ import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
import FilamentStockIcon from '../../../Icons/FilamentStockIcon' import FilamentStockIcon from '../../../Icons/FilamentStockIcon'
import NoteIcon from '../../../Icons/NoteIcon' import NoteIcon from '../../../Icons/NoteIcon'
import AuditLogIcon from '../../../Icons/AuditLogIcon' import AuditLogIcon from '../../../Icons/AuditLogIcon'
import GraphIcon from '../../../Icons/GraphIcon'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder'
import DocumentPrintButton from '../../common/DocumentPrintButton' import DocumentPrintButton from '../../common/DocumentPrintButton'
import UserNotifierToggle from '../../common/UserNotifierToggle' import UserNotifierToggle from '../../common/UserNotifierToggle'
import ScrollBox from '../../common/ScrollBox' import ScrollBox from '../../common/ScrollBox'
import HistoryDisplay from '../../common/HistoryDisplay'
const FilamentStockInfo = () => { const FilamentStockInfo = () => {
const location = useLocation() const location = useLocation()
@ -33,6 +35,7 @@ const FilamentStockInfo = () => {
{ {
info: true, info: true,
events: true, events: true,
history: true,
notes: true, notes: true,
auditLogs: false auditLogs: false
} }
@ -82,6 +85,7 @@ const FilamentStockInfo = () => {
items={[ items={[
{ key: 'info', label: 'Filament Stock Information' }, { key: 'info', label: 'Filament Stock Information' },
{ key: 'events', label: 'Filament Stock Events' }, { key: 'events', label: 'Filament Stock Events' },
{ key: 'history', label: 'Filament Stock History' },
{ key: 'notes', label: 'Notes' }, { key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' } { key: 'auditLogs', label: 'Audit Logs' }
]} ]}
@ -170,7 +174,25 @@ const FilamentStockInfo = () => {
<ObjectTable <ObjectTable
type='stockEvent' type='stockEvent'
masterFilter={{ 'parent._id': filamentStockId }} masterFilter={{ 'parent._id': filamentStockId }}
visibleColumns={{ 'parent._id': false }} visibleColumns={{ parent: false }}
/>
)}
</InfoCollapse>
<InfoCollapse
title='Filament Stock History'
icon={<GraphIcon />}
active={collapseState.history}
onToggle={(expanded) => updateCollapseState('history', expanded)}
collapseKey='history'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<HistoryDisplay
history={objectFormState.objectData?.history || []}
objectType='filamentStock'
chartType='line'
loading={objectFormState.loading}
/> />
)} )}
</InfoCollapse> </InfoCollapse>

View File

@ -12,6 +12,7 @@ import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import GraphIcon from '../../../Icons/GraphIcon.jsx'
import ObjectForm from '../../common/ObjectForm.jsx' import ObjectForm from '../../common/ObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx' import EditButtons from '../../common/EditButtons.jsx'
import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActivityIndicator from '../../common/ActivityIndicator.jsx'
@ -22,6 +23,7 @@ import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx' import ScrollBox from '../../common/ScrollBox.jsx'
import HistoryDisplay from '../../common/HistoryDisplay.jsx'
const log = loglevel.getLogger('PartStockInfo') const log = loglevel.getLogger('PartStockInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -36,6 +38,7 @@ const PartStockInfo = () => {
{ {
info: true, info: true,
stocks: true, stocks: true,
history: true,
notes: true, notes: true,
auditLogs: false auditLogs: false
} }
@ -88,6 +91,7 @@ const PartStockInfo = () => {
disabled={objectFormState.loading} disabled={objectFormState.loading}
items={[ items={[
{ key: 'info', label: 'Part Stock Information' }, { key: 'info', label: 'Part Stock Information' },
{ key: 'history', label: 'Part Stock History' },
{ key: 'notes', label: 'Notes' }, { key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' } { key: 'auditLogs', label: 'Audit Logs' }
]} ]}
@ -173,6 +177,25 @@ const PartStockInfo = () => {
</InfoCollapse> </InfoCollapse>
</ActionHandler> </ActionHandler>
<InfoCollapse
title='Part Stock History'
icon={<GraphIcon />}
active={collapseState.history}
onToggle={(expanded) => updateCollapseState('history', expanded)}
collapseKey='history'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<HistoryDisplay
history={objectFormState.objectData?.history || []}
objectType='partStock'
chartType='line'
loading={objectFormState.loading}
/>
)}
</InfoCollapse>
<InfoCollapse <InfoCollapse
title='Notes' title='Notes'
icon={<NoteIcon />} icon={<NoteIcon />}

View File

@ -19,6 +19,7 @@ import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import PartStockIcon from '../../../Icons/PartStockIcon.jsx' import PartStockIcon from '../../../Icons/PartStockIcon.jsx'
import GraphIcon from '../../../Icons/GraphIcon.jsx'
import ObjectForm from '../../common/ObjectForm.jsx' import ObjectForm from '../../common/ObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx' import EditButtons from '../../common/EditButtons.jsx'
import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActivityIndicator from '../../common/ActivityIndicator.jsx'
@ -29,6 +30,7 @@ import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx' import ScrollBox from '../../common/ScrollBox.jsx'
import HistoryDisplay from '../../common/HistoryDisplay.jsx'
const log = loglevel.getLogger('ProductStockInfo') const log = loglevel.getLogger('ProductStockInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -46,6 +48,7 @@ const ProductStockInfo = () => {
{ {
info: true, info: true,
partStocks: true, partStocks: true,
history: true,
notes: true, notes: true,
auditLogs: false auditLogs: false
} }
@ -112,6 +115,7 @@ const ProductStockInfo = () => {
items={[ items={[
{ key: 'info', label: 'Product Stock Information' }, { key: 'info', label: 'Product Stock Information' },
{ key: 'partStocks', label: 'Part Stocks' }, { key: 'partStocks', label: 'Part Stocks' },
{ key: 'history', label: 'Product Stock History' },
{ key: 'notes', label: 'Notes' }, { key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' } { key: 'auditLogs', label: 'Audit Logs' }
]} ]}
@ -217,6 +221,25 @@ const ProductStockInfo = () => {
</ObjectForm> </ObjectForm>
</ActionHandler> </ActionHandler>
<InfoCollapse
title='Product Stock History'
icon={<GraphIcon />}
active={collapseState.history}
onToggle={(expanded) => updateCollapseState('history', expanded)}
collapseKey='history'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<HistoryDisplay
history={objectFormState.objectData?.history || []}
objectType='productStock'
chartType='line'
loading={objectFormState.loading}
/>
)}
</InfoCollapse>
<InfoCollapse <InfoCollapse
title='Notes' title='Notes'
icon={<NoteIcon />} icon={<NoteIcon />}

View File

@ -243,7 +243,7 @@ const JobInfo = () => {
}} }}
> >
<DeployJob <DeployJob
objectData={{ _id: jobId }} objectData={{ ...objectFormState.objectData }}
onOk={() => { onOk={() => {
actionHandlerRef.current.clearAction() actionHandlerRef.current.clearAction()
setDeployJobOpen(false) setDeployJobOpen(false)

View File

@ -273,13 +273,14 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
visibleState={collapseState} visibleState={collapseState}
updateVisibleState={updateCollapseState} updateVisibleState={updateCollapseState}
/> />
</Space>
</Space>
</Flex>
<AlertsDisplay <AlertsDisplay
alerts={objectFormState.objectData?.alerts} alerts={objectFormState.objectData?.alerts}
printerId={printerId} printerId={printerId}
/> />
</Space>
</Space>
</Flex>
<ScrollBox> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
@ -427,7 +428,10 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
visibleProperties={{ visibleProperties={{
printers: false, printers: false,
createdAt: false, createdAt: false,
finishedAt: false finishedAt: false,
moonrakerJobId: false,
job: false,
printer: false
}} }}
objectPropertyProps={{ objectPropertyProps={{
showHyperlink: !slicerIntegration showHyperlink: !slicerIntegration

View File

@ -184,10 +184,18 @@ const SlicerIntegration = () => {
: objectData?.name || 'Slicer Integration'} : objectData?.name || 'Slicer Integration'}
</Title> </Title>
{!loading && ( {!loading && (
<div
style={{
maxWidth: '400px',
width: '400px',
minWidth: '100px'
}}
>
<ObjectProperty <ObjectProperty
objectData={objectData} objectData={objectData}
{...getModelProperty('printer', 'state')} {...getModelProperty('printer', 'state')}
/> />
</div>
)} )}
</Flex> </Flex>
{slicerUploadsButton()} {slicerUploadsButton()}

View File

@ -1,7 +1,7 @@
import { Flex } from 'antd' import { Flex } from 'antd'
import useCollapseState from '../hooks/useCollapseState.jsx' import useCollapseState from '../hooks/useCollapseState.jsx'
import StatsDisplay from '../common/StatsDisplay' import StatsDisplay from '../common/StatsDisplay'
import HistoryDisplay from '../common/HistoryDisplay' import ModelHistoryDisplay from '../common/ModelHistoryDisplay'
import InfoCollapse from '../common/InfoCollapse' import InfoCollapse from '../common/InfoCollapse'
import ScrollBox from '../common/ScrollBox' import ScrollBox from '../common/ScrollBox'
import ObjectTable from '../common/ObjectTable' import ObjectTable from '../common/ObjectTable'
@ -82,7 +82,7 @@ const ProductionOverview = () => {
collapseKey='printerHistory' collapseKey='printerHistory'
canCollapse={false} canCollapse={false}
> >
<HistoryDisplay objectType='printer' /> <ModelHistoryDisplay objectType='printer' />
</InfoCollapse> </InfoCollapse>
<InfoCollapse <InfoCollapse
title='Online Printers' title='Online Printers'
@ -108,7 +108,7 @@ const ProductionOverview = () => {
canCollapse={false} canCollapse={false}
collapseKey='jobHistory' collapseKey='jobHistory'
> >
<HistoryDisplay objectType='job' /> <ModelHistoryDisplay objectType='job' />
</InfoCollapse> </InfoCollapse>
<InfoCollapse <InfoCollapse
title='Queued Jobs' title='Queued Jobs'
@ -120,10 +120,7 @@ const ProductionOverview = () => {
canCollapse={false} canCollapse={false}
collapseKey='queuedJobs' collapseKey='queuedJobs'
> >
<ObjectTable <ObjectTable type='job' masterFilter={{ state: 'queued' }} />
type='job'
masterFilter={{ 'state.type': 'queued' }}
/>
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</Flex> </Flex>

View File

@ -1,11 +1,10 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { createElement } from 'react' import { createElement } from 'react'
import { Flex, Alert, Button, Dropdown, Popover } from 'antd' import { Flex, Alert, Button, Dropdown } from 'antd'
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon' import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import { CaretDownOutlined } from '@ant-design/icons' import { CaretDownOutlined } from '@ant-design/icons'
import { useMediaQuery } from 'react-responsive'
import { getModelByName } from '../../../database/ObjectModels' import { getModelByName } from '../../../database/ObjectModels'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
@ -15,7 +14,6 @@ const AlertsDisplay = ({
showDismiss = true, showDismiss = true,
showActions = true showActions = true
}) => { }) => {
const isMobile = useMediaQuery({ maxWidth: 768 })
const getAlertType = (type, priority) => { const getAlertType = (type, priority) => {
if (type === 'error' || priority === '9') return 'error' if (type === 'error' || priority === '9') return 'error'
if (type === 'warning' || priority === '8') return 'warning' if (type === 'warning' || priority === '8') return 'warning'
@ -176,23 +174,11 @@ const AlertsDisplay = ({
) )
}) })
if (isMobile) {
return ( return (
<Popover <Flex gap='small' style={{ width: '100%' }} vertical align='stretch'>
content={alertElements} {alertElements}
trigger='hover' </Flex>
arrow={false}
placement='bottom'
classNames={{
root: 'printer-alerts-display-popover'
}}
>
<Button>Alerts</Button>
</Popover>
) )
}
return <Flex gap='small'>{alertElements}</Flex>
} }
AlertsDisplay.propTypes = { AlertsDisplay.propTypes = {

View File

@ -1,9 +1,11 @@
import { useEffect, useState, useContext, useMemo } from 'react' import { useMemo, useState } from 'react'
import { Card, Segmented, Flex, Popover, DatePicker, Button, Space } from 'antd' import { Card, Segmented, Flex, Popover, DatePicker, Button, Space } from 'antd'
import { import {
ResponsiveContainer, ResponsiveContainer,
BarChart, BarChart,
Bar, Bar,
LineChart,
Line,
CartesianGrid, CartesianGrid,
XAxis, XAxis,
YAxis, YAxis,
@ -12,151 +14,70 @@ import {
} from 'recharts' } from 'recharts'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { getModelByName } from '../../../database/ObjectModels'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useThemeContext } from '../context/ThemeContext' import { useThemeContext } from '../context/ThemeContext'
import MissingPlaceholder from './MissingPlaceholder' import MissingPlaceholder from './MissingPlaceholder'
import CheckIcon from '../../Icons/CheckIcon' import CheckIcon from '../../Icons/CheckIcon'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
const HistoryDisplay = ({ const HISTORY_SERIES_CONFIG = {
objectType, filamentStock: {
startDate, series: [
endDate, {
styles, key: 'currentWeight.net',
height = 400 label: 'Net Weight',
}) => { color: 'processing',
const { getModelHistory, connected } = useContext(ApiServerContext) suffix: 'g'
const { token } = useContext(AuthContext) },
const [historyData, setHistoryData] = useState([]) {
const [loading, setLoading] = useState(true) key: 'currentWeight.gross',
const [error, setError] = useState(null) label: 'Gross Weight',
const [timeRange, setTimeRange] = useState('4hrs') color: 'warning',
const [startCustomDate, setStartCustomDate] = useState(null) suffix: 'g'
const [endCustomDate, setEndCustomDate] = useState(null) }
const { isDarkMode, getColors } = useThemeContext() ],
// Calculate dates based on selected time range or provided props formatValue: (value, suffix = '') =>
const { defaultStartDate, defaultEndDate } = useMemo(() => { `${Number(value ?? 0).toFixed(2)}${suffix}`
const now = new Date() },
partStock: {
series: [
{
key: 'currentQuantity',
label: 'Current Quantity',
color: 'processing'
}
],
formatValue: (value) => Number(value ?? 0).toFixed(2)
},
productStock: {
series: [
{
key: 'currentQuantity',
label: 'Current Quantity',
color: 'processing'
}
],
formatValue: (value) => Number(value ?? 0).toFixed(2)
},
stockEvent: {
series: [
{
key: 'value',
label: 'Value',
color: 'processing',
variance: true
}
],
formatValue: (value, suffix = '') => {
const numericValue = Number(value ?? 0)
const isPositive = numericValue > 0
const isNegative = numericValue < 0
return `${isPositive ? '+' : isNegative ? '-' : ''}${Math.abs(numericValue).toFixed(2)}${suffix}`
}
}
}
// If startDate/endDate props are provided, use them (ignore time range) const getNestedValue = (obj, path) => {
if (startDate || endDate) {
return {
defaultStartDate:
startDate || new Date(now.getTime() - 24 * 60 * 60 * 1000),
defaultEndDate: endDate || now
}
}
// Handle custom date range
if (timeRange === 'custom' && startCustomDate && endCustomDate) {
return {
defaultStartDate: startCustomDate.toDate(),
defaultEndDate: endCustomDate.toDate()
}
}
// Otherwise, calculate based on selected time range
const hoursMap = {
'8hrs': 8,
'12hrs': 12,
'24hrs': 24,
'4hrs': 4,
'1hrs': 1
}
const hours = hoursMap[timeRange] || 1
return {
defaultStartDate: new Date(now.getTime() - hours * 60 * 60 * 1000),
defaultEndDate: now
}
}, [startDate, endDate, timeRange, startCustomDate, endCustomDate])
useEffect(() => {
if (!objectType || !getModelHistory || !token || !connected) {
return
}
const fetchHistory = async () => {
try {
setLoading(true)
setError(null)
const fetchedHistory = await getModelHistory(
objectType,
defaultStartDate,
defaultEndDate
)
if (fetchedHistory) {
setHistoryData(fetchedHistory)
} else {
setError('Failed to fetch history')
}
} catch (err) {
console.error('Error fetching history:', err)
setError('Failed to fetch history')
} finally {
setLoading(false)
}
}
fetchHistory()
}, [
objectType,
getModelHistory,
token,
connected,
defaultStartDate,
defaultEndDate
])
if (!objectType) {
return null
}
const model = getModelByName(objectType)
if (!model) {
return null
}
const modelStats = model.stats || []
if (modelStats.length === 0) {
return null
}
if (error || !Array.isArray(historyData)) {
return null
}
/**
* Extracts the stat key from history data based on stat definition name
* e.g., "states.standby.count" -> "standby", "standbyPrinters" -> "standby"
* The history data keys are the rollup names (like "standby", "printing")
*/
const extractStatKey = (statName) => {
// Handle dot notation paths like "states.standby.count"
if (statName.includes('.')) {
const parts = statName.split('.')
// Extract the middle part (e.g., "standby" from "states.standby.count")
// Skip first part (usually "states") and last part (usually "count")
if (parts.length >= 2) {
return parts[parts.length - 2] // Get second-to-last part
}
return parts[parts.length - 1] // Fallback to last part
}
// Remove common suffixes
return statName
.replace(/Printers?$/i, '')
.replace(/Jobs?$/i, '')
.toLowerCase()
}
/**
* Gets a nested value from an object using dot notation
*/
const getNestedValue = (obj, path) => {
if (!obj || !path) return undefined if (!obj || !path) return undefined
const keys = path.split('.') const keys = path.split('.')
let value = obj let value = obj
@ -165,102 +86,94 @@ const HistoryDisplay = ({
value = value[key] value = value[key]
} }
return value return value
} }
/** const HistoryDisplay = ({
* Extracts numeric value from a rollup object loading = false,
* Handles both old format (number) and new format ({count: 0}, {sum: 100}, {avg: 5.5}) history = [],
*/ objectType,
const extractValue = (value, preferredOperation) => { chartType = 'bar',
if (typeof value === 'number') { unit,
return value startDate,
} endDate,
if (value && typeof value === 'object') { styles,
// If a preferred operation is specified and exists, use it height = 400
if (preferredOperation && value[preferredOperation] !== undefined) { }) => {
return value[preferredOperation] const [timeRange, setTimeRange] = useState('4hrs')
} const [startCustomDate, setStartCustomDate] = useState(null)
const [endCustomDate, setEndCustomDate] = useState(null)
const { isDarkMode, getColors } = useThemeContext()
// Handle new format: {count: 0}, {sum: 100}, {avg: 5.5} const config = HISTORY_SERIES_CONFIG[objectType]
const operationKeys = ['count', 'sum', 'avg']
for (const key of operationKeys) {
if (value[key] !== undefined) {
return value[key]
}
}
// Fallback: return first numeric value found
const firstValue = Object.values(value)[0]
return typeof firstValue === 'number' ? firstValue : 0
}
return 0
}
/** const { defaultStartDate, defaultEndDate } = useMemo(() => {
* Gets the stat value from history data point const now = new Date()
*/
const getStatValueFromPoint = (point, statDef) => {
// Handle special combined stats (support both 'combine' and 'sum' properties)
const combineList = statDef.combine || statDef.sum
if (combineList) {
return combineList.reduce((sum, statName) => {
const tempStatDef = { name: statName }
const value = getStatValueFromPoint(point, tempStatDef)
return sum + (value || 0)
}, 0)
}
const statName = statDef.name if (startDate || endDate) {
// Determine operation from stat name
let operation
if (statName.endsWith('.count')) operation = 'count'
else if (statName.endsWith('.sum')) operation = 'sum'
else if (statName.endsWith('.avg')) operation = 'avg'
let value
// Extract the key that matches the history data structure
// History data has keys like "standby", "printing" (the rollup names)
const statKey = extractStatKey(statName)
value = point[statKey]
// If not found with extracted key, try direct access with full path
if (value === undefined && statName.includes('.')) {
value = getNestedValue(point, statName)
}
// If still not found, try with original stat name
if (value === undefined) {
value = point[statName]
}
return extractValue(value, operation)
}
// Transform data for the chart using model stats configuration
// Each data point has: { date: '2024-01-01', standby: {count: 0}, printing: {count: 0}, ... }
// We need to transform to: [{ date: '2024-01-01', category: 'Ready', value: 0 }, ...]
const chartData = historyData.flatMap((point) => {
return modelStats.map((statDef) => {
const statValue = getStatValueFromPoint(point, statDef)
const label = statDef.label || statDef.name
return { return {
date: point.date, defaultStartDate:
dateFormatted: dayjs(point.date).format('DD/MM HH:mm'), startDate || new Date(now.getTime() - 24 * 60 * 60 * 1000),
category: label, defaultEndDate: endDate || now
value: statValue || 0
} }
}
if (timeRange === 'custom' && startCustomDate && endCustomDate) {
return {
defaultStartDate: startCustomDate.toDate(),
defaultEndDate: endCustomDate.toDate()
}
}
const timeRangeMap = {
'24hrs': 24 * 60,
'12hrs': 12 * 60,
'8hrs': 8 * 60,
'4hrs': 4 * 60,
'1hrs': 60,
'30mins': 30,
'15mins': 15,
'5mins': 5
}
const minutes = timeRangeMap[timeRange] || 60
return {
defaultStartDate: new Date(now.getTime() - minutes * 60 * 1000),
defaultEndDate: now
}
}, [startDate, endDate, timeRange, startCustomDate, endCustomDate])
const chartRows = useMemo(() => {
if (!config || !Array.isArray(history)) {
return []
}
const start = new Date(defaultStartDate).getTime()
const end = new Date(defaultEndDate).getTime()
return history
.filter((point) => {
const timestamp = new Date(point.timestamp).getTime()
return timestamp >= start && timestamp <= end
}) })
.map((point) => {
const row = {
timestamp: point.timestamp,
dateFormatted: dayjs(point.timestamp).format('DD/MM HH:mm')
}
config.series.forEach((seriesDef) => {
row[seriesDef.label] = getNestedValue(point, seriesDef.key) ?? 0
}) })
// Sort by date return row
chartData.sort((a, b) => {
return new Date(a.date) - new Date(b.date)
}) })
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp))
}, [history, config, defaultStartDate, defaultEndDate])
if (!objectType || !config) {
return null
}
const themeColors = getColors() const themeColors = getColors()
// Create color mapping from model stats
const colors = { const colors = {
success: themeColors.colorSuccess, success: themeColors.colorSuccess,
processing: themeColors.colorInfo, processing: themeColors.colorInfo,
@ -274,27 +187,18 @@ const HistoryDisplay = ({
volcano: themeColors.colorVolcano volcano: themeColors.colorVolcano
} }
// Build color range array based on model stats order const colorRange = config.series.map((seriesDef) => {
const colorRange = modelStats.map((stat) => { if (seriesDef.color) {
if (stat.color) { return colors[seriesDef.color] || seriesDef.color
return colors[stat.color] || stat.color
} }
return colors.default return colors.default
}) })
const chartRows = Array.from( const formatTooltipValue = (value, seriesLabel) => {
chartData const seriesDef = config.series.find((item) => item.label === seriesLabel)
.reduce((acc, item) => { const suffix = seriesDef?.suffix || unit || ''
const existing = acc.get(item.date) || { return config.formatValue(value, suffix)
date: item.date,
dateFormatted: item.dateFormatted
} }
existing[item.category] = item.value
acc.set(item.date, existing)
return acc
}, new Map())
.values()
).sort((a, b) => new Date(a.date) - new Date(b.date))
const customTimeRangeContent = ( const customTimeRangeContent = (
<Space.Compact> <Space.Compact>
@ -323,6 +227,71 @@ const HistoryDisplay = ({
</Space.Compact> </Space.Compact>
) )
const chartProps = {
data: chartRows
}
const axisTickStyle = {
fill: isDarkMode ? '#d9d9d9' : '#595959',
fontSize: 12
}
const tooltipStyle = {
backgroundColor: isDarkMode ? '#1f1f1f' : '#ffffff',
border: `1px solid ${isDarkMode ? '#303030' : '#f0f0f0'}`,
borderRadius: 8
}
const renderChart = () => {
const commonElements = (
<>
<CartesianGrid
strokeDasharray='3 3'
stroke={isDarkMode ? '#303030' : '#f0f0f0'}
/>
<XAxis dataKey='dateFormatted' tick={axisTickStyle} />
<YAxis tick={axisTickStyle} />
<Tooltip
contentStyle={tooltipStyle}
formatter={(value, name) => [formatTooltipValue(value, name), name]}
/>
<Legend />
</>
)
if (chartType === 'line') {
return (
<LineChart {...chartProps}>
{commonElements}
{config.series.map((seriesDef, index) => (
<Line
key={seriesDef.label}
type='monotone'
dataKey={seriesDef.label}
stroke={colorRange[index] || colors.default}
strokeWidth={2}
dot={false}
/>
))}
</LineChart>
)
}
return (
<BarChart {...chartProps}>
{commonElements}
{config.series.map((seriesDef, index) => (
<Bar
key={seriesDef.label}
dataKey={seriesDef.label}
stackId='history'
fill={colorRange[index] || colors.default}
/>
))}
</BarChart>
)
}
return ( return (
<Card <Card
style={{ width: '100%' }} style={{ width: '100%' }}
@ -358,7 +327,10 @@ const HistoryDisplay = ({
{ label: '12hr', value: '12hrs' }, { label: '12hr', value: '12hrs' },
{ label: '8hr', value: '8hrs' }, { label: '8hr', value: '8hrs' },
{ label: '4hr', value: '4hrs' }, { label: '4hr', value: '4hrs' },
{ label: '1hr', value: '1hrs' } { label: '1hr', value: '1hrs' },
{ label: '30m', value: '30mins' },
{ label: '15m', value: '15mins' },
{ label: '5m', value: '5mins' }
]} ]}
value={timeRange} value={timeRange}
onChange={setTimeRange} onChange={setTimeRange}
@ -370,44 +342,7 @@ const HistoryDisplay = ({
{chartRows.length > 0 && ( {chartRows.length > 0 && (
<div style={{ width: '100%', height: `${height}px` }}> <div style={{ width: '100%', height: `${height}px` }}>
<ResponsiveContainer width='100%' height='100%'> <ResponsiveContainer width='100%' height='100%'>
<BarChart data={chartRows}> {renderChart()}
<CartesianGrid
strokeDasharray='3 3'
stroke={isDarkMode ? '#303030' : '#f0f0f0'}
/>
<XAxis
dataKey='dateFormatted'
tick={{
fill: isDarkMode ? '#d9d9d9' : '#595959',
fontSize: 12
}}
/>
<YAxis
tick={{
fill: isDarkMode ? '#d9d9d9' : '#595959',
fontSize: 12
}}
/>
<Tooltip
contentStyle={{
backgroundColor: isDarkMode ? '#1f1f1f' : '#ffffff',
border: `1px solid ${isDarkMode ? '#303030' : '#f0f0f0'}`,
borderRadius: 8
}}
/>
<Legend />
{modelStats.map((statDef, index) => {
const category = statDef.label || statDef.name
return (
<Bar
key={category}
dataKey={category}
stackId='history'
fill={colorRange[index] || colors.default}
/>
)
})}
</BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
)} )}
@ -426,13 +361,22 @@ const HistoryDisplay = ({
} }
HistoryDisplay.propTypes = { HistoryDisplay.propTypes = {
objectType: PropTypes.string.isRequired, history: PropTypes.arrayOf(PropTypes.object),
objectType: PropTypes.oneOf([
'filamentStock',
'partStock',
'productStock',
'stockEvent'
]).isRequired,
chartType: PropTypes.oneOf(['line', 'bar']),
unit: PropTypes.string,
startDate: PropTypes.oneOfType([ startDate: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.instanceOf(Date) PropTypes.instanceOf(Date)
]), ]),
styles: PropTypes.object, loading: PropTypes.bool,
endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
styles: PropTypes.object,
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) height: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
} }

View File

@ -0,0 +1,439 @@
import { useEffect, useState, useContext, useMemo } from 'react'
import { Card, Segmented, Flex, Popover, DatePicker, Button, Space } from 'antd'
import {
ResponsiveContainer,
BarChart,
Bar,
CartesianGrid,
XAxis,
YAxis,
Tooltip,
Legend
} from 'recharts'
import PropTypes from 'prop-types'
import { getModelByName } from '../../../database/ObjectModels'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import dayjs from 'dayjs'
import { useThemeContext } from '../context/ThemeContext'
import MissingPlaceholder from './MissingPlaceholder'
import CheckIcon from '../../Icons/CheckIcon'
import { LoadingOutlined } from '@ant-design/icons'
const ModelHistoryDisplay = ({
objectType,
startDate,
endDate,
styles,
height = 400
}) => {
const { getModelHistory, connected } = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
const [historyData, setHistoryData] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [timeRange, setTimeRange] = useState('4hrs')
const [startCustomDate, setStartCustomDate] = useState(null)
const [endCustomDate, setEndCustomDate] = useState(null)
const { isDarkMode, getColors } = useThemeContext()
// Calculate dates based on selected time range or provided props
const { defaultStartDate, defaultEndDate } = useMemo(() => {
const now = new Date()
// If startDate/endDate props are provided, use them (ignore time range)
if (startDate || endDate) {
return {
defaultStartDate:
startDate || new Date(now.getTime() - 24 * 60 * 60 * 1000),
defaultEndDate: endDate || now
}
}
// Handle custom date range
if (timeRange === 'custom' && startCustomDate && endCustomDate) {
return {
defaultStartDate: startCustomDate.toDate(),
defaultEndDate: endCustomDate.toDate()
}
}
// Otherwise, calculate based on selected time range
const hoursMap = {
'8hrs': 8,
'12hrs': 12,
'24hrs': 24,
'4hrs': 4,
'1hrs': 1
}
const hours = hoursMap[timeRange] || 1
return {
defaultStartDate: new Date(now.getTime() - hours * 60 * 60 * 1000),
defaultEndDate: now
}
}, [startDate, endDate, timeRange, startCustomDate, endCustomDate])
useEffect(() => {
if (!objectType || !getModelHistory || !token || !connected) {
return
}
const fetchHistory = async () => {
try {
setLoading(true)
setError(null)
const fetchedHistory = await getModelHistory(
objectType,
defaultStartDate,
defaultEndDate
)
if (fetchedHistory) {
setHistoryData(fetchedHistory)
} else {
setError('Failed to fetch history')
}
} catch (err) {
console.error('Error fetching history:', err)
setError('Failed to fetch history')
} finally {
setLoading(false)
}
}
fetchHistory()
}, [
objectType,
getModelHistory,
token,
connected,
defaultStartDate,
defaultEndDate
])
if (!objectType) {
return null
}
const model = getModelByName(objectType)
if (!model) {
return null
}
const modelStats = model.stats || []
if (modelStats.length === 0) {
return null
}
if (error || !Array.isArray(historyData)) {
return null
}
/**
* Extracts the stat key from history data based on stat definition name
* e.g., "states.standby.count" -> "standby", "standbyPrinters" -> "standby"
* The history data keys are the rollup names (like "standby", "printing")
*/
const extractStatKey = (statName) => {
// Handle dot notation paths like "states.standby.count"
if (statName.includes('.')) {
const parts = statName.split('.')
// Extract the middle part (e.g., "standby" from "states.standby.count")
// Skip first part (usually "states") and last part (usually "count")
if (parts.length >= 2) {
return parts[parts.length - 2] // Get second-to-last part
}
return parts[parts.length - 1] // Fallback to last part
}
// Remove common suffixes
return statName
.replace(/Printers?$/i, '')
.replace(/Jobs?$/i, '')
.toLowerCase()
}
/**
* Gets a nested value from an object using dot notation
*/
const getNestedValue = (obj, path) => {
if (!obj || !path) return undefined
const keys = path.split('.')
let value = obj
for (const key of keys) {
if (value === null || value === undefined) return undefined
value = value[key]
}
return value
}
/**
* Extracts numeric value from a rollup object
* Handles both old format (number) and new format ({count: 0}, {sum: 100}, {avg: 5.5})
*/
const extractValue = (value, preferredOperation) => {
if (typeof value === 'number') {
return value
}
if (value && typeof value === 'object') {
// If a preferred operation is specified and exists, use it
if (preferredOperation && value[preferredOperation] !== undefined) {
return value[preferredOperation]
}
// Handle new format: {count: 0}, {sum: 100}, {avg: 5.5}
const operationKeys = ['count', 'sum', 'avg']
for (const key of operationKeys) {
if (value[key] !== undefined) {
return value[key]
}
}
// Fallback: return first numeric value found
const firstValue = Object.values(value)[0]
return typeof firstValue === 'number' ? firstValue : 0
}
return 0
}
/**
* Gets the stat value from history data point
*/
const getStatValueFromPoint = (point, statDef) => {
// Handle special combined stats (support both 'combine' and 'sum' properties)
const combineList = statDef.combine || statDef.sum
if (combineList) {
return combineList.reduce((sum, statName) => {
const tempStatDef = { name: statName }
const value = getStatValueFromPoint(point, tempStatDef)
return sum + (value || 0)
}, 0)
}
const statName = statDef.name
// Determine operation from stat name
let operation
if (statName.endsWith('.count')) operation = 'count'
else if (statName.endsWith('.sum')) operation = 'sum'
else if (statName.endsWith('.avg')) operation = 'avg'
let value
// Extract the key that matches the history data structure
// History data has keys like "standby", "printing" (the rollup names)
const statKey = extractStatKey(statName)
value = point[statKey]
// If not found with extracted key, try direct access with full path
if (value === undefined && statName.includes('.')) {
value = getNestedValue(point, statName)
}
// If still not found, try with original stat name
if (value === undefined) {
value = point[statName]
}
return extractValue(value, operation)
}
// Transform data for the chart using model stats configuration
// Each data point has: { date: '2024-01-01', standby: {count: 0}, printing: {count: 0}, ... }
// We need to transform to: [{ date: '2024-01-01', category: 'Ready', value: 0 }, ...]
const chartData = historyData.flatMap((point) => {
return modelStats.map((statDef) => {
const statValue = getStatValueFromPoint(point, statDef)
const label = statDef.label || statDef.name
return {
date: point.date,
dateFormatted: dayjs(point.date).format('DD/MM HH:mm'),
category: label,
value: statValue || 0
}
})
})
// Sort by date
chartData.sort((a, b) => {
return new Date(a.date) - new Date(b.date)
})
const themeColors = getColors()
// Create color mapping from model stats
const colors = {
success: themeColors.colorSuccess,
processing: themeColors.colorInfo,
error: themeColors.colorError,
warning: themeColors.colorWarning,
default: '#8c8c8c',
cyan: themeColors.colorCyan,
pink: themeColors.colorPink,
purple: themeColors.colorPurple,
magenta: themeColors.colorMagenta,
volcano: themeColors.colorVolcano
}
// Build color range array based on model stats order
const colorRange = modelStats.map((stat) => {
if (stat.color) {
return colors[stat.color] || stat.color
}
return colors.default
})
const chartRows = Array.from(
chartData
.reduce((acc, item) => {
const existing = acc.get(item.date) || {
date: item.date,
dateFormatted: item.dateFormatted
}
existing[item.category] = item.value
acc.set(item.date, existing)
return acc
}, new Map())
.values()
).sort((a, b) => new Date(a.date) - new Date(b.date))
const customTimeRangeContent = (
<Space.Compact>
<DatePicker.RangePicker
onChange={(dates) => {
if (dates) {
setStartCustomDate(dates[0])
setEndCustomDate(dates[1])
} else {
setStartCustomDate(null)
setEndCustomDate(null)
}
}}
value={[startCustomDate, endCustomDate]}
/>
<Button
type='primary'
onClick={() => {
if (startCustomDate && endCustomDate) {
setTimeRange('custom')
}
}}
disabled={!startCustomDate || !endCustomDate}
icon={<CheckIcon />}
></Button>
</Space.Compact>
)
return (
<Card
style={{ width: '100%' }}
styles={{ body: { padding: '12px', ...styles } }}
>
<Flex gap='small' vertical>
{!startDate && !endDate && (
<Flex justify='space-between'>
<Flex align='center' gap='5px'>
<Popover
content={customTimeRangeContent}
trigger='hover'
arrow={false}
placement='bottomLeft'
styles={{ body: { borderRadius: '22.5px' } }}
>
<Segmented
size='small'
options={[{ label: 'Custom', value: 'custom' }]}
value={timeRange}
onChange={setTimeRange}
disabled={loading}
/>
</Popover>
</Flex>
<Flex gap='middle'>
{loading == true && <LoadingOutlined />}
<Segmented
size='small'
options={[
{ label: '24hr', value: '24hrs' },
{ label: '12hr', value: '12hrs' },
{ label: '8hr', value: '8hrs' },
{ label: '4hr', value: '4hrs' },
{ label: '1hr', value: '1hrs' }
]}
value={timeRange}
onChange={setTimeRange}
disabled={loading}
/>
</Flex>
</Flex>
)}
{chartRows.length > 0 && (
<div style={{ width: '100%', height: `${height}px` }}>
<ResponsiveContainer width='100%' height='100%'>
<BarChart data={chartRows}>
<CartesianGrid
strokeDasharray='3 3'
stroke={isDarkMode ? '#303030' : '#f0f0f0'}
/>
<XAxis
dataKey='dateFormatted'
tick={{
fill: isDarkMode ? '#d9d9d9' : '#595959',
fontSize: 12
}}
/>
<YAxis
tick={{
fill: isDarkMode ? '#d9d9d9' : '#595959',
fontSize: 12
}}
/>
<Tooltip
contentStyle={{
backgroundColor: isDarkMode ? '#1f1f1f' : '#ffffff',
border: `1px solid ${isDarkMode ? '#303030' : '#f0f0f0'}`,
borderRadius: 8
}}
/>
<Legend />
{modelStats.map((statDef, index) => {
const category = statDef.label || statDef.name
return (
<Bar
key={category}
dataKey={category}
stackId='history'
fill={colorRange[index] || colors.default}
/>
)
})}
</BarChart>
</ResponsiveContainer>
</div>
)}
{loading == false && chartRows.length == 0 && (
<Flex
justify='center'
align='center'
style={{ height: `${height}px` }}
>
<MissingPlaceholder message='No data available.' />
</Flex>
)}
</Flex>
</Card>
)
}
ModelHistoryDisplay.propTypes = {
objectType: PropTypes.string.isRequired,
startDate: PropTypes.oneOfType([
PropTypes.string,
PropTypes.instanceOf(Date)
]),
styles: PropTypes.object,
endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
}
export default ModelHistoryDisplay

View File

@ -29,6 +29,7 @@ const StateDisplay = ({
progress: 0 progress: 0
} }
const [showMessageModal, setShowMessageModal] = useState(false) const [showMessageModal, setShowMessageModal] = useState(false)
const progressValue = Math.round(currentState.progress * 1000) / 10
return ( return (
<> <>
<Flex gap='small' align={'center'}> <Flex gap='small' align={'center'}>
@ -40,9 +41,10 @@ const StateDisplay = ({
{showProgress && {showProgress &&
loadingProgressTypes.includes(currentState.type) && loadingProgressTypes.includes(currentState.type) &&
currentState?.progress && currentState?.progress &&
progressValue !== 100 &&
currentState?.progress > 0 ? ( currentState?.progress > 0 ? (
<Progress <Progress
percent={Math.round(currentState.progress * 100)} percent={progressValue}
status={ status={
activeProgressTypes.includes(currentState.type) ? 'active' : '' activeProgressTypes.includes(currentState.type) ? 'active' : ''
} }

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/graphicon.svg?react'
const GraphIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default GraphIcon

View File

@ -30,7 +30,13 @@ export const FilamentStock = {
'createdAt', 'createdAt',
'updatedAt' 'updatedAt'
], ],
filters: ['_id', 'filament', 'filament._id', 'filamentSku', 'filamentSku._id'], filters: [
'_id',
'filament',
'filament._id',
'filamentSku',
'filamentSku._id'
],
sorters: ['createdAt', 'updatedAt', 'filament', 'filamentSku'], sorters: ['createdAt', 'updatedAt', 'filament', 'filamentSku'],
group: ['filament', 'filamentSku'], group: ['filament', 'filamentSku'],
properties: [ properties: [
@ -107,7 +113,7 @@ export const FilamentStock = {
label: 'Stock location', label: 'Stock location',
type: 'object', type: 'object',
objectType: 'stockLocation', objectType: 'stockLocation',
required: false, required: true,
showHyperlink: true, showHyperlink: true,
columnWidth: 200 columnWidth: 200
}, },

View File

@ -8,7 +8,7 @@ export const StockEvent = {
prefix: 'SEV', prefix: 'SEV',
icon: StockEventIcon, icon: StockEventIcon,
actions: [], actions: [],
columns: ['_reference', 'owner', 'parent', 'value', 'createdAt'], columns: ['_reference', 'owner', 'parent', 'value', 'createdAt', 'updatedAt'],
filters: ['_id', 'owner', 'parent'], filters: ['_id', 'owner', 'parent'],
sorters: ['createdAt'], sorters: ['createdAt'],
properties: [ properties: [