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]
); );

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

@@ -10,12 +10,7 @@ interface DropdownProps {
onChange?: () => void; onChange?: () => void;
} }
const RegularDropDown: React.FC<DropdownProps> = ({ const RegularDropDown: 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("");
@@ -50,10 +45,7 @@ const RegularDropDown: React.FC<DropdownProps> = ({
// 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 &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false); setIsOpen(false);
} }
}; };
@@ -84,11 +76,7 @@ const RegularDropDown: React.FC<DropdownProps> = ({
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const term = event.target.value; const term = event.target.value;
setSearchTerm(term); setSearchTerm(term);
setFilteredOptions( setFilteredOptions(options.filter((option) => option.toLowerCase().includes(term.toLowerCase())));
options.filter((option) =>
option.toLowerCase().includes(term.toLowerCase())
)
);
}; };
return ( return (
@@ -114,22 +102,12 @@ const RegularDropDown: React.FC<DropdownProps> = ({
> >
{search && ( {search && (
<div className="dropdown-search"> <div className="dropdown-search">
<input <input type="text" placeholder="Search..." value={searchTerm} onChange={handleSearchChange} />
type="text"
placeholder="Search..."
value={searchTerm}
onChange={handleSearchChange}
/>
</div> </div>
)} )}
{filteredOptions.length > 0 ? ( {filteredOptions.length > 0 ? (
filteredOptions.map((option, index) => ( filteredOptions.map((option, index) => (
<div <div className="option" key={index} onClick={() => handleOptionClick(option)} title={option}>
className="option"
key={index}
onClick={() => handleOptionClick(option)}
title={option}
>
{option} {option}
</div> </div>
)) ))

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,7 +75,8 @@ const RegularDropDownID: React.FC<DropdownProps> = ({
<div className="icon"></div> <div className="icon"></div>
</div> </div>
{isOpen && createPortal( {isOpen &&
createPortal(
<div className="dropdown-options" style={{ position: "absolute", top: position.top, left: position.left, width: position.width, zIndex: 9999 }}> <div className="dropdown-options" style={{ position: "absolute", top: position.top, left: position.left, width: position.width, zIndex: 9999 }}>
{search && ( {search && (
<div className="dropdown-search"> <div className="dropdown-search">
@@ -101,6 +97,6 @@ const RegularDropDownID: React.FC<DropdownProps> = ({
)} )}
</div> </div>
); );
} };
export default RegularDropDownID export default RegularDropDownID;

View File

@@ -46,7 +46,6 @@ textarea {
} }
input[type="number"] { input[type="number"] {
// Chrome, Safari, Edge, Opera // Chrome, Safari, Edge, Opera
&::-webkit-outer-spin-button, &::-webkit-outer-spin-button,
&::-webkit-inner-spin-button { &::-webkit-inner-spin-button {
@@ -252,6 +251,7 @@ input[type="number"] {
.dropdown-options { .dropdown-options {
position: absolute; position: absolute;
width: 100%; width: 100%;
min-width: 150px;
background: var(--background-color); background: var(--background-color);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: #{$border-radius-large}; border-radius: #{$border-radius-large};
@@ -794,7 +794,6 @@ input[type="number"] {
} }
} }
.data-picker { .data-picker {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -820,9 +819,7 @@ input[type="number"] {
.colorValue { .colorValue {
width: 100%; width: 100%;
background: linear-gradient(90.85deg, background: linear-gradient(90.85deg, rgba(240, 228, 255, 0.3) 3.6%, rgba(211, 174, 253, 0.3) 96.04%);
rgba(240, 228, 255, 0.3) 3.6%,
rgba(211, 174, 253, 0.3) 96.04%);
text-align: center; text-align: center;
padding: 4px 0; padding: 4px 0;
border-radius: 100px; border-radius: 100px;

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;
}
}