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 { 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">
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user