feat: Add simulation dashboard editor with analyzer and manager components.

This commit is contained in:
2025-12-18 10:08:53 +05:30
parent 97ab47354c
commit 5b9f3f1728
3 changed files with 184 additions and 13 deletions

View File

@@ -0,0 +1,170 @@
import { useEffect } from "react";
import { useSceneContext } from "../../modules/scene/sceneContext";
const AnalyzerManager: React.FC = () => {
const { analysisStore, simulationDashBoardStore } = useSceneContext();
const { analysis, getAssetAnalysis, getSystemMetrics } = analysisStore();
const { blocks, updateGraphData } = simulationDashBoardStore();
// Helper to resolve value from path like "efficiency.overallEffectiveness"
const resolvePath = (obj: any, path: string | string[]) => {
if (!obj || !path) return undefined;
const actualPath = Array.isArray(path) ? path[0] : path;
if (typeof actualPath !== "string") return undefined;
return actualPath.split(".").reduce((prev, curr) => (prev ? prev[curr] : undefined), obj);
};
// Helper to get formatted value
const getFormattedValue = (value: any): string => {
if (typeof value === "number") {
return value.toFixed(2);
}
return String(value ?? "-");
};
useEffect(() => {
if (!analysis) return;
blocks.forEach((block) => {
block.elements.forEach((element) => {
// 1. Handle Label-Value (Direct DOM Manipulation)
if (element.type === "label-value") {
if (!element.dataSource || !element.dataValue) return;
let value: any = "-";
// Ensure dataValue is treated as string if possible, or take first if array
const mixedDataValue = element.dataValue;
const dataValueStr = Array.isArray(mixedDataValue) ? mixedDataValue[0] : mixedDataValue;
if (typeof dataValueStr !== "string") return;
if (element.dataSource === "global") {
// Handle global metrics
// Expected format: "global.systemPerformance.overallOEE" -> we need last part relative to metrics
// But dataValue usually comes as "systemPerformance.overallOEE" if dataSource is global?
// Let's check ElementEditor:
// It passes "global.systemPerformance.overallOEE" as the ID.
// We need to parse this.
const metrics = getSystemMetrics();
if (metrics) {
// If the ID starts with global., strip it.
const path = dataValueStr.startsWith("global.")
? dataValueStr.replace("global.", "")
: dataValueStr;
value = resolvePath(metrics, path);
}
} else {
// Handle Asset Specific
const assetAnalysis = getAssetAnalysis(element.dataSource);
if (assetAnalysis) {
value = resolvePath(assetAnalysis, dataValueStr);
}
}
// Update DOM
const valueElement = document.querySelector(`[data-element-id="${element.elementUuid}"] .value-text`);
if (valueElement) {
valueElement.textContent = getFormattedValue(value);
}
}
// 2. Handle Graph (Store Update)
// We shouldn't update store on every frame if data hasn't changed or simpler approach?
// For graphs, we usually push new data points.
// Assuming we want to visualize historical data or current state trends.
else if (element.type === "graph") {
if (element.dataType === "single-machine") {
if (!element.dataSource || !element.dataValue) return;
// Single machine, multiple values potentially?
// element.dataValue is string[] for single-machine graph
const assetId = element.dataSource as string;
const dataKeys = element.dataValue as string[]; // e.g. ["efficiency.overallEffectiveness"]
// For now support getting the FIRST value as the main value to plot?
// Or if we track history?
// The requirement says: "update the dom ... to show the value".
// UseAnalysisStore has 'historicalData'.
// But usually graphs update by appending new points.
// Here we might just pull the latest historical data or current value.
const assetAnalysis = getAssetAnalysis(assetId);
if (assetAnalysis) {
// If it's a historical plot, we might want to read from assetAnalysis.historicalData (if it exists on client)
// or just push current value to the graph element's local state.
// But here we are asked to manage values using AnalyzerManager.
// Let's construct a data point.
// GraphDataPoint = { name: string, value: number, ... }
// We use analysis.lastUpdateTime or similar as 'name' (X-axis)
const timeLabel = new Date().toLocaleTimeString();
// We need to construct a new GraphDataPoint
const newPoint: any = { name: timeLabel };
let hasValidData = false;
dataKeys.forEach((key, index) => {
const val = resolvePath(assetAnalysis, key);
if (typeof val === "number") {
newPoint["value"] = val; // Primary value
// If we have multiple lines, we might need value1, value2 etc, but Recharts usually takes keys.
// The ElementContent uses 'value' key hardcoded for single line?
// Let's check ElementContent.tsx...
// It renders <Line dataKey="value" ... />
// So it only supports ONE series called 'value' currently for default graphs?
// Or we can map index to keys?
// For now, let's assume single generic 'value' or update if strictly array.
hasValidData = true;
}
});
if (hasValidData) {
// We need to update the element's graphData.
// This is a store update, triggers re-render.
// BE CAREFUL: varying this too fast will kill performance.
// Maybe throttle this? useAnalysisStore updates often?
const currentGraphData = element.graphData || [];
const newGraphData = [...currentGraphData, newPoint].slice(-20); // Keep last 20 points
updateGraphData(block.blockUuid, element.elementUuid, newGraphData);
}
}
} else if (element.dataType === "multiple-machine") {
// Multiple machines, single common value
const assetIds = element.dataSource as string[];
const commonValueKey = element.commonValue as string;
if (assetIds && commonValueKey) {
// Bar chart comparing machines?
// Data point per machine.
const newGraphData = assetIds.map(assetId => {
const assetAnalysis = getAssetAnalysis(assetId);
const val = assetAnalysis ? resolvePath(assetAnalysis, commonValueKey) : 0;
return {
name: assetAnalysis?.assetName || assetId,
value: typeof val === "number" ? val : 0
};
});
updateGraphData(block.blockUuid, element.elementUuid, newGraphData);
}
}
}
});
});
}, [analysis, blocks, getAssetAnalysis, getSystemMetrics, updateGraphData]);
return null;
};
export default AnalyzerManager;

View File

@@ -5,9 +5,11 @@ import ControlPanel from "./ControlPanel";
import SwapModal from "./SwapModal";
import { Block } from "../../types/exportedTypes";
import DataModelPanel from "./components/models/DataModelPanel";
import AnalyzerManager from "./AnalyzerManager";
import { useSceneContext } from "../../modules/scene/sceneContext";
import useModuleStore from "../../store/ui/useModuleStore";
import { usePlayButtonStore } from "../../store/ui/usePlayButtonStore";
import { calculateMinBlockSize } from "./functions/block/calculateMinBlockSize";
import { handleElementDragStart, handleElementResizeStart, handleBlockResizeStart, handleSwapStart, handleSwapTarget, handleBlockClick, handleElementClick } from "./functions/eventHandlers";
import BlockGrid from "./components/block/BlockGrid";
@@ -25,6 +27,7 @@ const DashboardEditor: React.FC = () => {
const { selectedVersion } = versionStore();
const { projectId } = useParams();
const { activeModule } = useModuleStore();
const { isPlaying } = usePlayButtonStore();
const {
blocks,
selectedBlock,
@@ -410,6 +413,8 @@ const DashboardEditor: React.FC = () => {
/>
)}
{activeModule === "simulation" && isPlaying && <AnalyzerManager />}
{/* BlockGrid */}
<div className="block-grid-container">

View File

@@ -426,10 +426,8 @@ function Analyzer() {
// Update historical data
const timestamp = new Date().toISOString();
if (!historicalDataRef.current[vehicle.modelUuid]) {
historicalDataRef.current[vehicle.modelUuid] = [];
}
historicalDataRef.current[vehicle.modelUuid].push({
const currentData = historicalDataRef.current[vehicle.modelUuid] || [];
historicalDataRef.current[vehicle.modelUuid] = [...currentData, {
timestamp,
phase: vehicle.currentPhase,
load: vehicle.currentLoad,
@@ -438,7 +436,7 @@ function Analyzer() {
performance: performance.performanceRate,
speed: vehicle.speed,
tripsCompleted,
});
}].slice(-100);
return {
assetId: vehicle.modelUuid,
@@ -699,10 +697,8 @@ function Analyzer() {
// Update historical data
const timestamp = new Date().toISOString();
if (!historicalDataRef.current[machine.modelUuid]) {
historicalDataRef.current[machine.modelUuid] = [];
}
historicalDataRef.current[machine.modelUuid].push({
const currentData = historicalDataRef.current[machine.modelUuid] || [];
historicalDataRef.current[machine.modelUuid] = [...currentData, {
timestamp,
processTime: actualProcessTime,
partsProcessed,
@@ -710,7 +706,7 @@ function Analyzer() {
state: machine.state,
defectRate,
performance: performance.performanceRate,
});
}].slice(-100);
return {
assetId: machine.modelUuid,
@@ -1340,9 +1336,9 @@ function Analyzer() {
const averageResidenceTime =
materialHistoryRef.current.length > 0
? materialHistoryRef.current.reduce((sum, entry) => {
const residenceTime = new Date(entry.removedAt).getTime() - (entry.material.startTime || 0);
return sum + (residenceTime || 0);
}, 0) / materialHistoryRef.current.length
const residenceTime = new Date(entry.removedAt).getTime() - (entry.material.startTime || 0);
return sum + (residenceTime || 0);
}, 0) / materialHistoryRef.current.length
: 0;
// Bottleneck Identification