import { useEffect, useState, useContext, useRef } from 'react' import { Flex, Tag, Card, Typography, Skeleton, Badge } from 'antd' import PropTypes from 'prop-types' import { getModelByName } from '../../../database/ObjectModels' import { ApiServerContext } from '../context/ApiServerContext' import { AuthContext } from '../context/AuthContext' import { round } from '../utils/Utils' const { Text } = Typography /** * Maps stat names to Tag colors for visual indication */ const getTagColor = (statName) => { const name = statName.toLowerCase() // Success states if ( name.includes('complete') || name.includes('ready') || name.includes('standby') || name.includes('success') ) { return 'success' } // Error states if ( name.includes('error') || name.includes('failed') || name.includes('cancelled') ) { return 'error' } // Warning states if ( name.includes('queued') || name.includes('pending') || name.includes('waiting') ) { return 'warning' } if (name.includes('printing')) { return 'processing' } // Default states return 'default' } /*i* * Gets a nested value from an object using dot notation * e.g., getNestedValue(obj, 'states.ready') -> obj.states.ready */ 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 extractRollupValue = (value) => { if (typeof value === 'number') { return value } if (value && typeof value === 'object') { // 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 the stats data object * Handles both flattened and nested structures, including dot notation paths * Now also handles new format with operation objects ({count: 0}, {sum: 100}, etc.) */ const getStatValue = (stats, statName, statDef) => { if (!stats) return 0 // Handle special combined stats (support both 'combine' and 'sum' properties) const combineList = statDef.combine || statDef.sum if (combineList) { return combineList.reduce((sum, name) => { const value = getStatValue(stats, name, {}) return sum + (value || 0) }, 0) } // Handle dot notation paths (e.g., "states.ready") if (statName.includes('.')) { const value = getNestedValue(stats, statName) return extractRollupValue(value) } // Try direct access first (flattened structure) if (stats[statName] !== undefined) { return extractRollupValue(stats[statName]) } // Try nested structure (stats.states.statName) if (stats.states && stats.states[statName] !== undefined) { return extractRollupValue(stats.states[statName]) } // Try with objectType prefix removed (e.g., "standbyPrinters" -> "standby") const simplifiedName = statName.replace( /^(standby|printing|error|offline|queued|complete|failed|cancelled|draft)/i, (match) => { return match.toLowerCase() } ) if (stats[simplifiedName] !== undefined) { return extractRollupValue(stats[simplifiedName]) } if (stats.states && stats.states[simplifiedName] !== undefined) { return extractRollupValue(stats.states[simplifiedName]) } return 0 } /** * Extracts the base stat name from model stat names * e.g., "standbyPrinters" -> "standby", "printingPrinters" -> "printing" * Preserves dot notation paths (e.g., "states.ready" -> "states.ready") */ const extractBaseStatName = (statName) => { // If it's a dot notation path, return as-is if (statName.includes('.')) { return statName } // Remove common suffixes const baseName = statName .replace(/Printers?$/i, '') .replace(/Jobs?$/i, '') .toLowerCase() return baseName } const StatsDisplay = ({ objectType, stats: statsProp }) => { const { getModelStats, connected, subscribeToModelStats } = useContext(ApiServerContext) const { token } = useContext(AuthContext) const [stats, setStats] = useState(statsProp || null) const [loading, setLoading] = useState(!statsProp) const [error, setError] = useState(null) const initializedRef = useRef(false) useEffect(() => { // If stats are provided as prop, use them and don't fetch if (statsProp) { setStats(statsProp) setLoading(false) initializedRef.current = true return } // Otherwise, fetch stats if ( !objectType || !getModelStats || !token || !connected || initializedRef.current == true ) { return } const fetchStats = async () => { try { setLoading(true) setError(null) const fetchedStats = await getModelStats(objectType) if (fetchedStats) { setStats(fetchedStats) } else { setError('Failed to fetch stats') } } catch (err) { console.error('Error fetching stats:', err) setError('Failed to fetch stats') } finally { setLoading(false) initializedRef.current = true } } fetchStats() }, [objectType, getModelStats, token, connected, statsProp]) // Subscribe to real-time stats updates useEffect(() => { // Only subscribe if stats are not provided as prop and we have the required dependencies if (!objectType || !connected || !subscribeToModelStats) { return } const statsUnsubscribe = subscribeToModelStats( objectType, (updatedStats) => { setStats(updatedStats) } ) return () => { if (statsUnsubscribe) statsUnsubscribe() } }, [objectType, connected, subscribeToModelStats, statsProp]) if (!objectType) { return null } const model = getModelByName(objectType) if (!model) { return null } const Icon = model.icon const modelStats = model.stats || [] if (modelStats.length === 0) { return null } if (error) { return null } return ( {modelStats.map((statDef) => { const baseStatName = extractBaseStatName(statDef.name) var statValue = getStatValue(stats, baseStatName, statDef) const tagColor = statDef.color || getTagColor(statDef.name) const label = statDef.label || statDef.name if (statDef?.roundNumber) { statValue = round(statValue, statDef?.roundNumber) } const statusColors = [ 'success', 'warning', 'error', 'processing', 'default' ] var badgeProps = { status: tagColor } if (!statusColors.includes(tagColor)) { badgeProps = { color: tagColor } } const content = ( {label} {Icon && ( )} {loading ? ( ) : ( {statDef?.prefix} {statValue} {statDef?.suffix} )} ) if (tagColor === 'default') { return ( {content} ) } return ( {content} ) })} ) } StatsDisplay.propTypes = { objectType: PropTypes.string.isRequired, stats: PropTypes.object // Optional - if not provided, will fetch automatically } export default StatsDisplay