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 */}
<div className="block-grid-container">
<div className={`${editMode ? "editable" : ""} block-grid-container`}>
<BlockGrid
blocks={blocks}
handleAddElement={async (blockId, type, graphType) => {

View File

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

View File

@@ -9,6 +9,7 @@ import { AddIcon, DeviceIcon, ParametersIcon, ResizeHeightIcon } from "../../../
import DataDetailedDropdown from "../../../ui/inputs/DataDetailedDropdown";
import { useSceneContext } from "../../../../modules/scene/sceneContext";
import ElementDesign from "./ElementDesign";
import { useVisualizationStore } from "../../../../store/visualization/useVisualizationStore";
interface ElementEditorProps {
elementEditorRef: RefObject<HTMLDivElement>;
@@ -64,27 +65,30 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
const [selectType, setSelectType] = useState("design");
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 [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);
// 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
useEffect(() => {
const panelEl = panelRef.current || (elementEditorRef && (elementEditorRef as any).current);
const width = panelEl?.offsetWidth || 300;
const nx = Math.max(0, window.innerWidth - width - 40);
const ny = 80;
initialPositionRef.current = { x: nx, y: ny };
setPosition({ x: nx, y: ny });
if (!editorPosition) {
const panelEl = panelRef.current || (elementEditorRef && (elementEditorRef as any).current);
const width = panelEl?.offsetWidth || 300;
const nx = Math.max(0, window.innerWidth - width - 40);
const ny = 80;
initialPositionRef.current = { x: nx, y: ny };
setPosition({ x: nx, y: ny });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const draggingRef = useRef(false);
const startXRef = useRef(0);
const startYRef = useRef(0);
@@ -157,7 +161,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
window.removeEventListener("pointerup", onPointerUp);
try {
(panel as Element).releasePointerCapture?.((e as any).pointerId);
} catch (err) { }
} catch (err) {}
};
window.addEventListener("pointermove", onPointerMove);

View File

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

View File

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

View File

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