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 */}
|
||||
|
||||
<div className="block-grid-container">
|
||||
<div className={`${editMode ? "editable" : ""} block-grid-container`}>
|
||||
<BlockGrid
|
||||
blocks={blocks}
|
||||
handleAddElement={async (blockId, type, graphType) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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] || [];
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user