diff --git a/client/sensorhub-client-nocode/README.md b/client/sensorhub-client-nocode/README.md new file mode 100644 index 0000000..af165c4 --- /dev/null +++ b/client/sensorhub-client-nocode/README.md @@ -0,0 +1,55 @@ +# DataFeed Driver + +**Driver Dependencies:** +Ensure the `sensorhub-ui-datafeed` module is included. This module provides custom configuration forms for the datafeed-driver. + +Update `AdminUI` in the `config.json` by adding the following under the `customForms` array: +```json +"customForms": [ +{ +"objClass": "org.sensorhub.ui.CustomUIConfig", +"configClass": "com.botts.impl.sensor.datafeed.DataFeedConfig", +"uiClass": "com.botts.ui.DataFeedConfigForm" +}, +{ +"objClass": "org.sensorhub.ui.CustomUIConfig", +"configClass": "com.botts.api.parser.DataParserConfig", +"uiClass": "com.botts.ui.DataParserConfigForm" +}, +{ +"objClass": "org.sensorhub.ui.CustomUIConfig", +"configClass": "com.botts.api.parser.data.DataField", +"uiClass": "com.botts.ui.ProtobufParserConfigForm" +}, +{ +"objClass": "org.sensorhub.ui.CustomUIConfig", +"configClass": "com.botts.impl.parser.protobuf.ProtobufDataParserConfig", +"uiClass": "com.botts.ui.ProtobufParserConfigForm" +} +], +``` + +## Configuration + +### Communication Configuration +There are two types of communication providers currently implemented for the data feed driver: Stream-Based and Message Queue based communication. + +**Stream-Based Communication** +Used for continuous data streams +- Example: + - TCP + - UDP + +**Message Queue Communication:** +Used for event-driven messaging +- Example: + - MQTT + - Kafka + + +### Data Parser Configuration +Choose a parser to interpret the incoming data format. The following parsers are currently supported: +- CSV +- JSON +- XML +- Protobuf \ No newline at end of file diff --git a/client/sensorhub-client-nocode/build.gradle b/client/sensorhub-client-nocode/build.gradle new file mode 100644 index 0000000..72bea52 --- /dev/null +++ b/client/sensorhub-client-nocode/build.gradle @@ -0,0 +1,37 @@ +description = 'No Code Client' +ext.details = "A client module for building datastream mappings and encoding data to different outbound formats" +version = '1.0.0' + +dependencies { + implementation 'org.sensorhub:sensorhub-core:' + oshCoreVersion + + implementation project(':sensorhub-parser-utils') + implementation project(':sensorhub-ui-nocode') + + testImplementation('junit:junit:4.13.1') +} + +test { + useJUnit() +} + +osgi { + manifest { + attributes ('Bundle-Vendor': 'GeoRobotix Innovative Research, Inc.') + attributes ('Bundle-Activator': 'com.georobotix.impl.client.nocode.Activator') + } +} + +ext.pom >>= { + developers { + developer { + id 'kalynstricklin' + name 'Kalyn Stricklin' + organization 'GeoRobotix Innovative Research' + organizationUrl 'https://georobotix.us' + } + } +} +repositories { + mavenCentral() +} diff --git a/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/Activator.java b/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/Activator.java new file mode 100644 index 0000000..35f6916 --- /dev/null +++ b/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/Activator.java @@ -0,0 +1,22 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ +package com.georobotix.impl.client.nocode; + +import org.sensorhub.utils.OshBundleActivator; + +/** + * The presence of this class tells the OpenSensorHub OSGI machinery about this module. + * It is referenced in the 'Bundle-Activator' attribute in the OSGI section of the build.gradle file. + */ +@SuppressWarnings("unused") +public class Activator extends OshBundleActivator { +} diff --git a/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/Descriptor.java b/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/Descriptor.java new file mode 100644 index 0000000..80b015d --- /dev/null +++ b/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/Descriptor.java @@ -0,0 +1,34 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ +package com.georobotix.impl.client.nocode; + +import org.sensorhub.api.module.IModule; +import org.sensorhub.api.module.IModuleProvider; +import org.sensorhub.api.module.ModuleConfig; +import org.sensorhub.impl.module.JarModuleProvider; + +public class Descriptor extends JarModuleProvider implements IModuleProvider { + + @Override + public Class> getModuleClass() + { + return NoCodeClientModule.class; + } + + + @Override + public Class getModuleConfigClass() + { + return NoCodeClientConfig.class; + } + +} \ No newline at end of file diff --git a/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/NoCodeClient.java b/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/NoCodeClient.java new file mode 100644 index 0000000..c2a0f1f --- /dev/null +++ b/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/NoCodeClient.java @@ -0,0 +1,17 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ +package com.georobotix.impl.client.nocode; + + +public class NoCodeClient { + +} \ No newline at end of file diff --git a/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/NoCodeClientConfig.java b/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/NoCodeClientConfig.java new file mode 100644 index 0000000..400ae08 --- /dev/null +++ b/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/NoCodeClientConfig.java @@ -0,0 +1,26 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ +package com.georobotix.impl.client.nocode; + +import com.georobotix.ui.nocode.NoCodeEditor; +import org.sensorhub.api.client.ClientConfig; +import org.sensorhub.api.comm.CommProviderConfig; +import org.sensorhub.api.config.DisplayInfo; + + +public class NoCodeClientConfig extends ClientConfig { + @DisplayInfo(desc = "Comm settings used to interface") + public CommProviderConfig commSettings; + + @DisplayInfo(label = "Mapping Configuration", desc = "Build mappings from one datastream to any outbound format") + public NoCodeEditor noCodeEditor = new NoCodeEditor(); +} diff --git a/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/NoCodeClientModule.java b/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/NoCodeClientModule.java new file mode 100644 index 0000000..1882668 --- /dev/null +++ b/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/NoCodeClientModule.java @@ -0,0 +1,24 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ +package com.georobotix.impl.client.nocode; + + +import org.sensorhub.api.client.IClientModule; +import org.sensorhub.impl.module.AbstractModule; + +public class NoCodeClientModule extends AbstractModule implements IClientModule { + + @Override + public boolean isConnected() { + return false; + } +} \ No newline at end of file diff --git a/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/OutboundFormatsConfig.java b/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/OutboundFormatsConfig.java new file mode 100644 index 0000000..ad6ad37 --- /dev/null +++ b/client/sensorhub-client-nocode/src/main/java/com/georobotix/impl/client/nocode/OutboundFormatsConfig.java @@ -0,0 +1,15 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ +package com.georobotix.impl.client.nocode; + +public class OutboundFormatsConfig { +} diff --git a/client/sensorhub-client-nocode/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider b/client/sensorhub-client-nocode/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider new file mode 100644 index 0000000..ce16711 --- /dev/null +++ b/client/sensorhub-client-nocode/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider @@ -0,0 +1 @@ +com.georobotix.impl.client.nocode.Descriptor diff --git a/ui/sensorhub-ui-nocode/README.md b/ui/sensorhub-ui-nocode/README.md new file mode 100644 index 0000000..206b58c --- /dev/null +++ b/ui/sensorhub-ui-nocode/README.md @@ -0,0 +1,15 @@ +# UI Forms for the No Code Client + + +## Configuration + +Add this codeblock in the config.json, replacing the `customForms` block inside the `AdminUI` config. +```json +"customForms": [ +{ +"objClass": "org.sensorhub.ui.CustomUIConfig", +"configClass": "com.georobotix.ui.nocode.NoCodeEditor", +"uiClass": "com.georobotix.ui.nocode.NoCodeEditorForm" +} +], +``` diff --git a/ui/sensorhub-ui-nocode/build.gradle b/ui/sensorhub-ui-nocode/build.gradle new file mode 100644 index 0000000..6f05a2a --- /dev/null +++ b/ui/sensorhub-ui-nocode/build.gradle @@ -0,0 +1,40 @@ +description = 'No Code UI' +ext.details = "UI forms for the no-code client" +version = '1.0.0' + +dependencies { + implementation 'org.sensorhub:sensorhub-core:' + oshCoreVersion + implementation 'org.sensorhub:sensorhub-webui-core:' + oshCoreVersion + embeddedImpl 'com.vaadin:vaadin-server:8.14.3' + embeddedImpl 'com.vaadin:vaadin-compatibility-server:8.14.3' + embeddedImpl 'com.vaadin:vaadin-themes:8.14.3' + + implementation project(':sensorhub-parser-utils') + + testImplementation('junit:junit:4.13.1') +} + +test { + useJUnit() +} + +osgi { + manifest { + attributes ('Bundle-Vendor': 'GeoRobotix Innovative Research, Inc.') + attributes ('Bundle-Activator': 'com.georobotix.ui.nocode.Activator') + } +} + +ext.pom >>= { + developers { + developer { + id 'kalynstricklin' + name 'Kalyn Stricklin' + organization 'GeoRobotix Innovative Research' + organizationUrl 'https://georobotix.us' + } + } +} +repositories { + mavenCentral() +} diff --git a/ui/sensorhub-ui-nocode/src/main/java/com/georobotix/ui/nocode/Activator.java b/ui/sensorhub-ui-nocode/src/main/java/com/georobotix/ui/nocode/Activator.java new file mode 100644 index 0000000..806d1f6 --- /dev/null +++ b/ui/sensorhub-ui-nocode/src/main/java/com/georobotix/ui/nocode/Activator.java @@ -0,0 +1,18 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ +package com.georobotix.ui.nocode; + +import org.osgi.framework.BundleActivator; +import org.sensorhub.utils.OshBundleActivator; + +public class Activator extends OshBundleActivator implements BundleActivator { +} diff --git a/ui/sensorhub-ui-nocode/src/main/java/com/georobotix/ui/nocode/MappingEntry.java b/ui/sensorhub-ui-nocode/src/main/java/com/georobotix/ui/nocode/MappingEntry.java new file mode 100644 index 0000000..574755c --- /dev/null +++ b/ui/sensorhub-ui-nocode/src/main/java/com/georobotix/ui/nocode/MappingEntry.java @@ -0,0 +1,29 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ +package com.georobotix.ui.nocode; + +import org.sensorhub.api.config.DisplayInfo; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class MappingEntry { + + @DisplayInfo(label = "Datastream Name", desc = "The source datastream to map from") + public String datastreamName = ""; + + @DisplayInfo(label = "Serializer", desc = "The output serializer (JSON, CSV, XML, Protobuf, CoT)") + public String serializer = "JSON"; + + @DisplayInfo(label = "Field Mappings", desc = "Map output field names to source datastream fields") + public Map fieldMappings = new LinkedHashMap<>(); +} diff --git a/ui/sensorhub-ui-nocode/src/main/java/com/georobotix/ui/nocode/NoCodeEditor.java b/ui/sensorhub-ui-nocode/src/main/java/com/georobotix/ui/nocode/NoCodeEditor.java new file mode 100644 index 0000000..04a2979 --- /dev/null +++ b/ui/sensorhub-ui-nocode/src/main/java/com/georobotix/ui/nocode/NoCodeEditor.java @@ -0,0 +1,22 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ +package com.georobotix.ui.nocode; + +import org.sensorhub.api.config.DisplayInfo; + +import java.util.ArrayList; +import java.util.List; + +public class NoCodeEditor { + @DisplayInfo(label = "Datastream Mappings", desc = "List of datastream-to-outbound-format mappings") + public List mappings = new ArrayList<>(); +} diff --git a/ui/sensorhub-ui-nocode/src/main/java/com/georobotix/ui/nocode/NoCodeEditorForm.java b/ui/sensorhub-ui-nocode/src/main/java/com/georobotix/ui/nocode/NoCodeEditorForm.java new file mode 100644 index 0000000..63e8d1d --- /dev/null +++ b/ui/sensorhub-ui-nocode/src/main/java/com/georobotix/ui/nocode/NoCodeEditorForm.java @@ -0,0 +1,282 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ +package com.georobotix.ui.nocode; + +import com.vaadin.shared.ui.MarginInfo; +import com.vaadin.ui.*; +import net.opengis.swe.v20.DataComponent; +import org.sensorhub.api.datastore.obs.IDataStreamStore; +import org.sensorhub.ui.GenericConfigForm; +import org.sensorhub.ui.data.MyBeanItem; + +import java.util.*; + +public class NoCodeEditorForm extends GenericConfigForm { + private final Map dataStreamMap = new LinkedHashMap<>(); + private final List mappingEntries = new ArrayList<>(); + //import org.vast.swe.fast.JsonDataWriterGson; + //import org.vast.swe.fast.TextDataWriter; + //import org.vast.swe.fast.XmlDataWriter; + //TODO: dont hardcode these, use submodules similar to parsers when avail + private static final List serializerFormats = Arrays.asList("JSON", "CSV", "XML", "Protobuf", "CoT"); + + @Override + protected boolean isFieldVisible(String propId) { + if (propId.endsWith("mappings")) + return false; + return super.isFieldVisible(propId); + } + + @Override + public void build(String title, String popupText, MyBeanItem beanItem, boolean includeSubForms) { + super.build(title, popupText, beanItem, includeSubForms); + + if (beanItem.getBean() instanceof NoCodeEditor) { + NoCodeEditor editor = (NoCodeEditor) beanItem.getBean(); + mappingEntries.clear(); + mappingEntries.addAll(editor.mappings); + + loadDataStreams(); + buildMappingLayoutUI(editor); + } + } + + private void loadDataStreams() { + dataStreamMap.clear(); + getParentHub().getDatabaseRegistry().getObsSystemDatabases().forEach(database -> { + IDataStreamStore dsStore = database.getDataStreamStore(); + dsStore.forEach((key, dsInfo) -> { + String name = dsInfo.getName(); + if (name != null && !name.isEmpty()) { + dataStreamMap.put(name, dsInfo.getRecordStructure()); + } + }); + }); + } + + private void buildMappingLayoutUI(NoCodeEditor editor) { + VerticalLayout container = new VerticalLayout(); + container.setSpacing(true); + container.setMargin(new MarginInfo(true, false)); + container.setWidth("100%"); + + Label header = new Label("Mapping Configuration"); + header.addStyleName("h3"); + container.addComponent(header); + + VerticalLayout mappingsLayout = new VerticalLayout(); + mappingsLayout.setSpacing(true); + mappingsLayout.setWidth("100%"); + + Button addBtn = new Button("Add Mapping"); + addBtn.addClickListener(e -> { + MappingEntry newEntry = new MappingEntry(); + editor.mappings.add(newEntry); + mappingEntries.add(newEntry); + mappingsLayout.addComponent(buildMappingRow(newEntry, mappingsLayout, editor)); + }); + container.addComponent(addBtn); + + for (MappingEntry entry : mappingEntries) { + mappingsLayout.addComponent(buildMappingRow(entry, mappingsLayout, editor)); + } + + container.addComponent(mappingsLayout); + + addComponent(container); + } + + + private Panel buildMappingRow(MappingEntry entry, VerticalLayout mappingsLayout, NoCodeEditor editor) { + Panel panel = new Panel(); + panel.setWidth("100%"); + + VerticalLayout panelContent = new VerticalLayout(); + panelContent.setSpacing(true); + panelContent.setMargin(true); + panelContent.setWidth("100%"); + + HorizontalLayout topRow = new HorizontalLayout(); + topRow.setSpacing(true); + topRow.setWidth("100%"); + + ComboBox datastreamCombo = new ComboBox<>("Datastream"); + datastreamCombo.setWidth("100%"); + datastreamCombo.setEmptySelectionAllowed(false); + datastreamCombo.setItems(dataStreamMap.keySet()); + datastreamCombo.setPlaceholder("Select a datastream..."); + if (!entry.datastreamName.isEmpty()) { + datastreamCombo.setValue(entry.datastreamName); + } + + ComboBox serializerCombo = new ComboBox<>("Serializer"); + serializerCombo.setWidth("200px"); + serializerCombo.setEmptySelectionAllowed(false); + serializerCombo.setItems(serializerFormats); + serializerCombo.setValue(entry.serializer); + serializerCombo.addValueChangeListener(e -> { + if (e.getValue() != null) { + entry.serializer = e.getValue(); + } + }); + + // if the serializer has templates (CoT ? / protobuf) then we should let the user select it + // and it will auto generate the fields to map to + Button removeBtn = new Button("Remove"); + removeBtn.addStyleName("danger"); + removeBtn.addClickListener(e -> { + editor.mappings.remove(entry); + mappingEntries.remove(entry); + mappingsLayout.removeComponent(panel); + }); + + topRow.addComponents(datastreamCombo, serializerCombo, removeBtn); + topRow.setExpandRatio(datastreamCombo, 1f); + topRow.setComponentAlignment(removeBtn, Alignment.BOTTOM_RIGHT); + panelContent.addComponent(topRow); + + VerticalLayout fieldRows = new VerticalLayout(); + fieldRows.setSpacing(true); + fieldRows.setWidth("100%"); + fieldRows.setMargin(false); + + + List sourceFields = new ArrayList<>(); + List> sourceCombos = new ArrayList<>(); + + if (!entry.datastreamName.isEmpty() && dataStreamMap.containsKey(entry.datastreamName)) { + DataComponent recordStructure = dataStreamMap.get(entry.datastreamName); + if (recordStructure != null) { + collectFieldPaths(recordStructure, "", sourceFields); + } + } + + HorizontalLayout headerRow = new HorizontalLayout(); + headerRow.setSpacing(true); + headerRow.setWidth("100%"); + Label outputHeader = new Label("Output Field"); + outputHeader.setWidth("100%"); + outputHeader.addStyleName("bold"); + Label spacer = new Label(""); + spacer.setWidthUndefined(); + Label sourceHeader = new Label("Source Field"); + sourceHeader.setWidth("100%"); + sourceHeader.addStyleName("bold"); + headerRow.addComponents(outputHeader, spacer, sourceHeader); + headerRow.setExpandRatio(outputHeader, 1f); + headerRow.setExpandRatio(sourceHeader, 1f); + fieldRows.addComponent(headerRow); + + for (Map.Entry fieldEntry : entry.fieldMappings.entrySet()) { + fieldRows.addComponent(buildFieldRow(fieldEntry.getKey(), fieldEntry.getValue(), entry, fieldRows, sourceFields, sourceCombos)); + } + + Button addFieldBtn = new Button("Add Field"); + addFieldBtn.addClickListener(e -> { + String outputName = "field" + (entry.fieldMappings.size() + 1); + entry.fieldMappings.put(outputName, ""); + int idx = fieldRows.getComponentIndex(addFieldBtn); + fieldRows.addComponent(buildFieldRow(outputName, "", entry, fieldRows, sourceFields, sourceCombos), idx); + }); + fieldRows.addComponent(addFieldBtn); + + datastreamCombo.addValueChangeListener(e -> { + String dsName = e.getValue(); + if (dsName == null) return; + entry.datastreamName = dsName; + + sourceFields.clear(); + DataComponent recordStructure = dataStreamMap.get(dsName); + if (recordStructure != null) { + collectFieldPaths(recordStructure, "", sourceFields); + } + + for (ComboBox combo : sourceCombos) { + combo.clear(); + combo.setItems(sourceFields); + } + entry.fieldMappings.replaceAll((key, value) -> ""); + }); + + panelContent.addComponent(fieldRows); + panel.setContent(panelContent); + + return panel; + } + + private HorizontalLayout buildFieldRow(String outputName, String sourceName, MappingEntry entry, VerticalLayout fieldRows, List sourceFields, List> sourceCombos) { + HorizontalLayout row = new HorizontalLayout(); + row.setSpacing(true); + row.setWidth("100%"); + + TextField outputField = new TextField(); + outputField.setWidth("100%"); + outputField.setValue(outputName); + outputField.setPlaceholder("Output field name"); + String[] currentKey = {outputName}; + outputField.addValueChangeListener(e -> { + String sourceVal = entry.fieldMappings.remove(currentKey[0]); + if (sourceVal == null) sourceVal = ""; + currentKey[0] = e.getValue(); + entry.fieldMappings.put(currentKey[0], sourceVal); + }); + + ComboBox sourceCombo = new ComboBox<>(); + sourceCombo.setWidth("100%"); + sourceCombo.setEmptySelectionAllowed(true); + sourceCombo.setItems(sourceFields); + sourceCombo.setPlaceholder("Select source field..."); + if (sourceName != null && !sourceName.isEmpty() && sourceFields.contains(sourceName)) { + sourceCombo.setValue(sourceName); + } + sourceCombos.add(sourceCombo); + sourceCombo.addValueChangeListener(e -> { + String currentOutputName = outputField.getValue(); + if (e.getValue() != null) { + entry.fieldMappings.put(currentOutputName, e.getValue()); + } else { + entry.fieldMappings.put(currentOutputName, ""); + } + }); + + Label colonLabel = new Label(":"); + colonLabel.setWidthUndefined(); + + Button removeFieldBtn = new Button("\u00D7"); + removeFieldBtn.addStyleName("small"); + removeFieldBtn.addClickListener(e -> { + entry.fieldMappings.remove(outputField.getValue()); + sourceCombos.remove(sourceCombo); + fieldRows.removeComponent(row); + }); + + row.addComponents(outputField, colonLabel, sourceCombo, removeFieldBtn); + row.setExpandRatio(outputField, 1f); + row.setExpandRatio(sourceCombo, 1f); + row.setComponentAlignment(removeFieldBtn, Alignment.MIDDLE_CENTER); + + return row; + } + + private void collectFieldPaths(DataComponent component, String prefix, List paths) { + for (int i = 0; i < component.getComponentCount(); i++) { + DataComponent child = component.getComponent(i); + String path = prefix.isEmpty() ? child.getName() : prefix + "/" + child.getName(); + + // need to check nested fields + if (child.getComponentCount() > 0) + collectFieldPaths(child, path, paths); + else + paths.add(path); + } + } +}