feat: implement simulation dashboard element data management with new input components and styles

This commit is contained in:
2025-12-22 12:20:36 +05:30
parent f9fe57c72c
commit bd79b36e8e
9 changed files with 988 additions and 980 deletions

View File

@@ -107,23 +107,17 @@ const AnalyzerManager: React.FC = () => {
const assetAnalysis = getAssetAnalysis(assetId); const assetAnalysis = getAssetAnalysis(assetId);
if (assetAnalysis) { if (assetAnalysis) {
const timeLabel = new Date().toLocaleTimeString(); const newGraphData = dataKeys.map((key) => {
const newPoint: GraphDataPoint = { name: timeLabel, value: 0 };
let hasValidData = false;
dataKeys.forEach((key) => {
const val = resolvePath(assetAnalysis, key); const val = resolvePath(assetAnalysis, key);
if (typeof val === "number") { return {
newPoint["value"] = val; // Make the key readable or just use it as name
hasValidData = true; name: key.split(".").pop() || key,
} value: typeof val === "number" ? val : 0,
};
}); });
if (hasValidData) { // Deep check to avoid unnecessary updates
const currentGraphData = element.graphData || []; if (JSON.stringify(newGraphData) !== JSON.stringify(element.graphData)) {
const newGraphData = [...currentGraphData, newPoint].slice(-10);
// Always update for single-machine as we are appending time-series data
updateGraphData(block.blockUuid, element.elementUuid, newGraphData); updateGraphData(block.blockUuid, element.elementUuid, newGraphData);
} }
} }

View File

@@ -85,7 +85,7 @@ const ElementData: React.FC<ElementDataProps> = ({
const totalAssetOptions = assetDropdownItems.length; const totalAssetOptions = assetDropdownItems.length;
return ( return (
<div className="data-details"> <div className="fields-wrapper data-details">
{element?.type === "label-value" && ( {element?.type === "label-value" && (
<> <>
<div className="design-section"> <div className="design-section">
@@ -99,7 +99,7 @@ const ElementData: React.FC<ElementDataProps> = ({
updateElementData(selectedBlock, selectedElement, { label: value }); updateElementData(selectedBlock, selectedElement, { label: value });
}} }}
/> />
<div className="data"> <div className="datas">
<DataDetailedDropdown <DataDetailedDropdown
title="Data Source" title="Data Source"
placeholder="Select assets" placeholder="Select assets"
@@ -134,9 +134,10 @@ const ElementData: React.FC<ElementDataProps> = ({
/> />
</div> </div>
<div className="data"> <div className="datas">
<DataDetailedDropdown <DataDetailedDropdown
title="Data Value" title="Data Value"
className="fill-width"
placeholder="Select Value" placeholder="Select Value"
sections={getLableValueDropdownItems(element.dataBinding?.dataSource as string | undefined)} sections={getLableValueDropdownItems(element.dataBinding?.dataSource as string | undefined)}
value={ value={
@@ -163,8 +164,8 @@ const ElementData: React.FC<ElementDataProps> = ({
{/* Data Mapping */} {/* Data Mapping */}
{element?.type === "graph" && ( {element?.type === "graph" && (
<div className="data-mapping"> <div className="data-mapping design-section">
<div className="heading">Data Mapping</div> <div className="sub-header">Data Mapping</div>
<div className="type-switch"> <div className="type-switch">
<div className={`type ${selectDataMapping === "singleMachine" ? "active" : ""}`} onClick={() => handleDataTypeSwitch("singleMachine")}> <div className={`type ${selectDataMapping === "singleMachine" ? "active" : ""}`} onClick={() => handleDataTypeSwitch("singleMachine")}>
@@ -243,6 +244,7 @@ const ElementData: React.FC<ElementDataProps> = ({
{multipleValueFields.map((field) => ( {multipleValueFields.map((field) => (
<DataSourceSelector <DataSourceSelector
key={field.id} key={field.id}
className="fill-width"
label={field.label} label={field.label}
options={field.options} options={field.options}
selected={field.options.find((o) => o.id === element.dataBinding?.commonValue)?.label ?? ""} selected={field.options.find((o) => o.id === element.dataBinding?.commonValue)?.label ?? ""}
@@ -258,6 +260,7 @@ const ElementData: React.FC<ElementDataProps> = ({
{multipleSourceFields.map((field, index) => ( {multipleSourceFields.map((field, index) => (
<DataSourceSelector <DataSourceSelector
key={field.id} key={field.id}
className="dual-buttons"
label={field.label} label={field.label}
options={getFilteredOptions(field.options, element.dataBinding?.dataSource, index)} options={getFilteredOptions(field.options, element.dataBinding?.dataSource, index)}
selected={getEventByModelUuid(selectedProduct.productUuid, (element.dataBinding?.dataSource as string[])?.[index] || "")?.modelName ?? ""} selected={getEventByModelUuid(selectedProduct.productUuid, (element.dataBinding?.dataSource as string[])?.[index] || "")?.modelName ?? ""}

View File

@@ -450,7 +450,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
return dataValues.map((value, index) => ({ return dataValues.map((value, index) => ({
id: `data-value-${index}`, id: `data-value-${index}`,
label: `Data Value ${index + 1}`, label: `Value ${index + 1}`,
showEyeDropper: false, showEyeDropper: false,
options: valueOptions, options: valueOptions,
})); }));
@@ -459,7 +459,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
return [ return [
{ {
id: "data-value", id: "data-value",
label: "Data Value", label: "Value",
showEyeDropper: false, showEyeDropper: false,
options: [], options: [],
}, },
@@ -472,7 +472,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
return dataSources.map((_, index) => ({ return dataSources.map((_, index) => ({
id: `data-source-${index}`, id: `data-source-${index}`,
label: `Data Source ${index + 1}`, label: `Source ${index + 1}`,
showEyeDropper: true, showEyeDropper: true,
options: getAssetDropdownItems().map((item) => ({ options: getAssetDropdownItems().map((item) => ({
id: item.id, id: item.id,
@@ -485,7 +485,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
}, [element, getAssetDropdownItems]); }, [element, getAssetDropdownItems]);
const multipleValueFields = useMemo( const multipleValueFields = useMemo(
() => [{ id: "data-value", label: "Data Value", showEyeDropper: false, options: getCommonValueDropdownItems().map((item) => ({ id: item.id, label: item.label })) }], () => [{ id: "data-value", label: "Value", showEyeDropper: false, options: getCommonValueDropdownItems().map((item) => ({ id: item.id, label: item.label })) }],
[getCommonValueDropdownItems] [getCommonValueDropdownItems]
); );
@@ -524,7 +524,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
} }
}} }}
className="panel element-editor-panel" className="panel element-editor-panel"
style={{ position: "fixed", left: position.x, top: position.y, zIndex: 1000, width: panelWidth }} style={{ position: "fixed", left: position.x, top: position.y, zIndex: 1000, width: panelWidth }}
> >
<div className="free-move-button" onPointerDown={startDrag} onDoubleClick={resetPosition}> <div className="free-move-button" onPointerDown={startDrag} onDoubleClick={resetPosition}>
<ResizeHeightIcon /> <ResizeHeightIcon />

View File

@@ -9,6 +9,7 @@ export type DropdownItem = {
type DataDetailedDropdownProps = { type DataDetailedDropdownProps = {
title: string; title: string;
className?: string;
placeholder?: string; placeholder?: string;
sections: { sections: {
title?: string; title?: string;
@@ -22,7 +23,18 @@ type DataDetailedDropdownProps = {
onEyeDrop?: () => void; onEyeDrop?: () => void;
}; };
const DataDetailedDropdown: React.FC<DataDetailedDropdownProps> = ({ title, placeholder = "Select value", sections, value, onChange, dropDownHeader, eyedroper, eyeDropperActive, onEyeDrop }) => { const DataDetailedDropdown: React.FC<DataDetailedDropdownProps> = ({
title,
className,
placeholder = "Select value",
sections,
value,
onChange,
dropDownHeader,
eyedroper,
eyeDropperActive,
onEyeDrop,
}) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [isEyeDroperActiveLocal, setIsEyeDroperActiveLocal] = useState(false); const [isEyeDroperActiveLocal, setIsEyeDroperActiveLocal] = useState(false);
@@ -62,7 +74,7 @@ const DataDetailedDropdown: React.FC<DataDetailedDropdownProps> = ({ title, plac
}, [search, sections]); }, [search, sections]);
return ( return (
<div className="data-detailed-dropdown" ref={containerRef}> <div className={`data-detailed-dropdown ${className ?? ""}`} ref={containerRef}>
<div className="title">{title}</div> <div className="title">{title}</div>
<div className="input-container"> <div className="input-container">
<div className="input-wrapper"> <div className="input-wrapper">

View File

@@ -5,6 +5,7 @@ import { DeleteIcon } from "../../icons/ContextMenuIcons";
type DataSourceSelectorProps = { type DataSourceSelectorProps = {
label?: string; label?: string;
className?: string;
options: { options: {
id: string; id: string;
label: string; label: string;
@@ -18,7 +19,18 @@ type DataSourceSelectorProps = {
onEyeDrop?: () => void; onEyeDrop?: () => void;
}; };
const DataSourceSelector: React.FC<DataSourceSelectorProps> = ({ label = "Data Source", options, selected, onSelect, showEyeDropper = true, showDeleteBtn, onDelete, eyeDropperActive, onEyeDrop }) => { const DataSourceSelector: React.FC<DataSourceSelectorProps> = ({
label = "Data Source",
className,
options,
selected,
onSelect,
showEyeDropper = true,
showDeleteBtn,
onDelete,
eyeDropperActive,
onEyeDrop,
}) => {
// Local state fallback if no external control provided // Local state fallback if no external control provided
const [isEyeActiveLocal, setIsEyeActiveLocal] = useState(false); const [isEyeActiveLocal, setIsEyeActiveLocal] = useState(false);
@@ -33,7 +45,7 @@ const DataSourceSelector: React.FC<DataSourceSelectorProps> = ({ label = "Data S
}; };
return ( return (
<div className="datas"> <div className={`datas ${className ?? ""}`}>
<div className="datas__label">{label}</div> <div className="datas__label">{label}</div>
<div className="datas__class"> <div className="datas__class">

View File

@@ -2,145 +2,123 @@ import React, { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
interface DropdownProps { interface DropdownProps {
header: string; header: string;
options: string[]; options: string[];
onSelect: (option: string) => void; onSelect: (option: string) => void;
search?: boolean; search?: boolean;
onClick?: () => void; onClick?: () => void;
onChange?: () => void; onChange?: () => void;
} }
const RegularDropDown: React.FC<DropdownProps> = ({ const RegularDropDown: React.FC<DropdownProps> = ({ header, options, onSelect, search = true }) => {
header, const [isOpen, setIsOpen] = useState(false);
options, const [selectedOption, setSelectedOption] = useState<string | null>(null);
onSelect, const [searchTerm, setSearchTerm] = useState("");
search = true, const [filteredOptions, setFilteredOptions] = useState<string[]>(options);
}) => { const dropdownRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false); const [position, setPosition] = useState<{
const [selectedOption, setSelectedOption] = useState<string | null>(null); top: number;
const [searchTerm, setSearchTerm] = useState(""); left: number;
const [filteredOptions, setFilteredOptions] = useState<string[]>(options); width: number;
const dropdownRef = useRef<HTMLDivElement>(null); }>({
const [position, setPosition] = useState<{ top: 0,
top: number; left: 0,
left: number; width: 0,
width: number; });
}>({
top: 0,
left: 0,
width: 0,
});
// Reset when closed // Reset when closed
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
setSelectedOption(null); setSelectedOption(null);
setSearchTerm(""); setSearchTerm("");
setFilteredOptions(options); setFilteredOptions(options);
} }
}, [isOpen, options]); }, [isOpen, options]);
// Reset when header changes // Reset when header changes
useEffect(() => { useEffect(() => {
setSelectedOption(null); setSelectedOption(null);
setSearchTerm(""); setSearchTerm("");
setFilteredOptions(options); setFilteredOptions(options);
}, [header, options]); }, [header, options]);
// Close if clicked outside // Close if clicked outside
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
if ( if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
dropdownRef.current && setIsOpen(false);
!dropdownRef.current.contains(event.target as Node) }
) { };
document.addEventListener("click", handleClickOutside);
return () => document.removeEventListener("click", handleClickOutside);
}, []);
// Recalculate position when opening
useEffect(() => {
if (isOpen && dropdownRef.current) {
const rect = dropdownRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX,
width: rect.width,
});
}
}, [isOpen]);
const toggleDropdown = () => setIsOpen((prev) => !prev);
const handleOptionClick = (option: string) => {
setSelectedOption(option);
onSelect(option);
setIsOpen(false); setIsOpen(false);
}
}; };
document.addEventListener("click", handleClickOutside);
return () => document.removeEventListener("click", handleClickOutside);
}, []);
// Recalculate position when opening const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
useEffect(() => { const term = event.target.value;
if (isOpen && dropdownRef.current) { setSearchTerm(term);
const rect = dropdownRef.current.getBoundingClientRect(); setFilteredOptions(options.filter((option) => option.toLowerCase().includes(term.toLowerCase())));
setPosition({ };
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX,
width: rect.width,
});
}
}, [isOpen]);
const toggleDropdown = () => setIsOpen((prev) => !prev); return (
<div className="regularDropdown-container" ref={dropdownRef}>
{/* Header */}
<div className="dropdown-header flex-sb" onClick={toggleDropdown}>
<div className="key">{selectedOption || header}</div>
<div className="icon"></div>
</div>
const handleOptionClick = (option: string) => { {/* Options rendered in portal */}
setSelectedOption(option); {isOpen &&
onSelect(option); createPortal(
setIsOpen(false); <div
}; className="dropdown-options"
style={{
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => { position: "absolute",
const term = event.target.value; top: position.top,
setSearchTerm(term); left: position.left,
setFilteredOptions( width: position.width,
options.filter((option) => zIndex: 9999,
option.toLowerCase().includes(term.toLowerCase()) }}
) >
{search && (
<div className="dropdown-search">
<input type="text" placeholder="Search..." value={searchTerm} onChange={handleSearchChange} />
</div>
)}
{filteredOptions.length > 0 ? (
filteredOptions.map((option, index) => (
<div className="option" key={index} onClick={() => handleOptionClick(option)} title={option}>
{option}
</div>
))
) : (
<div className="no-options">No options found</div>
)}
</div>,
document.body
)}
</div>
); );
};
return (
<div className="regularDropdown-container" ref={dropdownRef}>
{/* Header */}
<div className="dropdown-header flex-sb" onClick={toggleDropdown}>
<div className="key">{selectedOption || header}</div>
<div className="icon"></div>
</div>
{/* Options rendered in portal */}
{isOpen &&
createPortal(
<div
className="dropdown-options"
style={{
position: "absolute",
top: position.top,
left: position.left,
width: position.width,
zIndex: 9999,
}}
>
{search && (
<div className="dropdown-search">
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={handleSearchChange}
/>
</div>
)}
{filteredOptions.length > 0 ? (
filteredOptions.map((option, index) => (
<div
className="option"
key={index}
onClick={() => handleOptionClick(option)}
title={option}
>
{option}
</div>
))
) : (
<div className="no-options">No options found</div>
)}
</div>,
document.body
)}
</div>
);
}; };
export default RegularDropDown; export default RegularDropDown;

View File

@@ -15,12 +15,7 @@ interface DropdownProps {
onChange?: () => void; onChange?: () => void;
} }
const RegularDropDownID: React.FC<DropdownProps> = ({ const RegularDropDownID: React.FC<DropdownProps> = ({ header, options, onSelect, search = true }) => {
header,
options,
onSelect,
search = true,
}) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState<string | null>(null); const [selectedOption, setSelectedOption] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
@@ -70,7 +65,7 @@ const RegularDropDownID: React.FC<DropdownProps> = ({
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const term = e.target.value; const term = e.target.value;
setSearchTerm(term); setSearchTerm(term);
setFilteredOptions(options.filter(o => o.label.toLowerCase().includes(term.toLowerCase()))); setFilteredOptions(options.filter((o) => o.label.toLowerCase().includes(term.toLowerCase())));
}; };
return ( return (
@@ -80,27 +75,28 @@ const RegularDropDownID: React.FC<DropdownProps> = ({
<div className="icon"></div> <div className="icon"></div>
</div> </div>
{isOpen && createPortal( {isOpen &&
<div className="dropdown-options" style={{ position: "absolute", top: position.top, left: position.left, width: position.width, zIndex: 9999 }}> createPortal(
{search && ( <div className="dropdown-options" style={{ position: "absolute", top: position.top, left: position.left, width: position.width, zIndex: 9999 }}>
<div className="dropdown-search"> {search && (
<input type="text" placeholder="Search..." value={searchTerm} onChange={handleSearchChange} /> <div className="dropdown-search">
</div> <input type="text" placeholder="Search..." value={searchTerm} onChange={handleSearchChange} />
)}
{filteredOptions.length > 0 ? (
filteredOptions.map((opt) => (
<div className="option" key={opt.id} onClick={() => handleOptionClick(opt)} title={opt.label}>
{opt.label}
</div> </div>
)) )}
) : ( {filteredOptions.length > 0 ? (
<div className="no-options">No options found</div> filteredOptions.map((opt) => (
)} <div className="option" key={opt.id} onClick={() => handleOptionClick(opt)} title={opt.label}>
</div>, {opt.label}
document.body </div>
)} ))
) : (
<div className="no-options">No options found</div>
)}
</div>,
document.body
)}
</div> </div>
); );
} };
export default RegularDropDownID export default RegularDropDownID;

File diff suppressed because it is too large Load Diff

View File

@@ -337,6 +337,11 @@
display: flex; display: flex;
} }
.sub-header {
padding: 4px 6px 8px;
font-weight: 500;
}
.design-section { .design-section {
padding: 4px; padding: 4px;
outline: 1px solid var(--border-color); outline: 1px solid var(--border-color);
@@ -353,6 +358,10 @@
.value-field-container { .value-field-container {
padding: 0; padding: 0;
margin: 0; margin: 0;
.label {
width: 90px;
max-width: 90px;
}
} }
.select-type { .select-type {
@@ -534,18 +543,6 @@
} }
} }
.datas {
// width: 100% ;
display: flex;
justify-content: space-between;
align-items: center;
.datas__label,
.datas__class {
flex: 1;
}
}
.type-switch { .type-switch {
display: flex; display: flex;
@@ -594,6 +591,101 @@
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
.datas {
width: 100%;
display: flex;
align-items: center;
// padding: 6px 12px;
&__label,
&__class,
.input-container,
.title {
flex: 1;
}
&__label,
.title {
flex: 0.8;
max-width: 90px;
min-width: 90px;
width: 90px;
line-height: 23px;
}
&__class,
.input-container {
display: flex;
align-items: center;
gap: 4px;
.input-wrapper {
max-width: 126px;
width: 126px;
.icon {
padding: 0;
}
}
.add-icon,
.delete {
display: flex;
height: 24px;
width: 24px;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
&:hover {
background: var(--background-color-input);
}
&.active {
background: var(--background-color-button);
}
}
.delete {
cursor: pointer;
&:hover {
background: var(--log-error-background-color);
path {
stroke: var(--log-error-text-color);
}
}
}
.regularDropdown-container {
max-width: 112px;
width: 112px;
.icon {
padding: 0;
}
}
}
}
.fields-wrapper {
display: flex;
flex-direction: column;
gap: 6px;
.add-field {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
svg path {
stroke: #ccacff !important;
}
.label {
color: #ccacff;
}
}
}
.data-wrapper { .data-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -633,97 +725,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 9px; gap: 9px;
background: var(--background-color);
border: 1px solid var(--border-color);
box-shadow: var(--box-shadow-medium);
padding: 15px 12px;
border-radius: 25px;
.heading {
padding: 4px 6px 8px;
font-weight: 500;
}
.fields-wrapper {
display: flex;
flex-direction: column;
gap: 6px;
.datas {
width: 100%;
display: flex;
align-items: center;
// padding: 6px 12px;
.datas__label,
.datas__class {
flex: 1;
}
.datas__label {
flex: 0.8;
min-width: 96px;
}
.datas__class {
display: flex;
align-items: center;
gap: 4px;
.add-icon,
.delete {
display: flex;
height: 24px;
width: 24px;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
&:hover {
background: var(--background-color-input);
}
&.active {
background: var(--background-color-button);
}
}
.delete {
cursor: pointer;
&:hover {
background: var(--log-error-background-color);
path {
stroke: var(--log-error-text-color);
}
}
}
.regularDropdown-container {
max-width: 106px;
width: 106px;
.icon {
padding: 0;
}
}
}
}
.add-field {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
svg path {
stroke: #ccacff !important;
}
.label {
color: #ccacff;
}
}
}
} }
} }
@@ -1106,3 +1107,18 @@
} }
} }
} }
.fill-width {
.input-wrapper,
.regularDropdown-container {
max-width: none !important;
width: 100% !important;
}
}
.dual-buttons {
.regularDropdown-container {
max-width: 70px !important;
width: 100% !important;
}
}