feat: Introduce a new simulation dashboard editor with block and element components, dedicated styling, visualization state management, and an analyzer.

This commit is contained in:
2025-12-20 09:43:28 +05:30
parent c072648397
commit 525bfb6541
6 changed files with 77 additions and 84 deletions

View File

@@ -418,7 +418,7 @@ const DashboardEditor: React.FC = () => {
{/* BlockGrid */} {/* BlockGrid */}
<div className="block-grid-container"> <div className={`${editMode ? "editable" : ""} block-grid-container`}>
<BlockGrid <BlockGrid
blocks={blocks} blocks={blocks}
handleAddElement={async (blockId, type, graphType) => { handleAddElement={async (blockId, type, graphType) => {

View File

@@ -13,6 +13,7 @@ import { AddIcon, DeviceIcon, ParametersIcon, ResizeHeightIcon } from "../../../
import DataDetailedDropdown from "../../../ui/inputs/DataDetailedDropdown"; import DataDetailedDropdown from "../../../ui/inputs/DataDetailedDropdown";
import RenameInput from "../../../ui/inputs/RenameInput"; import RenameInput from "../../../ui/inputs/RenameInput";
import DataSourceSelector from "../../../ui/inputs/DataSourceSelector"; import DataSourceSelector from "../../../ui/inputs/DataSourceSelector";
import { useVisualizationStore } from "../../../../store/visualization/useVisualizationStore";
interface BlockEditorProps { interface BlockEditorProps {
blockEditorRef: RefObject<HTMLDivElement>; blockEditorRef: RefObject<HTMLDivElement>;
@@ -38,15 +39,10 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
handleRemoveBlock, handleRemoveBlock,
}) => { }) => {
const [color, setColor] = useState("#000000"); const [color, setColor] = useState("#000000");
// Dragging for block editor
// Use position from VisualizationStore
const { editorPosition, setEditorPosition } = useVisualizationStore();
const panelRef = useRef<HTMLDivElement | null>(null); const panelRef = useRef<HTMLDivElement | null>(null);
const [position, setPosition] = useState<{ x: number; y: number }>(() => {
if (typeof window !== "undefined") {
// approximate initial left using a default panel width of 300
return { x: Math.max(0, window.innerWidth - 300 - 40), y: 80 };
}
return { x: 120, y: 80 };
});
const initialPositionRef = useRef<{ x: number; y: number } | null>(null); const initialPositionRef = useRef<{ x: number; y: number } | null>(null);
const draggingRef = useRef(false); const draggingRef = useRef(false);
const startXRef = useRef(0); const startXRef = useRef(0);
@@ -54,14 +50,21 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
const startLeftRef = useRef(0); const startLeftRef = useRef(0);
const startTopRef = useRef(0); const startTopRef = useRef(0);
const position = editorPosition || { x: 100, y: 80 };
const setPosition = (newPosition: { x: number; y: number }) => {
setEditorPosition(newPosition);
};
// compute exact initial position once we have panel dimensions // compute exact initial position once we have panel dimensions
useEffect(() => { useEffect(() => {
const panelEl = panelRef.current || (blockEditorRef && (blockEditorRef as any).current); if (!editorPosition) {
const width = panelEl?.offsetWidth || 300; const panelEl = panelRef.current || (blockEditorRef && (blockEditorRef as any).current);
const nx = Math.max(0, window.innerWidth - width - 40); const width = panelEl?.offsetWidth || 300;
const ny = 80; const nx = Math.max(0, window.innerWidth - width - 40);
initialPositionRef.current = { x: nx, y: ny }; const ny = 80;
setPosition({ x: nx, y: ny }); initialPositionRef.current = { x: nx, y: ny };
setPosition({ x: nx, y: ny });
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
@@ -73,7 +76,6 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
}, []); }, []);
const startDrag = (ev: React.PointerEvent) => { const startDrag = (ev: React.PointerEvent) => {
if (ev.detail > 1) return; if (ev.detail > 1) return;
const panel = panelRef.current || (blockEditorRef && (blockEditorRef as any).current); const panel = panelRef.current || (blockEditorRef && (blockEditorRef as any).current);
@@ -120,8 +122,6 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
}; };
const resetPosition = () => { const resetPosition = () => {
console.log("adsasdasdadasd");
if (initialPositionRef.current) { if (initialPositionRef.current) {
setPosition(initialPositionRef.current); setPosition(initialPositionRef.current);
return; return;
@@ -143,11 +143,7 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
className="panel block-editor-panel" className="panel block-editor-panel"
style={{ position: "fixed", left: position.x, top: position.y, zIndex: 999 }} style={{ position: "fixed", left: position.x, top: position.y, zIndex: 999 }}
> >
<div <div className="resize-icon" onDoubleClick={resetPosition} onPointerDown={startDrag}>
className="resize-icon"
onDoubleClick={resetPosition}
onPointerDown={startDrag}
>
<ResizeHeightIcon /> <ResizeHeightIcon />
</div> </div>

View File

@@ -9,6 +9,7 @@ import { AddIcon, DeviceIcon, ParametersIcon, ResizeHeightIcon } from "../../../
import DataDetailedDropdown from "../../../ui/inputs/DataDetailedDropdown"; import DataDetailedDropdown from "../../../ui/inputs/DataDetailedDropdown";
import { useSceneContext } from "../../../../modules/scene/sceneContext"; import { useSceneContext } from "../../../../modules/scene/sceneContext";
import ElementDesign from "./ElementDesign"; import ElementDesign from "./ElementDesign";
import { useVisualizationStore } from "../../../../store/visualization/useVisualizationStore";
interface ElementEditorProps { interface ElementEditorProps {
elementEditorRef: RefObject<HTMLDivElement>; elementEditorRef: RefObject<HTMLDivElement>;
@@ -64,27 +65,30 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
const [selectType, setSelectType] = useState("design"); const [selectType, setSelectType] = useState("design");
const [selectDataMapping, setSelectDataMapping] = useState(element?.type === "graph" && element.dataBinding?.dataType === "multiple-machine" ? "multipleMachine" : "singleMachine"); const [selectDataMapping, setSelectDataMapping] = useState(element?.type === "graph" && element.dataBinding?.dataType === "multiple-machine" ? "multipleMachine" : "singleMachine");
// Dragging state for the panel // Use shared position from VisualizationStore
const { editorPosition, setEditorPosition } = useVisualizationStore();
const panelRef = useRef<HTMLDivElement | null>(null); const panelRef = useRef<HTMLDivElement | null>(null);
const [position, setPosition] = useState<{ x: number; y: number }>(() => {
if (typeof window !== "undefined") {
// approximate initial left using a default panel width of 300
return { x: Math.max(0, window.innerWidth - 300 - 40), y: 80 };
}
return { x: 100, y: 80 };
});
const initialPositionRef = useRef<{ x: number; y: number } | null>(null); const initialPositionRef = useRef<{ x: number; y: number } | null>(null);
// Compute position from store or initialize
const position = editorPosition || { x: 100, y: 80 };
const setPosition = (newPosition: { x: number; y: number }) => {
setEditorPosition(newPosition);
};
// On mount, compute exact initial position based on panel width and store it // On mount, compute exact initial position based on panel width and store it
useEffect(() => { useEffect(() => {
const panelEl = panelRef.current || (elementEditorRef && (elementEditorRef as any).current); if (!editorPosition) {
const width = panelEl?.offsetWidth || 300; const panelEl = panelRef.current || (elementEditorRef && (elementEditorRef as any).current);
const nx = Math.max(0, window.innerWidth - width - 40); const width = panelEl?.offsetWidth || 300;
const ny = 80; const nx = Math.max(0, window.innerWidth - width - 40);
initialPositionRef.current = { x: nx, y: ny }; const ny = 80;
setPosition({ x: nx, y: ny }); initialPositionRef.current = { x: nx, y: ny };
setPosition({ x: nx, y: ny });
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const draggingRef = useRef(false); const draggingRef = useRef(false);
const startXRef = useRef(0); const startXRef = useRef(0);
const startYRef = useRef(0); const startYRef = useRef(0);
@@ -157,7 +161,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
window.removeEventListener("pointerup", onPointerUp); window.removeEventListener("pointerup", onPointerUp);
try { try {
(panel as Element).releasePointerCapture?.((e as any).pointerId); (panel as Element).releasePointerCapture?.((e as any).pointerId);
} catch (err) { } } catch (err) {}
}; };
window.addEventListener("pointermove", onPointerMove); window.addEventListener("pointermove", onPointerMove);

View File

@@ -1376,17 +1376,18 @@ function Analyzer() {
// Occupancy trends // Occupancy trends
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
if (!historicalDataRef.current[storage.modelUuid]) { const currentData = historicalDataRef.current[storage.modelUuid] || [];
historicalDataRef.current[storage.modelUuid] = []; historicalDataRef.current[storage.modelUuid] = [
} ...currentData,
historicalDataRef.current[storage.modelUuid].push({ {
timestamp, timestamp,
currentLoad, currentLoad,
utilizationRate, utilizationRate,
operation: storeOps > retrieveOps ? "store" : "retrieve", operation: storeOps > retrieveOps ? "store" : "retrieve",
totalOps, totalOps,
state: storage.state, state: storage.state,
}); },
].slice(-100);
// Calculate peak occupancy from historical data // Calculate peak occupancy from historical data
const occupancyData = historicalDataRef.current[storage.modelUuid] || []; const occupancyData = historicalDataRef.current[storage.modelUuid] || [];

View File

@@ -1,13 +1,22 @@
import { create } from "zustand"; import { create } from "zustand";
import { immer } from "zustand/middleware/immer"; import { immer } from "zustand/middleware/immer";
interface PanelPosition {
x: number;
y: number;
}
interface VisualizationState { interface VisualizationState {
// blocks: Block[] editorPosition: PanelPosition | null;
setEditorPosition: (position: PanelPosition) => void;
} }
export const useVisualizationStore = create<VisualizationState>()( export const useVisualizationStore = create<VisualizationState>()(
immer((set) => ({ immer((set) => ({
// blocks: [], editorPosition: null,
setEditorPosition: (position) =>
set((state) => {
state.editorPosition = position;
}),
})) }))
); );

View File

@@ -12,7 +12,7 @@
left: 0; left: 0;
pointer-events: none; pointer-events: none;
*> { * > {
pointer-events: auto; pointer-events: auto;
} }
@@ -256,8 +256,12 @@
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
width: fit-content !important;
pointer-events: none;
}
pointer-events: auto; .block-grid-container.editable {
pointer-events: all;
} }
.panel { .panel {
@@ -272,7 +276,6 @@
box-shadow: 0px 4px 8px rgba(60, 60, 67, 0.1019607843); box-shadow: 0px 4px 8px rgba(60, 60, 67, 0.1019607843);
z-index: 3; z-index: 3;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 11px; gap: 11px;
@@ -321,7 +324,6 @@
} }
.design-section { .design-section {
padding: 4px; padding: 4px;
outline: 1px solid var(--border-color); outline: 1px solid var(--border-color);
outline-offset: -1px; outline-offset: -1px;
@@ -356,7 +358,7 @@
} }
.position-canvas { .position-canvas {
background: #8D70AD33; background: #8d70ad33;
height: 110px; height: 110px;
border-radius: 17px; border-radius: 17px;
@@ -369,7 +371,7 @@
.canvas { .canvas {
width: 60%; width: 60%;
height: 27px; height: 27px;
background: #E0DFFF80; background: #e0dfff80;
// position: relative; // position: relative;
.value { .value {
@@ -420,7 +422,6 @@
display: flex; display: flex;
gap: 10px; gap: 10px;
width: fit-content; width: fit-content;
} }
.icon { .icon {
@@ -511,11 +512,9 @@
.datas__label, .datas__label,
.datas__class { .datas__class {
flex: 1; flex: 1;
} }
} }
.type-switch { .type-switch {
display: flex; display: flex;
padding: 6px 12px; padding: 6px 12px;
@@ -548,22 +547,16 @@
} }
} }
&.block-editor-panel { &.block-editor-panel {
// h4 { // h4 {
// color: #4caf50; // color: #4caf50;
// } // }
.design-section-wrapper { .design-section-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
} }
} }
.data-details { .data-details {
@@ -620,7 +613,8 @@
text-align: center; text-align: center;
} }
.type-switch {} .type-switch {
}
.fields-wrapper { .fields-wrapper {
display: flex; display: flex;
@@ -653,7 +647,6 @@
border-radius: 2px; border-radius: 2px;
&.active { &.active {
background: var(--background-color-button); background: var(--background-color-button);
} }
} }
@@ -673,11 +666,11 @@
cursor: pointer; cursor: pointer;
svg path { svg path {
stroke: #CCACFF !important; stroke: #ccacff !important;
} }
.label { .label {
color: #CCACFF; color: #ccacff;
} }
} }
} }
@@ -698,7 +691,6 @@
right: 40px; right: 40px;
.appearance { .appearance {
.design-datas-wrapper { .design-datas-wrapper {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 16px 16px; gap: 16px 16px;
@@ -708,14 +700,13 @@
width: 80%; width: 80%;
text-wrap: nowrap; text-wrap: nowrap;
} }
} }
} }
} }
.footer { .footer {
text-align: center; text-align: center;
color: #CCACFF; color: #ccacff;
} }
} }
} }
@@ -926,12 +917,6 @@
} }
} }
// DataDetailedDropdown // DataDetailedDropdown
.data-detailed-dropdown { .data-detailed-dropdown {
display: flex; display: flex;
@@ -973,7 +958,6 @@
} }
.input-wrapper { .input-wrapper {
outline: 1px solid var(--input-border-color); outline: 1px solid var(--input-border-color);
outline-offset: -1px; outline-offset: -1px;
border: none; border: none;
@@ -1011,7 +995,6 @@
gap: 8px; gap: 8px;
.search { .search {
outline: 1px solid var(--border-color); outline: 1px solid var(--border-color);
outline-offset: -1px; outline-offset: -1px;
border-radius: 12px; border-radius: 12px;
@@ -1063,7 +1046,7 @@
&.active { &.active {
span { span {
color: #CCACFF; color: #ccacff;
} }
} }
} }