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:
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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] || [];
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user