updated real time vis
This commit is contained in:
parent
4549a5cae4
commit
7950b58ba8
|
@ -11,6 +11,7 @@
|
|||
"chart.js": "^4.4.8",
|
||||
"path": "^0.12.7",
|
||||
"react": "^19.0.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.3.0",
|
||||
"zustand": "^5.0.3"
|
||||
|
@ -3247,6 +3248,16 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-chartjs-2": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
|
||||
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^4.1.1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"chart.js": "^4.4.8",
|
||||
"path": "^0.12.7",
|
||||
"react": "^19.0.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.3.0",
|
||||
"zustand": "^5.0.3"
|
||||
|
|
|
@ -6,7 +6,7 @@ const Templates = () => {
|
|||
const { templates, removeTemplate } = useTemplateStore();
|
||||
const { setSelectedZone } = useSelectedZoneStore();
|
||||
|
||||
console.log('templates: ', templates);
|
||||
console.log("templates: ", templates);
|
||||
const handleDeleteTemplate = (id: string) => {
|
||||
removeTemplate(id);
|
||||
};
|
||||
|
@ -15,40 +15,49 @@ const Templates = () => {
|
|||
setSelectedZone((prev) => ({
|
||||
...prev,
|
||||
panelOrder: template.panelOrder,
|
||||
activeSides: Array.from(new Set([...prev.activeSides, ...template.panelOrder])),
|
||||
activeSides: Array.from(
|
||||
new Set([...prev.activeSides, ...template.panelOrder])
|
||||
),
|
||||
widgets: template.widgets,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="template-list" style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
|
||||
gap: '1rem',
|
||||
padding: '1rem'
|
||||
}}>
|
||||
<div
|
||||
className="template-list"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))",
|
||||
gap: "1rem",
|
||||
padding: "1rem",
|
||||
}}
|
||||
>
|
||||
{templates.map((template) => (
|
||||
<div key={template.id} className="template-item" style={{
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '8px',
|
||||
padding: '1rem',
|
||||
transition: 'box-shadow 0.3s ease',
|
||||
|
||||
|
||||
}}>
|
||||
<div
|
||||
key={template.id}
|
||||
className="template-item"
|
||||
style={{
|
||||
border: "1px solid #e0e0e0",
|
||||
borderRadius: "8px",
|
||||
padding: "1rem",
|
||||
transition: "box-shadow 0.3s ease",
|
||||
}}
|
||||
>
|
||||
{template.snapshot && (
|
||||
<div style={{ position: 'relative', paddingBottom: '56.25%' }}> {/* 16:9 aspect ratio */}
|
||||
<div style={{ position: "relative", paddingBottom: "56.25%" }}>
|
||||
{" "}
|
||||
{/* 16:9 aspect ratio */}
|
||||
<img
|
||||
src={template.snapshot} // Corrected from template.image to template.snapshot
|
||||
alt={`${template.name} preview`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.3s ease',
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
transition: "transform 0.3s ease",
|
||||
// ':hover': {
|
||||
// transform: 'scale(1.05)'
|
||||
// }
|
||||
|
@ -57,17 +66,19 @@ const Templates = () => {
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: '0.5rem'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={() => handleLoadTemplate(template)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
fontWeight: '500',
|
||||
cursor: "pointer",
|
||||
fontWeight: "500",
|
||||
// ':hover': {
|
||||
// textDecoration: 'underline'
|
||||
// }
|
||||
|
@ -78,13 +89,13 @@ const Templates = () => {
|
|||
<button
|
||||
onClick={() => handleDeleteTemplate(template.id)}
|
||||
style={{
|
||||
padding: '0.25rem 0.5rem',
|
||||
background: '#ff4444',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
transition: 'opacity 0.3s ease',
|
||||
padding: "0.25rem 0.5rem",
|
||||
background: "#ff4444",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
transition: "opacity 0.3s ease",
|
||||
// ':hover': {
|
||||
// opacity: 0.8
|
||||
// }
|
||||
|
@ -97,12 +108,14 @@ const Templates = () => {
|
|||
</div>
|
||||
))}
|
||||
{templates.length === 0 && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
padding: '2rem',
|
||||
gridColumn: '1 / -1'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
color: "#666",
|
||||
padding: "2rem",
|
||||
gridColumn: "1 / -1",
|
||||
}}
|
||||
>
|
||||
No saved templates yet. Create one in the visualization view!
|
||||
</div>
|
||||
)}
|
||||
|
@ -111,4 +124,3 @@ const Templates = () => {
|
|||
};
|
||||
|
||||
export default Templates;
|
||||
|
||||
|
|
|
@ -2,10 +2,10 @@ import { useState } from "react";
|
|||
import ToggleHeader from "../../../../ui/inputs/ToggleHeader";
|
||||
import Widgets2D from "./Widgets2D";
|
||||
import Widgets3D from "./Widgets3D";
|
||||
import WidgetsTemplate from "./WidgetsTemplate";
|
||||
import WidgetsFloating from "./WidgetsFloating";
|
||||
|
||||
const Widgets = () => {
|
||||
const [activeOption, setActiveOption] = useState("2D");
|
||||
const [activeOption, setActiveOption] = useState("Floating");
|
||||
|
||||
const handleToggleClick = (option: string) => {
|
||||
setActiveOption(option);
|
||||
|
@ -14,13 +14,13 @@ const Widgets = () => {
|
|||
return (
|
||||
<div className="widget-left-sideBar">
|
||||
<ToggleHeader
|
||||
options={["2D", "3D", "Templates"]}
|
||||
options={["2D", "3D", "Floating"]}
|
||||
activeOption={activeOption}
|
||||
handleClick={handleToggleClick}
|
||||
/>
|
||||
{activeOption === "2D" && <Widgets2D />}
|
||||
{activeOption === "3D" && <Widgets3D />}
|
||||
{activeOption === "Templates" && <WidgetsTemplate />}
|
||||
{activeOption === "Floating" && <WidgetsFloating />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
import React, {
|
||||
useState,
|
||||
DragEvent,
|
||||
MouseEvent,
|
||||
useRef,
|
||||
useEffect,
|
||||
} from "react";
|
||||
|
||||
const WidgetsFloating: React.FC = () => {
|
||||
const stateWorking = [
|
||||
{ "Oil Tank": "24/341" },
|
||||
{ "Oil Refin": "36.023" },
|
||||
{ Transmission: "36.023" },
|
||||
{ Fuel: "36732" },
|
||||
{ Power: "1300" },
|
||||
{ Time: "13-9-2023" },
|
||||
];
|
||||
|
||||
// State for storing the dragged widget and its position
|
||||
const [draggedFloating, setDraggedFloating] = useState<string | null>(null);
|
||||
const [position, setPosition] = useState<{ x: number; y: number }>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
// State to store all placed widgets with their positions
|
||||
const [placedWidgets, setPlacedWidgets] = useState<
|
||||
{ name: string; x: number; y: number }[]
|
||||
>([]);
|
||||
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Handle the drag start event
|
||||
const handleDragStart = (
|
||||
event: DragEvent<HTMLDivElement>,
|
||||
widget: string
|
||||
) => {
|
||||
setDraggedFloating(widget);
|
||||
// Initialize position to the current position when drag starts
|
||||
setPosition({ x: event.clientX, y: event.clientY });
|
||||
};
|
||||
|
||||
// Handle the drag move event
|
||||
const handleDragMove = (event: MouseEvent) => {
|
||||
if (!draggedFloating) return;
|
||||
|
||||
// Calculate new position and update it
|
||||
const canvas = canvasRef.current;
|
||||
if (canvas) {
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
const newX = event.clientX - canvasRect.left;
|
||||
const newY = event.clientY - canvasRect.top;
|
||||
setPosition({ x: newX, y: newY });
|
||||
}
|
||||
};
|
||||
|
||||
// Handle the drag end event
|
||||
const handleDragEnd = () => {
|
||||
if (draggedFloating) {
|
||||
// Store the final position of the dragged widget
|
||||
setPlacedWidgets((prevWidgets) => [
|
||||
...prevWidgets,
|
||||
{ name: draggedFloating, x: position.x, y: position.y },
|
||||
]);
|
||||
// Reset the dragged floating widget after dragging is completed
|
||||
setDraggedFloating(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log("position: ", position);
|
||||
console.log("draggedFloating: ", draggedFloating);
|
||||
}, [draggedFloating, position]);
|
||||
|
||||
return (
|
||||
<div
|
||||
id="real-time-vis-canvas"
|
||||
ref={canvasRef}
|
||||
className="canvas"
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "400px",
|
||||
backgroundColor: "#f0f0f0",
|
||||
border: "1px solid #ccc",
|
||||
}}
|
||||
onMouseMove={handleDragMove}
|
||||
onMouseUp={handleDragEnd}
|
||||
>
|
||||
{/* The floating widget that's being dragged */}
|
||||
{draggedFloating && (
|
||||
<div
|
||||
className="floating"
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
cursor: "move",
|
||||
backgroundColor: "lightblue",
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
}}
|
||||
>
|
||||
{draggedFloating}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Render all placed widgets */}
|
||||
{placedWidgets.map((widget, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="floating"
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${widget.x}px`,
|
||||
top: `${widget.y}px`,
|
||||
backgroundColor: "lightgreen",
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
}}
|
||||
>
|
||||
{widget.name}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* The rest of your floating widgets */}
|
||||
<div
|
||||
className="floating working-state"
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, "working-state")}
|
||||
style={{ position: "absolute", top: "50px", left: "50px" }}
|
||||
>
|
||||
<div className="state-working-top">
|
||||
<div className="state-working-main">
|
||||
<div className="state">State</div>
|
||||
<div className="working-status">
|
||||
<span className="working">Working</span>
|
||||
<span className="dot"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="img">
|
||||
<img
|
||||
src="https://source.unsplash.com/random/150x100/?factory"
|
||||
alt="Factory"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="state-working-data">
|
||||
{stateWorking.map((state, index) => {
|
||||
const key = Object.keys(state)[0];
|
||||
const value = state[key];
|
||||
return (
|
||||
<div className="data-row" key={index}>
|
||||
<span className="data-key">{key}:</span>
|
||||
<span className="data-value">{value}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Other floating widgets */}
|
||||
<div
|
||||
className="floating"
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, "floating-2")}
|
||||
style={{ position: "absolute", top: "120px", left: "150px" }}
|
||||
>
|
||||
floating-2
|
||||
</div>
|
||||
<div
|
||||
className="floating"
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, "floating-3")}
|
||||
style={{ position: "absolute", top: "200px", left: "250px" }}
|
||||
>
|
||||
floating-3
|
||||
</div>
|
||||
<div
|
||||
className="floating"
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, "floating-4")}
|
||||
style={{ position: "absolute", top: "300px", left: "350px" }}
|
||||
>
|
||||
floating-4
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WidgetsFloating;
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
const WidgetsTemplate = () => {
|
||||
return (
|
||||
<div>
|
||||
WidgetsTemplate
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WidgetsTemplate
|
|
@ -65,7 +65,7 @@ const SideBarRight: React.FC = () => {
|
|||
)}
|
||||
|
||||
{/* realtime visualization */}
|
||||
{activeModule === "visualization" && <Visualization />}
|
||||
{toggleUI && activeModule === "visualization" && <Visualization />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import { RemoveIcon } from "../../../../icons/ExportCommonIcons";
|
||||
import RegularDropDown from "../../../../ui/inputs/RegularDropDown";
|
||||
import { AddIcon, RemoveIcon } from "../../../../icons/ExportCommonIcons";
|
||||
import MultiLevelDropDown from "../../../../ui/inputs/MultiLevelDropDown";
|
||||
|
||||
interface Child {
|
||||
id: number;
|
||||
easing: string;
|
||||
}
|
||||
// Define the data structure for demonstration purposes
|
||||
const DATA_STRUCTURE = {
|
||||
furnace: {
|
||||
coolingRate: "coolingRate",
|
||||
|
@ -35,13 +31,22 @@ const DATA_STRUCTURE = {
|
|||
data3: "Data 3",
|
||||
},
|
||||
timestamp: {
|
||||
data1: "Data 1",
|
||||
data1: {
|
||||
Data01: "Data 01",
|
||||
Data02: "Data 02",
|
||||
Data03: "Data 03",
|
||||
},
|
||||
data2: "Data 2",
|
||||
data3: "Data 3",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
interface Child {
|
||||
id: number;
|
||||
easing: string;
|
||||
}
|
||||
|
||||
interface Group {
|
||||
id: number;
|
||||
easing: string;
|
||||
|
@ -65,11 +70,7 @@ const Data = () => {
|
|||
{
|
||||
id: Date.now(),
|
||||
easing: "Connecter 1",
|
||||
children: [
|
||||
{ id: Date.now(), easing: "Linear" },
|
||||
{ id: Date.now() + 1, easing: "Ease Out" },
|
||||
{ id: Date.now() + 2, easing: "Linear" },
|
||||
],
|
||||
children: [{ id: Date.now(), easing: "Linear" }],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
@ -121,7 +122,43 @@ const Data = () => {
|
|||
{selectedChartId?.title && (
|
||||
<div className="sideBarHeader">{selectedChartId?.title}</div>
|
||||
)}
|
||||
{/* <MultiLevelDropDown data={DATA_STRUCTURE} /> */}
|
||||
{/* Render groups dynamically */}
|
||||
{chartDataGroups[selectedChartId?.id]?.map((group) => (
|
||||
<div key={group.id}>
|
||||
{group.children.map((child, index) => (
|
||||
<div key={child.id} className="datas">
|
||||
<div className="datas__label">Input {index + 1}</div>
|
||||
<div className="datas__class">
|
||||
<MultiLevelDropDown data={DATA_STRUCTURE} />
|
||||
{/* Add Icon */}
|
||||
{group.children.length < 7 && (
|
||||
<div
|
||||
className="icon"
|
||||
onClick={() => handleAddClick(group.id)} // Pass groupId to handleAddClick
|
||||
>
|
||||
<AddIcon />
|
||||
</div>
|
||||
)}
|
||||
{/* Remove Icon */}
|
||||
|
||||
<span
|
||||
className={`datas__separator ${
|
||||
group.children.length > 1 ? "" : "disable"
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevent event bubbling
|
||||
removeChild(group.id, child.id); // Pass groupId and childId to removeChild
|
||||
}}
|
||||
>
|
||||
<RemoveIcon />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="infoBox">
|
||||
<span className="infoIcon">i</span>
|
||||
<p>
|
||||
|
|
|
@ -19,6 +19,7 @@ import useTemplateStore from "../../store/useTemplateStore";
|
|||
import { useSelectedZoneStore } from "../../store/useZoneStore";
|
||||
|
||||
const Tools: React.FC = () => {
|
||||
const { templates } = useTemplateStore();
|
||||
const [activeTool, setActiveTool] = useState("cursor");
|
||||
const [activeSubTool, setActiveSubTool] = useState("cursor");
|
||||
const [toggleThreeD, setToggleThreeD] = useState(true);
|
||||
|
@ -196,7 +197,13 @@ const Tools: React.FC = () => {
|
|||
<div className="draw-tools">
|
||||
<div
|
||||
className={`tool-button`}
|
||||
onClick={() => handleSaveTemplate({ addTemplate, selectedZone })}
|
||||
onClick={() =>
|
||||
handleSaveTemplate({
|
||||
addTemplate,
|
||||
selectedZone,
|
||||
templates,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SaveTemplateIcon isActive={false} />
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import { useRef, useMemo } from "react";
|
||||
|
||||
import { Bar, Line } from "react-chartjs-2";
|
||||
|
||||
interface ChartComponentProps {
|
||||
type: any;
|
||||
title: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: string;
|
||||
fontWeight?: "Light" | "Regular" | "Bold";
|
||||
data: any;
|
||||
}
|
||||
|
||||
const LineGraphComponent = ({
|
||||
title,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight = "Regular",
|
||||
}: ChartComponentProps) => {
|
||||
// Memoize Font Weight Mapping
|
||||
const chartFontWeightMap = useMemo(
|
||||
() => ({
|
||||
Light: "lighter" as const,
|
||||
Regular: "normal" as const,
|
||||
Bold: "bold" as const,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
// Parse and Memoize Font Size
|
||||
const fontSizeValue = useMemo(
|
||||
() => (fontSize ? parseInt(fontSize) : 12),
|
||||
[fontSize]
|
||||
);
|
||||
|
||||
// Determine and Memoize Font Weight
|
||||
const fontWeightValue = useMemo(
|
||||
() => chartFontWeightMap[fontWeight],
|
||||
[fontWeight, chartFontWeightMap]
|
||||
);
|
||||
|
||||
// Memoize Chart Font Style
|
||||
const chartFontStyle = useMemo(
|
||||
() => ({
|
||||
family: fontFamily || "Arial",
|
||||
size: fontSizeValue,
|
||||
weight: fontWeightValue,
|
||||
}),
|
||||
[fontFamily, fontSizeValue, fontWeightValue]
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: title,
|
||||
font: chartFontStyle,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
display: false, // This hides the x-axis labels
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[title, chartFontStyle]
|
||||
);
|
||||
|
||||
const chartData = {
|
||||
labels: ["January", "February", "March", "April", "May", "June", "July"],
|
||||
datasets: [
|
||||
{
|
||||
label: "My First Dataset",
|
||||
data: [65, 59, 80, 81, 56, 55, 40],
|
||||
backgroundColor: "#6f42c1",
|
||||
borderColor: "#ffffff",
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return <Bar data={chartData} options={options} />;
|
||||
};
|
||||
|
||||
export default LineGraphComponent;
|
|
@ -0,0 +1,93 @@
|
|||
import { useRef, useMemo } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
|
||||
interface ChartComponentProps {
|
||||
type: any;
|
||||
title: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: string;
|
||||
fontWeight?: "Light" | "Regular" | "Bold";
|
||||
data: any;
|
||||
}
|
||||
|
||||
const LineGraphComponent = ({
|
||||
title,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight = "Regular",
|
||||
}: ChartComponentProps) => {
|
||||
// Memoize Font Weight Mapping
|
||||
const chartFontWeightMap = useMemo(
|
||||
() => ({
|
||||
Light: "lighter" as const,
|
||||
Regular: "normal" as const,
|
||||
Bold: "bold" as const,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
// Parse and Memoize Font Size
|
||||
const fontSizeValue = useMemo(
|
||||
() => (fontSize ? parseInt(fontSize) : 12),
|
||||
[fontSize]
|
||||
);
|
||||
|
||||
// Determine and Memoize Font Weight
|
||||
const fontWeightValue = useMemo(
|
||||
() => chartFontWeightMap[fontWeight],
|
||||
[fontWeight, chartFontWeightMap]
|
||||
);
|
||||
|
||||
// Memoize Chart Font Style
|
||||
const chartFontStyle = useMemo(
|
||||
() => ({
|
||||
family: fontFamily || "Arial",
|
||||
size: fontSizeValue,
|
||||
weight: fontWeightValue,
|
||||
}),
|
||||
[fontFamily, fontSizeValue, fontWeightValue]
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: title,
|
||||
font: chartFontStyle,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
display: false, // This hides the x-axis labels
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[title, chartFontStyle]
|
||||
);
|
||||
|
||||
const chartData = {
|
||||
labels: ["January", "February", "March", "April", "May", "June", "July"],
|
||||
datasets: [
|
||||
{
|
||||
label: "My First Dataset",
|
||||
data: [65, 59, 80, 81, 56, 55, 40],
|
||||
backgroundColor: "#6f42c1", // Updated to #6f42c1 (Purple)
|
||||
borderColor: "#ffffff", // Keeping border color white
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return <Line data={chartData} options={options} />;
|
||||
};
|
||||
|
||||
export default LineGraphComponent;
|
|
@ -0,0 +1,90 @@
|
|||
import { useRef, useMemo } from "react";
|
||||
import { Pie } from "react-chartjs-2";
|
||||
|
||||
interface ChartComponentProps {
|
||||
type: any;
|
||||
title: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: string;
|
||||
fontWeight?: "Light" | "Regular" | "Bold";
|
||||
data: any;
|
||||
}
|
||||
|
||||
const PieChartComponent = ({
|
||||
title,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight = "Regular",
|
||||
}: ChartComponentProps) => {
|
||||
// Memoize Font Weight Mapping
|
||||
const chartFontWeightMap = useMemo(
|
||||
() => ({
|
||||
Light: "lighter" as const,
|
||||
Regular: "normal" as const,
|
||||
Bold: "bold" as const,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
// Parse and Memoize Font Size
|
||||
const fontSizeValue = useMemo(
|
||||
() => (fontSize ? parseInt(fontSize) : 12),
|
||||
[fontSize]
|
||||
);
|
||||
|
||||
// Determine and Memoize Font Weight
|
||||
const fontWeightValue = useMemo(
|
||||
() => chartFontWeightMap[fontWeight],
|
||||
[fontWeight, chartFontWeightMap]
|
||||
);
|
||||
|
||||
// Memoize Chart Font Style
|
||||
const chartFontStyle = useMemo(
|
||||
() => ({
|
||||
family: fontFamily || "Arial",
|
||||
size: fontSizeValue,
|
||||
weight: fontWeightValue,
|
||||
}),
|
||||
[fontFamily, fontSizeValue, fontWeightValue]
|
||||
);
|
||||
|
||||
// Access the CSS variable for the primary accent color
|
||||
const accentColor = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--accent-color")
|
||||
.trim();
|
||||
|
||||
const options = useMemo(
|
||||
() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: title,
|
||||
font: chartFontStyle,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
[title, chartFontStyle]
|
||||
);
|
||||
|
||||
const chartData = {
|
||||
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Dataset",
|
||||
data: [12, 19, 3, 5, 2, 3],
|
||||
backgroundColor: ["#6f42c1"],
|
||||
borderColor: "#ffffff",
|
||||
borderWidth: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return <Pie data={chartData} options={options} />;
|
||||
};
|
||||
|
||||
export default PieChartComponent;
|
|
@ -11,7 +11,7 @@ type Side = "top" | "bottom" | "left" | "right";
|
|||
// Define the type for the props passed to the Buttons component
|
||||
interface ButtonsProps {
|
||||
selectedZone: {
|
||||
zoneName: string; // Add zoneName property
|
||||
zoneName: string;
|
||||
activeSides: Side[];
|
||||
panelOrder: Side[];
|
||||
lockedPanels: Side[];
|
||||
|
@ -25,7 +25,7 @@ interface ButtonsProps {
|
|||
};
|
||||
setSelectedZone: React.Dispatch<
|
||||
React.SetStateAction<{
|
||||
zoneName: string; // Ensure zoneName is included in the state type
|
||||
zoneName: string;
|
||||
activeSides: Side[];
|
||||
panelOrder: Side[];
|
||||
lockedPanels: Side[];
|
||||
|
@ -38,12 +38,18 @@ interface ButtonsProps {
|
|||
}[];
|
||||
}>
|
||||
>;
|
||||
hiddenPanels: Side[]; // Add this prop for hidden panels
|
||||
setHiddenPanels: React.Dispatch<React.SetStateAction<Side[]>>; // Add this prop for updating hidden panels
|
||||
}
|
||||
|
||||
const AddButtons: React.FC<ButtonsProps> = ({
|
||||
selectedZone,
|
||||
setSelectedZone,
|
||||
setHiddenPanels,
|
||||
hiddenPanels,
|
||||
}) => {
|
||||
// Local state to track hidden panels
|
||||
|
||||
// Function to toggle lock/unlock a panel
|
||||
const toggleLockPanel = (side: Side) => {
|
||||
const newLockedPanels = selectedZone.lockedPanels.includes(side)
|
||||
|
@ -61,18 +67,14 @@ const AddButtons: React.FC<ButtonsProps> = ({
|
|||
|
||||
// Function to toggle visibility of a panel
|
||||
const toggleVisibility = (side: Side) => {
|
||||
const newActiveSides = selectedZone.activeSides.includes(side)
|
||||
? selectedZone.activeSides.filter((s) => s !== side)
|
||||
: [...selectedZone.activeSides, side];
|
||||
|
||||
const updatedZone = {
|
||||
...selectedZone,
|
||||
activeSides: newActiveSides,
|
||||
panelOrder: newActiveSides,
|
||||
};
|
||||
|
||||
// Update the selectedZone state
|
||||
setSelectedZone(updatedZone);
|
||||
const isHidden = hiddenPanels.includes(side);
|
||||
if (isHidden) {
|
||||
// If the panel is already hidden, remove it from the hiddenPanels array
|
||||
setHiddenPanels(hiddenPanels.filter((panel) => panel !== side));
|
||||
} else {
|
||||
// If the panel is visible, add it to the hiddenPanels array
|
||||
setHiddenPanels([...hiddenPanels, side]);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to clean all widgets from a panel
|
||||
|
@ -145,8 +147,12 @@ const AddButtons: React.FC<ButtonsProps> = ({
|
|||
<div className="extra-Bs">
|
||||
{/* Hide Panel */}
|
||||
<div
|
||||
className="icon"
|
||||
title="Hide Panel"
|
||||
className={`icon ${
|
||||
hiddenPanels.includes(side) ? "active" : ""
|
||||
}`}
|
||||
title={
|
||||
hiddenPanels.includes(side) ? "Show Panel" : "Hide Panel"
|
||||
}
|
||||
onClick={() => toggleVisibility(side)}
|
||||
>
|
||||
<EyeIcon />
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
import React, { useEffect, useRef } from "react";
|
||||
import { useSelectedZoneStore } from "../../../store/useZoneStore";
|
||||
import { Widget } from "../../../store/useWidgetStore";
|
||||
|
||||
// Define the type for `Side`
|
||||
type Side = "top" | "bottom" | "left" | "right";
|
||||
|
||||
interface DisplayZoneProps {
|
||||
zonesData: {
|
||||
[key: string]: {
|
||||
activeSides: Side[];
|
||||
panelOrder: Side[];
|
||||
lockedPanels: Side[];
|
||||
widgets: Widget[];
|
||||
};
|
||||
};
|
||||
selectedZone: {
|
||||
zoneName: string;
|
||||
activeSides: Side[];
|
||||
panelOrder: Side[];
|
||||
lockedPanels: Side[];
|
||||
widgets: {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
panel: Side;
|
||||
data: any;
|
||||
}[];
|
||||
};
|
||||
setSelectedZone: React.Dispatch<
|
||||
React.SetStateAction<{
|
||||
zoneName: string;
|
||||
activeSides: Side[];
|
||||
panelOrder: Side[];
|
||||
lockedPanels: Side[];
|
||||
widgets: {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
panel: Side;
|
||||
data: any;
|
||||
}[];
|
||||
}>
|
||||
>;
|
||||
}
|
||||
|
||||
const DisplayZone: React.FC<DisplayZoneProps> = ({
|
||||
zonesData,
|
||||
selectedZone,
|
||||
setSelectedZone,
|
||||
}) => {
|
||||
// Ref for the container element
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// Example state for selectedOption and options (adjust based on your actual use case)
|
||||
const [selectedOption, setSelectedOption] = React.useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [options, setOptions] = React.useState<string[]>([]);
|
||||
|
||||
// Scroll to the selected option when it changes
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
|
||||
if (container && selectedOption) {
|
||||
// Handle scrolling to the selected option
|
||||
const index = options.findIndex((option) => {
|
||||
const formattedOption = formatOptionName(option);
|
||||
const selectedFormattedOption =
|
||||
selectedOption?.split("_")[1] || selectedOption;
|
||||
return formattedOption === selectedFormattedOption;
|
||||
});
|
||||
|
||||
if (index !== -1) {
|
||||
const optionElement = container.children[index] as HTMLElement;
|
||||
if (optionElement) {
|
||||
optionElement.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "center",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [selectedOption, options]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
event.preventDefault();
|
||||
if (container) {
|
||||
container.scrollBy({
|
||||
left: event.deltaY * 2, // Adjust the multiplier for faster scrolling
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let isDragging = false;
|
||||
let startX: number;
|
||||
let scrollLeft: number;
|
||||
|
||||
const handleMouseDown = (event: MouseEvent) => {
|
||||
isDragging = true;
|
||||
startX = event.pageX - (container?.offsetLeft || 0);
|
||||
scrollLeft = container?.scrollLeft || 0;
|
||||
};
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!isDragging || !container) return;
|
||||
event.preventDefault();
|
||||
const x = event.pageX - (container.offsetLeft || 0);
|
||||
const walk = (x - startX) * 2; // Adjust the multiplier for faster dragging
|
||||
container.scrollLeft = scrollLeft - walk;
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
isDragging = false;
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
isDragging = false;
|
||||
};
|
||||
|
||||
if (container) {
|
||||
container.addEventListener("wheel", handleWheel, { passive: false });
|
||||
container.addEventListener("mousedown", handleMouseDown);
|
||||
container.addEventListener("mousemove", handleMouseMove);
|
||||
container.addEventListener("mouseup", handleMouseUp);
|
||||
container.addEventListener("mouseleave", handleMouseLeave);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (container) {
|
||||
container.removeEventListener("wheel", handleWheel);
|
||||
container.removeEventListener("mousedown", handleMouseDown);
|
||||
container.removeEventListener("mousemove", handleMouseMove);
|
||||
container.removeEventListener("mouseup", handleMouseUp);
|
||||
container.removeEventListener("mouseleave", handleMouseLeave);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Helper function to format option names (customize as needed)
|
||||
const formatOptionName = (option: string): string => {
|
||||
// Replace underscores with spaces and capitalize the first letter
|
||||
return option.replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase());
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`zoon-wrapper ${
|
||||
selectedZone.activeSides.includes("bottom") && "bottom"
|
||||
}`}
|
||||
>
|
||||
{Object.keys(zonesData).map((zoneName, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`zone ${
|
||||
selectedZone.zoneName === zoneName ? "active" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedZone({
|
||||
zoneName,
|
||||
...zonesData[zoneName],
|
||||
});
|
||||
}}
|
||||
>
|
||||
{zoneName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisplayZone;
|
|
@ -1,217 +1,84 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import ChartComponent from "../../layout/sidebarLeft/visualization/widgets/ChartComponent";
|
||||
import { useWidgetStore } from "../../../store/useWidgetStore";
|
||||
import PieGraphComponent from "../charts/PieGraphComponent";
|
||||
import BarGraphComponent from "../charts/BarGraphComponent";
|
||||
import LineGraphComponent from "../charts/LineGraphComponent";
|
||||
|
||||
export const DraggableWidget = ({ widget }: { widget: any }) => {
|
||||
const { selectedChartId, setSelectedChartId } = useWidgetStore();
|
||||
|
||||
// State for managing the popup visibility and customization options
|
||||
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
||||
const [customizationOptions, setCustomizationOptions] = useState({
|
||||
templateBackground: "#ffffff",
|
||||
cardBackground: "#ffffff",
|
||||
cardOpacity: 1,
|
||||
cardBlur: 0,
|
||||
font: "Arial",
|
||||
margin: 0,
|
||||
radius: 5,
|
||||
shadow: "Low",
|
||||
});
|
||||
|
||||
// Handle pointer down to select the chart
|
||||
const handlePointerDown = () => {
|
||||
if (selectedChartId?.id !== widget.id) {
|
||||
setSelectedChartId(widget);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle double-click to open the popup
|
||||
const handleDoubleClick = () => {
|
||||
setIsPopupOpen(true);
|
||||
};
|
||||
|
||||
// Close the popup
|
||||
const handleClosePopup = () => {
|
||||
setIsPopupOpen(false);
|
||||
};
|
||||
|
||||
// Save the changes made in the popup
|
||||
const handleSaveChanges = () => {
|
||||
// Here you can save the customizationOptions to your store or API
|
||||
console.log("Saved Customization Options:", customizationOptions);
|
||||
setIsPopupOpen(false);
|
||||
};
|
||||
|
||||
// Compute dynamic card styles based on customizationOptions
|
||||
const cardStyle = useMemo(() => {
|
||||
const shadowLevels = {
|
||||
Low: "0px 2px 4px rgba(0, 0, 0, 0.1)",
|
||||
Medium: "0px 4px 8px rgba(0, 0, 0, 0.2)",
|
||||
High: "0px 8px 16px rgba(0, 0, 0, 0.3)",
|
||||
};
|
||||
|
||||
return {
|
||||
backgroundColor: customizationOptions.cardBackground,
|
||||
opacity: customizationOptions.cardOpacity,
|
||||
filter: `blur(${customizationOptions.cardBlur}px)`,
|
||||
fontFamily: customizationOptions.font,
|
||||
margin: `${customizationOptions.margin}px`,
|
||||
borderRadius: `${customizationOptions.radius}px`,
|
||||
// boxShadow: shadowLevels[customizationOptions.shadow],
|
||||
};
|
||||
}, [customizationOptions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
key={widget.id}
|
||||
className={`chart-container ${selectedChartId?.id === widget.id && "activeChart"}`}
|
||||
style={cardStyle} // Apply dynamic card styles here
|
||||
className={`chart-container ${
|
||||
selectedChartId?.id === widget.id && "activeChart"
|
||||
}`}
|
||||
onPointerDown={handlePointerDown}
|
||||
onDoubleClick={handleDoubleClick} // Add double-click event
|
||||
>
|
||||
{widget.type === "progress" ? (
|
||||
// <ProgressCard title={widget.title} data={widget.data} />
|
||||
<></>
|
||||
) : (
|
||||
<ChartComponent
|
||||
<>
|
||||
{widget.type === "line" && (
|
||||
<LineGraphComponent
|
||||
type={widget.type}
|
||||
title={widget.title}
|
||||
fontFamily={customizationOptions.font} // Pass font customization to ChartComponent
|
||||
fontSize={widget.fontSize}
|
||||
fontWeight={widget.fontWeight}
|
||||
data={widget.data}
|
||||
data={{
|
||||
measurements: [
|
||||
{ name: "testDevice", fields: "powerConsumption" },
|
||||
{ name: "furnace", fields: "powerConsumption" },
|
||||
],
|
||||
interval: 1000,
|
||||
duration: "1h",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Popup for Customizing Template Theme */}
|
||||
{isPopupOpen && (
|
||||
<div className="popup-overlay">
|
||||
<div className="popup-content">
|
||||
<h2>Customize Template Theme</h2>
|
||||
<div className="form-group">
|
||||
<label>Template Background</label>
|
||||
<input
|
||||
type="color"
|
||||
value={customizationOptions.templateBackground}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
templateBackground: e.target.value,
|
||||
}))
|
||||
}
|
||||
{widget.type === "bar" && (
|
||||
<BarGraphComponent
|
||||
type={widget.type}
|
||||
title={widget.title}
|
||||
fontSize={widget.fontSize}
|
||||
fontWeight={widget.fontWeight}
|
||||
data={{
|
||||
measurements: [
|
||||
{ name: "testDevice", fields: "powerConsumption" },
|
||||
{ name: "furnace", fields: "powerConsumption" },
|
||||
],
|
||||
interval: 1000,
|
||||
duration: "1h",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Card Background</label>
|
||||
<input
|
||||
type="color"
|
||||
value={customizationOptions.cardBackground}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
cardBackground: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Card Opacity</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={customizationOptions.cardOpacity}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
cardOpacity: parseFloat(e.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Card Blur</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="10"
|
||||
value={customizationOptions.cardBlur}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
cardBlur: parseInt(e.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Font</label>
|
||||
<select
|
||||
value={customizationOptions.font}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
font: e.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Times New Roman">Times New Roman</option>
|
||||
<option value="Courier New">Courier New</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Margin</label>
|
||||
<input
|
||||
type="number"
|
||||
value={customizationOptions.margin}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
margin: parseInt(e.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Radius</label>
|
||||
<input
|
||||
type="number"
|
||||
value={customizationOptions.radius}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
radius: parseInt(e.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Shadow</label>
|
||||
<select
|
||||
value={customizationOptions.shadow}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
shadow: e.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="Low">Low</option>
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="High">High</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="popup-actions">
|
||||
<button onClick={handleClosePopup}>Cancel</button>
|
||||
<button onClick={handleSaveChanges}>Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{widget.type === "pie" && (
|
||||
<PieGraphComponent
|
||||
type={widget.type}
|
||||
title={widget.title}
|
||||
fontSize={widget.fontSize}
|
||||
fontWeight={widget.fontWeight}
|
||||
data={{
|
||||
measurements: [
|
||||
{ name: "testDevice", fields: "powerConsumption" },
|
||||
{ name: "furnace", fields: "powerConsumption" },
|
||||
],
|
||||
interval: 1000,
|
||||
duration: "1h",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -84,46 +84,53 @@ const Panel: React.FC<PanelProps> = ({ selectedZone, setSelectedZone }) => {
|
|||
e.preventDefault();
|
||||
const { draggedAsset } = useWidgetStore.getState();
|
||||
|
||||
if (draggedAsset) {
|
||||
if (selectedZone.lockedPanels.includes(panel)) return;
|
||||
if (!draggedAsset) return;
|
||||
if (isPanelLocked(panel)) return;
|
||||
|
||||
const currentWidgetsInPanel = selectedZone.widgets.filter(
|
||||
(w) => w.panel === panel
|
||||
).length;
|
||||
const currentWidgetsCount = getCurrentWidgetCount(panel);
|
||||
const maxCapacity = calculatePanelCapacity(panel);
|
||||
|
||||
const dimensions = panelDimensions[panel];
|
||||
const CHART_WIDTH = 200;
|
||||
const CHART_HEIGHT = 200;
|
||||
let maxCharts = 0;
|
||||
if (currentWidgetsCount >= maxCapacity) return;
|
||||
|
||||
if (dimensions) {
|
||||
if (panel === "top" || panel === "bottom") {
|
||||
maxCharts = Math.floor(dimensions.width / CHART_WIDTH);
|
||||
} else {
|
||||
maxCharts = Math.floor(dimensions.height / CHART_HEIGHT);
|
||||
}
|
||||
} else {
|
||||
maxCharts = panel === "top" || panel === "bottom" ? 5 : 3;
|
||||
}
|
||||
|
||||
if (currentWidgetsInPanel >= maxCharts) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedZone = {
|
||||
...selectedZone,
|
||||
widgets: [
|
||||
...selectedZone.widgets,
|
||||
{
|
||||
...draggedAsset,
|
||||
id: generateUniqueId(),
|
||||
panel,
|
||||
},
|
||||
],
|
||||
addWidgetToPanel(draggedAsset, panel);
|
||||
};
|
||||
|
||||
setSelectedZone(updatedZone);
|
||||
// Helper functions
|
||||
const isPanelLocked = (panel: Side) =>
|
||||
selectedZone.lockedPanels.includes(panel);
|
||||
|
||||
const getCurrentWidgetCount = (panel: Side) =>
|
||||
selectedZone.widgets.filter(w => w.panel === panel).length;
|
||||
|
||||
const calculatePanelCapacity = (panel: Side) => {
|
||||
const CHART_WIDTH = 200;
|
||||
const CHART_HEIGHT = 200;
|
||||
const FALLBACK_HORIZONTAL_CAPACITY = 5;
|
||||
const FALLBACK_VERTICAL_CAPACITY = 3;
|
||||
|
||||
const dimensions = panelDimensions[panel];
|
||||
if (!dimensions) {
|
||||
return panel === "top" || panel === "bottom"
|
||||
? FALLBACK_HORIZONTAL_CAPACITY
|
||||
: FALLBACK_VERTICAL_CAPACITY;
|
||||
}
|
||||
|
||||
return panel === "top" || panel === "bottom"
|
||||
? Math.floor(dimensions.width / CHART_WIDTH)
|
||||
: Math.floor(dimensions.height / CHART_HEIGHT);
|
||||
};
|
||||
|
||||
const addWidgetToPanel = (asset: any, panel: Side) => {
|
||||
const newWidget = {
|
||||
...asset,
|
||||
id: generateUniqueId(),
|
||||
panel,
|
||||
};
|
||||
|
||||
setSelectedZone(prev => ({
|
||||
...prev,
|
||||
widgets: [...prev.widgets, newWidget]
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -172,7 +179,7 @@ const Panel: React.FC<PanelProps> = ({ selectedZone, setSelectedZone }) => {
|
|||
}}
|
||||
>
|
||||
<div
|
||||
className="panel-content"
|
||||
className={`panel-content ${isPlaying && "fullScreen"}`}
|
||||
style={{
|
||||
pointerEvents: selectedZone.lockedPanels.includes(side)
|
||||
? "none"
|
||||
|
@ -180,6 +187,7 @@ const Panel: React.FC<PanelProps> = ({ selectedZone, setSelectedZone }) => {
|
|||
opacity: selectedZone.lockedPanels.includes(side) ? "0.8" : "1",
|
||||
}}
|
||||
>
|
||||
<>{}</>
|
||||
{selectedZone.widgets
|
||||
.filter((w) => w.panel === side)
|
||||
.map((widget) => (
|
||||
|
@ -193,5 +201,3 @@ const Panel: React.FC<PanelProps> = ({ selectedZone, setSelectedZone }) => {
|
|||
};
|
||||
|
||||
export default Panel;
|
||||
// only load selected template
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
|
|||
import Panel from "./Panel";
|
||||
import AddButtons from "./AddButtons";
|
||||
import { useSelectedZoneStore } from "../../../store/useZoneStore";
|
||||
import DisplayZone from "./DisplayZone";
|
||||
|
||||
type Side = "top" | "bottom" | "left" | "right";
|
||||
|
||||
|
@ -15,6 +16,7 @@ interface Widget {
|
|||
}
|
||||
|
||||
const RealTimeVisulization: React.FC = () => {
|
||||
const [hiddenPanels, setHiddenPanels] = React.useState<Side[]>([]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [zonesData, setZonesData] = useState<{
|
||||
[key: string]: {
|
||||
|
@ -74,34 +76,19 @@ const RealTimeVisulization: React.FC = () => {
|
|||
style={{
|
||||
height: isPlaying ? "100vh" : "",
|
||||
width: isPlaying ? "100%" : "",
|
||||
left: isPlaying ? "50%" : "",
|
||||
left: isPlaying ? "0%" : "",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`zoon-wrapper ${
|
||||
selectedZone.activeSides.includes("bottom") && "bottom"
|
||||
}`}
|
||||
>
|
||||
{Object.keys(zonesData).map((zoneName, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`zone ${
|
||||
selectedZone.zoneName === zoneName ? "active" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedZone({
|
||||
zoneName,
|
||||
...zonesData[zoneName],
|
||||
});
|
||||
}}
|
||||
>
|
||||
{zoneName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<DisplayZone
|
||||
zonesData={zonesData}
|
||||
selectedZone={selectedZone}
|
||||
setSelectedZone={setSelectedZone}
|
||||
/>
|
||||
|
||||
{!isPlaying && (
|
||||
<AddButtons
|
||||
hiddenPanels={hiddenPanels}
|
||||
setHiddenPanels={setHiddenPanels}
|
||||
selectedZone={selectedZone}
|
||||
setSelectedZone={setSelectedZone}
|
||||
/>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
|
||||
// Dropdown Item Component
|
||||
const DropdownItem = ({
|
||||
|
@ -27,9 +26,11 @@ const DropdownItem = ({
|
|||
const NestedDropdown = ({
|
||||
label,
|
||||
children,
|
||||
onSelect,
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
onSelect: (selectedLabel: string) => void;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
|
@ -37,36 +38,47 @@ const NestedDropdown = ({
|
|||
<div className="nested-dropdown">
|
||||
{/* Dropdown Trigger */}
|
||||
<div
|
||||
className="dropdown-trigger"
|
||||
className={`dropdown-trigger ${open ? "open" : ""}`}
|
||||
onClick={() => setOpen(!open)} // Toggle submenu on click
|
||||
>
|
||||
{label} <span className="icon">{open ? "▼" : "▶"}</span>
|
||||
</div>
|
||||
|
||||
{/* Submenu */}
|
||||
{open && <div className="submenu">{children}</div>}
|
||||
{open && (
|
||||
<div className="submenu">
|
||||
{React.Children.map(children, (child) =>
|
||||
React.cloneElement(child as React.ReactElement, { onSelect })
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Recursive Function to Render Nested Data
|
||||
const renderNestedData = (data: Record<string, any>) => {
|
||||
const renderNestedData = (
|
||||
data: Record<string, any>,
|
||||
onSelect: (selectedLabel: string) => void
|
||||
) => {
|
||||
return Object.entries(data).map(([key, value]) => {
|
||||
if (typeof value === "object" && !Array.isArray(value)) {
|
||||
// If the value is an object, render it as a nested dropdown
|
||||
return (
|
||||
<NestedDropdown key={key} label={key}>
|
||||
{renderNestedData(value)}
|
||||
<NestedDropdown key={key} label={key} onSelect={onSelect}>
|
||||
{renderNestedData(value, onSelect)}
|
||||
</NestedDropdown>
|
||||
);
|
||||
} else if (Array.isArray(value)) {
|
||||
// If the value is an array, render each item as a dropdown item
|
||||
return value.map((item, index) => (
|
||||
<DropdownItem key={index} label={item} />
|
||||
<DropdownItem key={index} label={item} onClick={() => onSelect(item)} />
|
||||
));
|
||||
} else {
|
||||
// If the value is a simple string, render it as a dropdown item
|
||||
return <DropdownItem key={key} label={value} />;
|
||||
return (
|
||||
<DropdownItem key={key} label={value} onClick={() => onSelect(value)} />
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -74,19 +86,50 @@ const renderNestedData = (data: Record<string, any>) => {
|
|||
// Main Multi-Level Dropdown Component
|
||||
const MultiLevelDropdown = ({ data }: { data: Record<string, any> }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedLabel, setSelectedLabel] = useState("Dropdown trigger");
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Handle outer click to close the dropdown
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Handle selection of an item
|
||||
const handleSelect = (selectedLabel: string) => {
|
||||
setSelectedLabel(selectedLabel); // Update the dropdown trigger text
|
||||
setOpen(false); // Close the dropdown
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="multi-level-dropdown">
|
||||
<div className="multi-level-dropdown" ref={dropdownRef}>
|
||||
{/* Dropdown Trigger Button */}
|
||||
<button
|
||||
className="dropdown-button"
|
||||
className={`dropdown-button ${open ? "open" : ""}`}
|
||||
onClick={() => setOpen(!open)} // Toggle main menu on click
|
||||
>
|
||||
Dropdown trigger <span className="icon">▼</span>
|
||||
{selectedLabel} <span className="icon">▾</span>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{open && <div className="dropdown-menu">{renderNestedData(data)}</div>}
|
||||
{open && (
|
||||
<div className="dropdown-menu">
|
||||
<div className="dropdown-content">
|
||||
{renderNestedData(data, handleSelect)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,30 +7,68 @@ type HandleSaveTemplateProps = {
|
|||
panelOrder: string[]; // Adjust the type based on actual data structure
|
||||
widgets: any[]; // Replace `any` with the actual widget type
|
||||
};
|
||||
templates?: Template[];
|
||||
};
|
||||
|
||||
// Generate a unique ID (placeholder function)
|
||||
const generateUniqueId = (): string => {
|
||||
return Math.random().toString(36).substring(2, 15);
|
||||
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||
};
|
||||
|
||||
// Refactored function
|
||||
export const handleSaveTemplate = async ({
|
||||
addTemplate,
|
||||
selectedZone,
|
||||
templates = [],
|
||||
}: HandleSaveTemplateProps): Promise<void> => {
|
||||
try {
|
||||
// Check if the selected zone has any widgets
|
||||
if (!selectedZone.widgets || selectedZone.widgets.length === 0) {
|
||||
console.warn("Cannot save an empty template.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the template already exists
|
||||
const isDuplicate = templates.some((template) => {
|
||||
const isSamePanelOrder =
|
||||
JSON.stringify(template.panelOrder) ===
|
||||
JSON.stringify(selectedZone.panelOrder);
|
||||
const isSameWidgets =
|
||||
JSON.stringify(template.widgets) ===
|
||||
JSON.stringify(selectedZone.widgets);
|
||||
return isSamePanelOrder && isSameWidgets;
|
||||
});
|
||||
|
||||
if (isDuplicate) {
|
||||
console.warn("This template already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture visualization snapshot
|
||||
const snapshot = await captureVisualization();
|
||||
const template: Template = {
|
||||
if (!snapshot) {
|
||||
console.error("Failed to capture visualization snapshot.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new template
|
||||
const newTemplate: Template = {
|
||||
id: generateUniqueId(),
|
||||
name: `Template ${Date.now()}`,
|
||||
panelOrder: selectedZone.panelOrder,
|
||||
widgets: selectedZone.widgets,
|
||||
snapshot,
|
||||
};
|
||||
console.log('template: ', template);
|
||||
addTemplate(template);
|
||||
|
||||
console.log("Saving template:", newTemplate);
|
||||
|
||||
// Save the template
|
||||
try {
|
||||
addTemplate(newTemplate);
|
||||
} catch (error) {
|
||||
console.error('Failed to save template:', error);
|
||||
console.error("Failed to add template:", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save template:", error);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -35,7 +35,7 @@ $highlight-accent-color-dark: #403e6a; // Highlighted accent for dark mode
|
|||
$background-color: #fcfdfd; // Main background color
|
||||
$background-color-dark: #19191d; // Main background color for dark mode
|
||||
$background-color-secondary: #e1e0ff80; // Secondary background color
|
||||
$background-color-secondary-dark: #1f1f2399; // Secondary background color for dark mode
|
||||
$background-color-secondary-dark: #39394f99; // Secondary background color for dark mode
|
||||
|
||||
// Border colors
|
||||
$border-color: #e0dfff; // Default border color
|
||||
|
|
|
@ -36,9 +36,11 @@
|
|||
overflow-y: auto; // Optional: Enable scrolling if content exceeds height
|
||||
left: 0;
|
||||
top: 104%;
|
||||
|
||||
.option {
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
flex-direction: row !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary-color); // Optional: Hover effect
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
@use "../../../abstracts/variables" as *;
|
||||
@use "../../../abstracts/mixins" as *;
|
||||
|
||||
.floatingWidgets-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-top: 12px;
|
||||
padding: 6px;
|
||||
|
||||
.floating {
|
||||
|
||||
min-height: 170px;
|
||||
background: var(--background-color);
|
||||
border: 1.23px solid var(--border-color);
|
||||
box-shadow: 0px 4.91px 4.91px 0px #0000001c;
|
||||
border-radius: $border-radius-medium;
|
||||
padding: 12px 6px;
|
||||
}
|
||||
|
||||
.working-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.state-working-top {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.floatingWidgets-wrapper {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.floating.working-state {
|
||||
width: 100%;
|
||||
height: 283px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
|
||||
.state-working-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
// flex-direction: column;
|
||||
}
|
||||
|
||||
.state {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.working-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.working {
|
||||
font-size: 20px;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #4CAF50;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.img img {
|
||||
width: 150px;
|
||||
height: 100px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.state-working-data {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.data-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.data-key {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.data-value {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
|
@ -78,14 +78,14 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-right: 6px;
|
||||
padding: 6px;
|
||||
flex-wrap: nowrap;
|
||||
overflow: auto;
|
||||
|
||||
.chart {
|
||||
min-height: 170px;
|
||||
background: var(--background-primary, #fcfdfd);
|
||||
border: 1.23px solid var(--Grays-Gray-5, #e5e5ea);
|
||||
background: var(--background-color);
|
||||
border: 1.23px solid var(--border-color);
|
||||
box-shadow: 0px 4.91px 4.91px 0px #0000001c;
|
||||
border-radius: $border-radius-medium;
|
||||
padding: 12px 6px;
|
||||
|
@ -107,7 +107,7 @@
|
|||
|
||||
.stock {
|
||||
padding: 13px 5px;
|
||||
background-color: #e0dfff80;
|
||||
background-color: var(--background-color-secondary);
|
||||
border-radius: 6.33px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -291,8 +291,46 @@
|
|||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
|
||||
.datas {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.datas__class {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.multi-level-dropdown {
|
||||
|
||||
min-width: 100px;
|
||||
|
||||
.dropdown-button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.datas__class {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.datas__separator {}
|
||||
|
||||
.disable {
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
/* Disables all mouse interactions */
|
||||
opacity: 0.5;
|
||||
/* Optional: Makes the button look visually disabled */
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.sideBarHeader {
|
||||
color: #5c87df;
|
||||
color: var(--accent-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
@ -388,7 +426,7 @@
|
|||
width: 100%;
|
||||
height: 150px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 8px;
|
||||
// border-radius: 8px;
|
||||
}
|
||||
|
||||
.optionsContainer {
|
||||
|
@ -523,93 +561,119 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.multi-level-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
|
||||
.dropdown-button {
|
||||
background-color: #3b82f6; /* Blue background */
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
width: 100%;
|
||||
background-color: var(--background-color) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
padding: 5px 10px;
|
||||
|
||||
|
||||
// font-size: 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #2563eb; /* Darker blue on hover */
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 0.5rem;
|
||||
&.open {
|
||||
background-color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.5rem); /* Add spacing below the button */
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 12rem;
|
||||
background-color: white;
|
||||
border: 1px solid #e5e7eb; /* Light gray border */
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow */
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
min-width: 200px;
|
||||
overflow: auto;
|
||||
max-height: 600px;
|
||||
|
||||
/* Dropdown Item */
|
||||
.dropdown-item {
|
||||
.dropdown-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.nested-dropdown {
|
||||
// &:first-child{
|
||||
margin-left: 0;
|
||||
// }
|
||||
}
|
||||
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: block;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: #4b5563; /* Gray text */
|
||||
padding: 5px 10px;
|
||||
text-decoration: none;
|
||||
color: #000000;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f3f4f6; /* Light gray background on hover */
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Nested Dropdown */
|
||||
.nested-dropdown {
|
||||
position: relative;
|
||||
.nested-dropdown {
|
||||
margin-left: 20px;
|
||||
|
||||
.dropdown-trigger {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: #4b5563; /* Gray text */
|
||||
justify-content: space-between;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 14px;
|
||||
color: #000000;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f3f4f6; /* Light gray background on hover */
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
&.open {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.submenu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 100%; /* Position submenu to the right */
|
||||
width: 12rem;
|
||||
background-color: white;
|
||||
border: 1px solid #e5e7eb; /* Light gray border */
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow */
|
||||
z-index: 20;
|
||||
margin-top: 5px;
|
||||
padding-left: 20px;
|
||||
border-left: 2px solid #cccccc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@
|
|||
@use 'components/templates';
|
||||
@use 'components/tools';
|
||||
@use 'components/regularDropDown';
|
||||
@use 'components/visualization/floating/energyConsumed';
|
||||
|
||||
// layout
|
||||
@use 'layout/sidebar';
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
.realTime-viz {
|
||||
background-color: var(--background-color);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0px 4px 8px rgba(60, 60, 67, 0.1019607843);
|
||||
box-shadow: $box-shadow-medium;
|
||||
width: calc(100% - (320px + 270px + 80px));
|
||||
height: 600px;
|
||||
position: absolute;
|
||||
|
@ -30,7 +30,7 @@
|
|||
|
||||
.zoon-wrapper {
|
||||
display: flex;
|
||||
background-color: #e0dfff80;
|
||||
background-color: var(--background-color);
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 50%;
|
||||
|
@ -40,14 +40,14 @@
|
|||
border-radius: 8px;
|
||||
max-width: 80%;
|
||||
overflow: auto;
|
||||
|
||||
max-width: calc(100% - 450px);
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.zone {
|
||||
width: auto;
|
||||
background-color: #fcfdfd;
|
||||
background-color: var(--background-color);
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
white-space: nowrap;
|
||||
|
@ -83,43 +83,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 90%; // Take even more width on very small screens
|
||||
height: 400px; // Further reduce height
|
||||
top: 45%; // Adjust vertical position slightly upward
|
||||
|
||||
.panel {
|
||||
&.top-panel,
|
||||
&.bottom-panel {
|
||||
.panel-content {
|
||||
flex-direction: column; // Stack panels vertically on small screens
|
||||
|
||||
.chart-container {
|
||||
width: 100%; // Make charts full width
|
||||
height: 150px; // Reduce chart height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
width: 95%; // Take almost full width on very small devices
|
||||
height: 350px; // Further reduce height
|
||||
top: 40%; // Move slightly higher for better visibility
|
||||
|
||||
.side-button-container {
|
||||
flex-direction: row !important; // Force buttons into a row
|
||||
gap: 4px; // Reduce spacing between buttons
|
||||
|
||||
&.top,
|
||||
&.bottom {
|
||||
left: 50%; // Center horizontally
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
|
@ -189,6 +152,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
background-color: var(--background-color);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
|
@ -216,18 +180,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
&.top-panel,
|
||||
&.bottom-panel {
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
.fullScreen {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
width: 230px;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -247,7 +216,7 @@
|
|||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -275,6 +244,15 @@
|
|||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
|
Loading…
Reference in New Issue