feat: Add simulation dashboard editor with analyzer and manager components.
This commit is contained in:
170
app/src/components/SimulationDashboard/AnalyzerManager.tsx
Normal file
170
app/src/components/SimulationDashboard/AnalyzerManager.tsx
Normal 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;
|
||||||
@@ -5,9 +5,11 @@ import ControlPanel from "./ControlPanel";
|
|||||||
import SwapModal from "./SwapModal";
|
import SwapModal from "./SwapModal";
|
||||||
import { Block } from "../../types/exportedTypes";
|
import { Block } from "../../types/exportedTypes";
|
||||||
import DataModelPanel from "./components/models/DataModelPanel";
|
import DataModelPanel from "./components/models/DataModelPanel";
|
||||||
|
import AnalyzerManager from "./AnalyzerManager";
|
||||||
|
|
||||||
import { useSceneContext } from "../../modules/scene/sceneContext";
|
import { useSceneContext } from "../../modules/scene/sceneContext";
|
||||||
import useModuleStore from "../../store/ui/useModuleStore";
|
import useModuleStore from "../../store/ui/useModuleStore";
|
||||||
|
import { usePlayButtonStore } from "../../store/ui/usePlayButtonStore";
|
||||||
import { calculateMinBlockSize } from "./functions/block/calculateMinBlockSize";
|
import { calculateMinBlockSize } from "./functions/block/calculateMinBlockSize";
|
||||||
import { handleElementDragStart, handleElementResizeStart, handleBlockResizeStart, handleSwapStart, handleSwapTarget, handleBlockClick, handleElementClick } from "./functions/eventHandlers";
|
import { handleElementDragStart, handleElementResizeStart, handleBlockResizeStart, handleSwapStart, handleSwapTarget, handleBlockClick, handleElementClick } from "./functions/eventHandlers";
|
||||||
import BlockGrid from "./components/block/BlockGrid";
|
import BlockGrid from "./components/block/BlockGrid";
|
||||||
@@ -25,6 +27,7 @@ const DashboardEditor: React.FC = () => {
|
|||||||
const { selectedVersion } = versionStore();
|
const { selectedVersion } = versionStore();
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
const { activeModule } = useModuleStore();
|
const { activeModule } = useModuleStore();
|
||||||
|
const { isPlaying } = usePlayButtonStore();
|
||||||
const {
|
const {
|
||||||
blocks,
|
blocks,
|
||||||
selectedBlock,
|
selectedBlock,
|
||||||
@@ -410,6 +413,8 @@ const DashboardEditor: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{activeModule === "simulation" && isPlaying && <AnalyzerManager />}
|
||||||
|
|
||||||
{/* BlockGrid */}
|
{/* BlockGrid */}
|
||||||
|
|
||||||
<div className="block-grid-container">
|
<div className="block-grid-container">
|
||||||
|
|||||||
@@ -426,10 +426,8 @@ function Analyzer() {
|
|||||||
|
|
||||||
// Update historical data
|
// Update historical data
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
if (!historicalDataRef.current[vehicle.modelUuid]) {
|
const currentData = historicalDataRef.current[vehicle.modelUuid] || [];
|
||||||
historicalDataRef.current[vehicle.modelUuid] = [];
|
historicalDataRef.current[vehicle.modelUuid] = [...currentData, {
|
||||||
}
|
|
||||||
historicalDataRef.current[vehicle.modelUuid].push({
|
|
||||||
timestamp,
|
timestamp,
|
||||||
phase: vehicle.currentPhase,
|
phase: vehicle.currentPhase,
|
||||||
load: vehicle.currentLoad,
|
load: vehicle.currentLoad,
|
||||||
@@ -438,7 +436,7 @@ function Analyzer() {
|
|||||||
performance: performance.performanceRate,
|
performance: performance.performanceRate,
|
||||||
speed: vehicle.speed,
|
speed: vehicle.speed,
|
||||||
tripsCompleted,
|
tripsCompleted,
|
||||||
});
|
}].slice(-100);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assetId: vehicle.modelUuid,
|
assetId: vehicle.modelUuid,
|
||||||
@@ -699,10 +697,8 @@ function Analyzer() {
|
|||||||
|
|
||||||
// Update historical data
|
// Update historical data
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
if (!historicalDataRef.current[machine.modelUuid]) {
|
const currentData = historicalDataRef.current[machine.modelUuid] || [];
|
||||||
historicalDataRef.current[machine.modelUuid] = [];
|
historicalDataRef.current[machine.modelUuid] = [...currentData, {
|
||||||
}
|
|
||||||
historicalDataRef.current[machine.modelUuid].push({
|
|
||||||
timestamp,
|
timestamp,
|
||||||
processTime: actualProcessTime,
|
processTime: actualProcessTime,
|
||||||
partsProcessed,
|
partsProcessed,
|
||||||
@@ -710,7 +706,7 @@ function Analyzer() {
|
|||||||
state: machine.state,
|
state: machine.state,
|
||||||
defectRate,
|
defectRate,
|
||||||
performance: performance.performanceRate,
|
performance: performance.performanceRate,
|
||||||
});
|
}].slice(-100);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assetId: machine.modelUuid,
|
assetId: machine.modelUuid,
|
||||||
@@ -1340,9 +1336,9 @@ function Analyzer() {
|
|||||||
const averageResidenceTime =
|
const averageResidenceTime =
|
||||||
materialHistoryRef.current.length > 0
|
materialHistoryRef.current.length > 0
|
||||||
? materialHistoryRef.current.reduce((sum, entry) => {
|
? materialHistoryRef.current.reduce((sum, entry) => {
|
||||||
const residenceTime = new Date(entry.removedAt).getTime() - (entry.material.startTime || 0);
|
const residenceTime = new Date(entry.removedAt).getTime() - (entry.material.startTime || 0);
|
||||||
return sum + (residenceTime || 0);
|
return sum + (residenceTime || 0);
|
||||||
}, 0) / materialHistoryRef.current.length
|
}, 0) / materialHistoryRef.current.length
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Bottleneck Identification
|
// Bottleneck Identification
|
||||||
|
|||||||
Reference in New Issue
Block a user