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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import { DeleteIcon } from "../../icons/ContextMenuIcons";
type DataSourceSelectorProps = {
label?: string;
className?: string;
options: {
id: string;
label: string;
@@ -18,7 +19,18 @@ type DataSourceSelectorProps = {
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
const [isEyeActiveLocal, setIsEyeActiveLocal] = useState(false);
@@ -33,7 +45,7 @@ const DataSourceSelector: React.FC<DataSourceSelectorProps> = ({ label = "Data S
};
return (
<div className="datas">
<div className={`datas ${className ?? ""}`}>
<div className="datas__label">{label}</div>
<div className="datas__class">

View File

@@ -2,145 +2,123 @@ import React, { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
interface DropdownProps {
header: string;
options: string[];
onSelect: (option: string) => void;
search?: boolean;
onClick?: () => void;
onChange?: () => void;
header: string;
options: string[];
onSelect: (option: string) => void;
search?: boolean;
onClick?: () => void;
onChange?: () => void;
}
const RegularDropDown: React.FC<DropdownProps> = ({
header,
options,
onSelect,
search = true,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const [filteredOptions, setFilteredOptions] = useState<string[]>(options);
const dropdownRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState<{
top: number;
left: number;
width: number;
}>({
top: 0,
left: 0,
width: 0,
});
const RegularDropDown: React.FC<DropdownProps> = ({ header, options, onSelect, search = true }) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const [filteredOptions, setFilteredOptions] = useState<string[]>(options);
const dropdownRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState<{
top: number;
left: number;
width: number;
}>({
top: 0,
left: 0,
width: 0,
});
// Reset when closed
useEffect(() => {
if (!isOpen) {
setSelectedOption(null);
setSearchTerm("");
setFilteredOptions(options);
}
}, [isOpen, options]);
// Reset when closed
useEffect(() => {
if (!isOpen) {
setSelectedOption(null);
setSearchTerm("");
setFilteredOptions(options);
}
}, [isOpen, options]);
// Reset when header changes
useEffect(() => {
setSelectedOption(null);
setSearchTerm("");
setFilteredOptions(options);
}, [header, options]);
// Reset when header changes
useEffect(() => {
setSelectedOption(null);
setSearchTerm("");
setFilteredOptions(options);
}, [header, options]);
// Close if clicked outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
// Close if clicked outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
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);
}
};
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 handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const term = event.target.value;
setSearchTerm(term);
setFilteredOptions(options.filter((option) => option.toLowerCase().includes(term.toLowerCase())));
};
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) => {
setSelectedOption(option);
onSelect(option);
setIsOpen(false);
};
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const term = event.target.value;
setSearchTerm(term);
setFilteredOptions(
options.filter((option) =>
option.toLowerCase().includes(term.toLowerCase())
)
{/* 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>
);
};
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;

View File

@@ -15,12 +15,7 @@ interface DropdownProps {
onChange?: () => void;
}
const RegularDropDownID: React.FC<DropdownProps> = ({
header,
options,
onSelect,
search = true,
}) => {
const RegularDropDownID: React.FC<DropdownProps> = ({ header, options, onSelect, search = true }) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState("");
@@ -70,7 +65,7 @@ const RegularDropDownID: React.FC<DropdownProps> = ({
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const term = e.target.value;
setSearchTerm(term);
setFilteredOptions(options.filter(o => o.label.toLowerCase().includes(term.toLowerCase())));
setFilteredOptions(options.filter((o) => o.label.toLowerCase().includes(term.toLowerCase())));
};
return (
@@ -80,27 +75,28 @@ const RegularDropDownID: React.FC<DropdownProps> = ({
<div className="icon"></div>
</div>
{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((opt) => (
<div className="option" key={opt.id} onClick={() => handleOptionClick(opt)} title={opt.label}>
{opt.label}
{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>
))
) : (
<div className="no-options">No options found</div>
)}
</div>,
document.body
)}
)}
{filteredOptions.length > 0 ? (
filteredOptions.map((opt) => (
<div className="option" key={opt.id} onClick={() => handleOptionClick(opt)} title={opt.label}>
{opt.label}
</div>
))
) : (
<div className="no-options">No options found</div>
)}
</div>,
document.body
)}
</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;
}
.sub-header {
padding: 4px 6px 8px;
font-weight: 500;
}
.design-section {
padding: 4px;
outline: 1px solid var(--border-color);
@@ -353,6 +358,10 @@
.value-field-container {
padding: 0;
margin: 0;
.label {
width: 90px;
max-width: 90px;
}
}
.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 {
display: flex;
@@ -594,6 +591,101 @@
flex-direction: column;
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 {
display: flex;
flex-direction: column;
@@ -633,97 +725,6 @@
display: flex;
flex-direction: column;
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;
}
}