import { useEffect, useState, useContext, useMemo } from 'react'
import { Card, Spin, Segmented, Flex } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import { Column } from '@ant-design/charts'
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'
const HistoryDisplay = ({ objectType, startDate, endDate, styles }) => {
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 { 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
}
}
// 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])
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,
info: themeColors.colorInfo,
error: themeColors.colorError,
warning: themeColors.colorWarning,
default: '#8c8c8c'
}
// 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 config = {
data: chartData,
xField: 'dateFormatted',
yField: 'value',
theme: {
type: isDarkMode ? 'dark' : 'light',
fontFamily: '"DM Sans", -apple-system, BlinkMacSystemFont, sans-serif'
},
stack: true,
colorField: 'category',
columnWidthRatio: 1,
scale: {
color: {
range: colorRange
}
},
smooth: true,
interaction: {
tooltip: {
marker: false
}
},
legend: {
position: 'top'
},
animation: {
appear: {
animation: 'wave-in',
duration: 1000
}
}
}
return (
}>
{!startDate && !endDate && (
)}
)
}
HistoryDisplay.propTypes = {
objectType: PropTypes.string.isRequired,
startDate: PropTypes.oneOfType([
PropTypes.string,
PropTypes.instanceOf(Date)
]),
styles: PropTypes.object,
endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)])
}
export default HistoryDisplay