From 76a84a39543ee393a99920a343129456bc3180a9 Mon Sep 17 00:00:00 2001 From: Philipp Zehnder Date: Thu, 15 Aug 2024 09:41:33 +0200 Subject: [PATCH] feat(#3112): opc ua multi node selection editor (#3138) * refactor(#3116): Create component for button menu * refactor(#3116): Move static property utils * refactor(#3116): Add test to validate tree view of opc ua and buttons * refactor(#3116): Extract button menu in static-tree-input * refactor(#3116): Extract component selected-nodes * refactor(#3116): Fix CSS for component selected-nodes * refactor(#3116): Fix CSS for component selected-nodes * refactor(#3116): Harmonized naming of component * refactor(#3116): Extract static-runtime-resolvable-tree-input component * refactor(#3116): Add test to show node details * refactor(#3116): Add component for node details * refactor(#3116): Add missing headers * refactor(#3116): Add missing headers * feat(#3112): Add button to switch editor mode * feat(#3112): Refactor parameter extractor for tree properties * feat(#3112): First version of text input works now * feat(#3112): First version of text input works now * feat(#3112): First version of text input works now * feat(#3112):Ensure switch between Tree and text view works as expected * feat(#3112): Handle broken node ids * feat(#3112): Add description header to text editor * feat(#3112): Remove empty lines * feat(#3112): Fix tree view editor navigation * feat(#3112): Add exception when node id is invalid * feat(#3112): fix checkstyle * Add missing css class * Fix minor styling issues * feat(#3112): Fix editing of opc ua adapters --------- Co-authored-by: Dominik Riemer --- .../api/extractor/IParameterExtractor.java | 7 + .../opcua/adapter/OpcUaNodeBrowser.java | 61 +++- .../opcua/config/SpOpcUaConfigExtractor.java | 2 +- .../OpcUaNodeMetadataExtractorTest.java | 67 ++--- .../extractor/AbstractParameterExtractor.java | 274 ++++++++++++------ .../AdapterParameterExtractorTest.java | 5 +- ui/cypress/support/builder/AdapterBuilder.ts | 4 +- ...Builder.ts => TreeNodeUserInputBuilder.ts} | 25 +- .../{TreeNode.ts => TreeNodeUserInput.ts} | 7 +- ui/cypress/support/model/UserInput.ts | 4 +- ui/cypress/support/utils/ErrorMessageUtils.ts | 26 ++ .../support/utils/connect/ConnectUtils.ts | 11 + .../userInput/TreeStaticPropertyUtils.ts | 89 +++++- ui/cypress/tests/connect/opcAdapter.spec.ts | 93 ------ .../connect/opcAdapterConfiguration.spec.ts | 99 ------- .../opcua/opcAdapterConfiguration.spec.ts | 178 ++++++++++++ .../opcua/startAndEditOpcAdapters.spec.ts | 167 +++++++++++ .../sp-exception-message.component.html | 2 +- ui/src/app/core-ui/core-ui.module.ts | 2 + ...ntime-resolvable-tree-input.component.html | 24 +- ...ntime-resolvable-tree-input.component.scss | 9 - ...runtime-resolvable-tree-input.component.ts | 34 ++- ...tic-tree-input-browse-nodes.component.scss | 4 +- ...tatic-tree-input-browse-nodes.component.ts | 1 + ...atic-tree-input-button-menu.component.html | 29 +- ...atic-tree-input-button-menu.component.scss | 21 ++ ...static-tree-input-button-menu.component.ts | 9 + ...c-tree-input-selected-nodes.component.scss | 23 ++ ...tic-tree-input-selected-nodes.component.ts | 5 +- ...atic-tree-input-text-editor.component.html | 26 ++ ...static-tree-input-text-editor.component.ts | 77 +++++ 31 files changed, 1010 insertions(+), 375 deletions(-) rename ui/cypress/support/builder/{TreeNodeBuilder.ts => TreeNodeUserInputBuilder.ts} (66%) rename ui/cypress/support/model/{TreeNode.ts => TreeNodeUserInput.ts} (86%) create mode 100644 ui/cypress/support/utils/ErrorMessageUtils.ts delete mode 100644 ui/cypress/tests/connect/opcAdapter.spec.ts delete mode 100644 ui/cypress/tests/connect/opcAdapterConfiguration.spec.ts create mode 100644 ui/cypress/tests/connect/opcua/opcAdapterConfiguration.spec.ts create mode 100644 ui/cypress/tests/connect/opcua/startAndEditOpcAdapters.spec.ts create mode 100644 ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-button-menu/static-tree-input-button-menu.component.scss create mode 100644 ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-selected-nodes/static-tree-input-selected-nodes.component.scss create mode 100644 ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-text-editor/static-tree-input-text-editor.component.html create mode 100644 ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-text-editor/static-tree-input-text-editor.component.ts diff --git a/streampipes-extensions-api/src/main/java/org/apache/streampipes/extensions/api/extractor/IParameterExtractor.java b/streampipes-extensions-api/src/main/java/org/apache/streampipes/extensions/api/extractor/IParameterExtractor.java index dbcdb4eba4..396f0f3961 100644 --- a/streampipes-extensions-api/src/main/java/org/apache/streampipes/extensions/api/extractor/IParameterExtractor.java +++ b/streampipes-extensions-api/src/main/java/org/apache/streampipes/extensions/api/extractor/IParameterExtractor.java @@ -60,6 +60,13 @@ public interface IParameterExtractor { List selectedMultiValues(String internalName, Class targetClass); + List selectedTreeNodesInternalNames(String internalName, + Class targetClass); + + /** + * @deprecated use {@link #selectedTreeNodesInternalNames(String, Class)} instead + */ + @Deprecated(since = "0.97.0", forRemoval = true) List selectedTreeNodesInternalNames(String internalName, Class targetClass, boolean onlyDataNodes); diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeBrowser.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeBrowser.java index fa33a2896e..e7dde9d990 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeBrowser.java +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeBrowser.java @@ -29,6 +29,7 @@ import org.eclipse.milo.opcua.sdk.client.nodes.UaVariableNode; import org.eclipse.milo.opcua.stack.core.Identifiers; import org.eclipse.milo.opcua.stack.core.UaException; +import org.eclipse.milo.opcua.stack.core.UaRuntimeException; import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; @@ -49,8 +50,10 @@ public class OpcUaNodeBrowser { private static final Logger LOG = LoggerFactory.getLogger(OpcUaNodeBrowser.class); - public OpcUaNodeBrowser(OpcUaClient client, - OpcUaConfig spOpcUaClientConfig) { + public OpcUaNodeBrowser( + OpcUaClient client, + OpcUaConfig spOpcUaClientConfig + ) { this.client = client; this.spOpcConfig = spOpcUaClientConfig; } @@ -85,14 +88,37 @@ public List buildNodeTreeFromOrigin(String nextBaseNodeToResolve) private OpcNode toOpcNode(String nodeName) throws UaException { AddressSpace addressSpace = getAddressSpace(); - NodeId nodeId = NodeId.parse(nodeName); - UaNode node = addressSpace.getNode(nodeId); - LOG.info("Using node of type {}", node.getNodeClass().toString()); + NodeId nodeId; + try { + nodeId = NodeId.parse(nodeName); + } catch (UaRuntimeException e) { + throw new UaException( + StatusCode.BAD.getValue(), "Node ID " + nodeName + " is not in the correct format. " + + "The correct format is `ns=;=`.", e); + } + + UaNode node; + try { + node = addressSpace.getNode(nodeId); + } catch (UaException e) { + throw new UaException( + StatusCode.BAD.getValue(), + "Node with ID " + nodeId + " is not present in the OPC UA server.", e + ); + } + + LOG.info( + "Using node of type {}", + node.getNodeClass() + .toString() + ); if (node instanceof UaVariableNode) { - UInteger value = (UInteger) ((UaVariableNode) node).getDataType().getIdentifier(); - return new OpcNode(node.getDisplayName().getText(), OpcUaTypes.getType(value), node.getNodeId()); + UInteger value = (UInteger) ((UaVariableNode) node).getDataType() + .getIdentifier(); + return new OpcNode(node.getDisplayName() + .getText(), OpcUaTypes.getType(value), node.getNodeId()); } LOG.warn("Node {} not of type UaVariableNode", node.getDisplayName()); @@ -100,16 +126,20 @@ private OpcNode toOpcNode(String nodeName) throws UaException { throw new UaException(StatusCode.BAD, "Node is not of type BaseDataVariableTypeNode"); } - private List findChildren(OpcUaClient client, - NodeId nodeId) throws UaException { + private List findChildren( + OpcUaClient client, + NodeId nodeId + ) throws UaException { return client .getAddressSpace() .browseNodes(nodeId) .stream() .map(node -> { TreeInputNode childNode = new TreeInputNode(); - childNode.setNodeName(node.getDisplayName().getText()); - childNode.setInternalNodeName(node.getNodeId().toParseableString()); + childNode.setNodeName(node.getDisplayName() + .getText()); + childNode.setInternalNodeName(node.getNodeId() + .toParseableString()); childNode.setDataNode(isDataNode(node)); childNode.setNodeMetadata(new OpcUaNodeMetadataExtractor(client, node).extract()); return childNode; @@ -118,13 +148,18 @@ private List findChildren(OpcUaClient client, } - private AddressSpace getAddressSpace() { return client.getAddressSpace(); } private boolean isDataNode(UaNode node) { - return (node.getNodeClass().equals(NodeClass.Variable) || (node.getNodeClass().equals(NodeClass.VariableType))) + return ( + node.getNodeClass() + .equals(NodeClass.Variable) || ( + node.getNodeClass() + .equals(NodeClass.VariableType) + ) + ) && node instanceof UaVariableNode; } diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/SpOpcUaConfigExtractor.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/SpOpcUaConfigExtractor.java index 1145cb3501..26cae21569 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/SpOpcUaConfigExtractor.java +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/SpOpcUaConfigExtractor.java @@ -74,7 +74,7 @@ public static T extractSharedConfig(IParameterExtractor String selectedAlternativeAuthentication = extractor.selectedAlternativeInternalId(ACCESS_MODE.name()); List selectedNodeNames = - extractor.selectedTreeNodesInternalNames(AVAILABLE_NODES.name(), String.class, true); + extractor.selectedTreeNodesInternalNames(AVAILABLE_NODES.name(), String.class); config.setSelectedNodeNames(selectedNodeNames); diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/test/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeMetadataExtractorTest.java b/streampipes-extensions/streampipes-connectors-opcua/src/test/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeMetadataExtractorTest.java index d37f81d8c3..4a5360563f 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/src/test/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeMetadataExtractorTest.java +++ b/streampipes-extensions/streampipes-connectors-opcua/src/test/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeMetadataExtractorTest.java @@ -25,9 +25,10 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -45,8 +46,8 @@ public void testExtractDescription() { extractor.extractDescription(); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("Description")); - Assertions.assertEquals(description, metadata.get("Description")); + assertTrue(metadata.containsKey("Description")); + assertEquals(description, metadata.get("Description")); } @Test @@ -58,8 +59,8 @@ public void testExtractDescriptionNull() { extractor.extractDescription(); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("Description")); - Assertions.assertEquals("", metadata.get("Description")); + assertTrue(metadata.containsKey("Description")); + assertEquals("", metadata.get("Description")); } @Test @@ -71,8 +72,8 @@ public void testExtractNamespaceIndex() { extractor.extractNamespaceIndex(); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("NamespaceIndex")); - Assertions.assertEquals("1", metadata.get("NamespaceIndex")); + assertTrue(metadata.containsKey("NamespaceIndex")); + assertEquals("1", metadata.get("NamespaceIndex")); } @Test @@ -84,8 +85,8 @@ public void testExtractNamespaceIndexNull() { extractor.extractNamespaceIndex(); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("NamespaceIndex")); - Assertions.assertEquals("", metadata.get("NamespaceIndex")); + assertTrue(metadata.containsKey("NamespaceIndex")); + assertEquals("", metadata.get("NamespaceIndex")); } @Test @@ -97,8 +98,8 @@ public void testExtractNodeClass() { extractor.extractNodeClass(); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("NodeClass")); - Assertions.assertEquals("Variable", metadata.get("NodeClass")); + assertTrue(metadata.containsKey("NodeClass")); + assertEquals("Variable", metadata.get("NodeClass")); } @Test @@ -110,8 +111,8 @@ public void testExtractNodeClassNull() { extractor.extractNodeClass(); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("NodeClass")); - Assertions.assertEquals("", metadata.get("NodeClass")); + assertTrue(metadata.containsKey("NodeClass")); + assertEquals("", metadata.get("NodeClass")); } @Test @@ -124,8 +125,8 @@ public void testExtractBrowseName() { extractor.extractBrowseName(); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("BrowseName")); - Assertions.assertEquals(expectedNodeName, metadata.get("BrowseName")); + assertTrue(metadata.containsKey("BrowseName")); + assertEquals(expectedNodeName, metadata.get("BrowseName")); } @Test @@ -137,8 +138,8 @@ public void testExtractBrowseNameNull() { extractor.extractBrowseName(); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("BrowseName")); - Assertions.assertEquals("", metadata.get("BrowseName")); + assertTrue(metadata.containsKey("BrowseName")); + assertEquals("", metadata.get("BrowseName")); } @Test @@ -151,8 +152,8 @@ public void testExtractDisplayName() { extractor.extractDisplayName(); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("DisplayName")); - Assertions.assertEquals(expectedName, metadata.get("DisplayName")); + assertTrue(metadata.containsKey("DisplayName")); + assertEquals(expectedName, metadata.get("DisplayName")); } @Test @@ -164,8 +165,8 @@ public void testExtractDisplayNameNull() { extractor.extractDisplayName(); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("DisplayName")); - Assertions.assertEquals("", metadata.get("DisplayName")); + assertTrue(metadata.containsKey("DisplayName")); + assertEquals("", metadata.get("DisplayName")); } @Test @@ -177,8 +178,8 @@ public void testExtractSourceTime() { extractor.extractSourceTime(value); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("SourceTime")); - Assertions.assertEquals(date, metadata.get("SourceTime")); + assertTrue(metadata.containsKey("SourceTime")); + assertEquals(date, metadata.get("SourceTime")); } @Test @@ -190,8 +191,8 @@ public void testExtractSourceTimeNull() { extractor.extractSourceTime(value); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("SourceTime")); - Assertions.assertEquals("", metadata.get("SourceTime")); + assertTrue(metadata.containsKey("SourceTime")); + assertEquals("", metadata.get("SourceTime")); } @Test @@ -203,8 +204,8 @@ public void testExtractServerTime() { extractor.extractServerTime(value); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("ServerTime")); - Assertions.assertEquals(date, metadata.get("ServerTime")); + assertTrue(metadata.containsKey("ServerTime")); + assertEquals(date, metadata.get("ServerTime")); } @Test @@ -216,8 +217,8 @@ public void testExtractServerTimeNull() { extractor.extractServerTime(value); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("ServerTime")); - Assertions.assertEquals("", metadata.get("ServerTime")); + assertTrue(metadata.containsKey("ServerTime")); + assertEquals("", metadata.get("ServerTime")); } @Test @@ -230,8 +231,8 @@ public void testExtractDataType() { extractor.extractDataType(dataTypeNode); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("DataType")); - Assertions.assertEquals(expectedName, metadata.get("DataType")); + assertTrue(metadata.containsKey("DataType")); + assertEquals(expectedName, metadata.get("DataType")); } @Test @@ -243,8 +244,8 @@ public void testExtractDataTypeNull() { extractor.extractDataType(dataTypeNode); var metadata = extractor.getMetadata(); - Assertions.assertTrue(metadata.containsKey("DataType")); - Assertions.assertEquals("", metadata.get("DataType")); + assertTrue(metadata.containsKey("DataType")); + assertEquals("", metadata.get("DataType")); } private OpcUaNodeMetadataExtractor getExtractor(UaNode node) { diff --git a/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/extractor/AbstractParameterExtractor.java b/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/extractor/AbstractParameterExtractor.java index 2bec1f786f..68026a6d85 100644 --- a/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/extractor/AbstractParameterExtractor.java +++ b/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/extractor/AbstractParameterExtractor.java @@ -67,7 +67,8 @@ public abstract class AbstractParameterExtractor ep.getRuntimeName().equals(runtimeName)) + .filter(ep -> ep.getRuntimeName() + .equals(runtimeName)) .map(ep -> (EventPropertyPrimitive) ep) .findFirst() .get() @@ -117,7 +119,7 @@ public Object singleValueParameter(EventPropertyPrimitive targetType, String int @Override public V singleValueParameter(String internalName, Class targetClass) { return typeParser.parse(getStaticPropertyByName(internalName, FreeTextStaticProperty.class) - .getValue(), targetClass); + .getValue(), targetClass); } @Override @@ -127,8 +129,9 @@ public String textParameter(String internalName) { @Override public String secretValue(String internalName) { - return (getStaticPropertyByName(internalName, SecretStaticProperty.class) - .getValue()); + return ( + getStaticPropertyByName(internalName, SecretStaticProperty.class) + .getValue()); } @Override @@ -151,15 +154,17 @@ public String selectedFilename(String internalName) { return getStaticPropertyByName(internalName, FileStaticProperty.class).getLocationPath(); } - private V selectedSingleValue(String internalName, Class targetClass, - Class oneOfStaticProperty) { + private V selectedSingleValue( + String internalName, Class targetClass, + Class oneOfStaticProperty + ) { return typeParser.parse(getStaticPropertyByName(internalName, oneOfStaticProperty) - .getOptions() - .stream() - .filter(Option::isSelected) - .findFirst() - .get() - .getName(), targetClass); + .getOptions() + .stream() + .filter(Option::isSelected) + .findFirst() + .get() + .getName(), targetClass); } @Override @@ -170,12 +175,12 @@ public V selectedSingleValue(String internalName, Class targetClass) { @Override public V selectedSingleValueInternalName(String internalName, Class targetClass) { return typeParser.parse(getStaticPropertyByName(internalName, OneOfStaticProperty.class) - .getOptions() - .stream() - .filter(Option::isSelected) - .findFirst() - .get() - .getInternalName(), targetClass); + .getOptions() + .stream() + .filter(Option::isSelected) + .findFirst() + .get() + .getInternalName(), targetClass); } @Override @@ -188,14 +193,18 @@ public List collectionMembersAsGroup(String internalName) { .collect(Collectors.toList()); } - private Boolean comparePropertyRuntimeType(EventProperty eventProperty, - Datatypes datatype) { + private Boolean comparePropertyRuntimeType( + EventProperty eventProperty, + Datatypes datatype + ) { return comparePropertyRuntimeType(eventProperty, datatype, false); } - private Boolean comparePropertyRuntimeType(EventProperty eventProperty, - Datatypes datatype, - boolean ignoreListElements) { + private Boolean comparePropertyRuntimeType( + EventProperty eventProperty, + Datatypes datatype, + boolean ignoreListElements + ) { EventPropertyPrimitive testProperty = null; if (eventProperty instanceof EventPropertyList && !ignoreListElements) { testProperty = (EventPropertyPrimitive) ((EventPropertyList) eventProperty).getEventProperty(); @@ -204,7 +213,8 @@ private Boolean comparePropertyRuntimeType(EventProperty eventProperty, } if (testProperty != null) { - return testProperty.getRuntimeType().equals(datatype.toString()); + return testProperty.getRuntimeType() + .equals(datatype.toString()); } else { return false; } @@ -240,29 +250,68 @@ public List selectedMultiValues(String internalName, Class targetClass } @Override - public List selectedTreeNodesInternalNames(String internalName, - Class targetClass, - boolean onlyDataNodes) { + /** + * Extracts the user configuration from the tree static property. + * @param internalName the internal name of the static property + * @param targetClass the target class of the internal names + * @return the list of selected nodes internal names + */ + public List selectedTreeNodesInternalNames( + String internalName, + Class targetClass + ) { + var runtimeResolvableTreeInputStaticProperty = getStaticPropertyByName( + internalName, + RuntimeResolvableTreeInputStaticProperty.class + ); + + return runtimeResolvableTreeInputStaticProperty.getSelectedNodesInternalNames() + .stream() + .map(node -> typeParser.parse(node, targetClass)) + .toList(); + } + + @Override + @Deprecated(since = "0.97.0", forRemoval = true) + /** + * This method returns a list of all nodes. + * Therefore, it requires both the property selectedNodesInternalNames and the nodes property to be set. + * The nodes are used to check for the data type. The problem with this implementation is, is that the client (e.g. + * UI) must get the nodes from the OPC UA server. + * + * @deprecated use {@link #selectedTreeNodesInternalNames(String, Class)} instead + */ + public List selectedTreeNodesInternalNames( + String internalName, + Class targetClass, + boolean onlyDataNodes + ) { List allNodes = new ArrayList<>(); RuntimeResolvableTreeInputStaticProperty sp = getStaticPropertyByName(internalName, RuntimeResolvableTreeInputStaticProperty.class); - if (!sp.getNodes().isEmpty()) { - sp.getNodes().forEach(node -> buildFlatTree(node, allNodes)); + if (!sp.getNodes() + .isEmpty()) { + sp.getNodes() + .forEach(node -> buildFlatTree(node, allNodes)); } if (!allNodes.isEmpty()) { return sp.getSelectedNodesInternalNames() - .stream() - .filter(node -> { - if (!onlyDataNodes) { - return true; - } else { - var existingNode = allNodes.stream().filter(n -> n.getInternalNodeName().equals(node)).findFirst(); - return existingNode.map(TreeInputNode::isDataNode).orElse(false); - } - }) - .map(node -> typeParser.parse(node, targetClass)) - .collect(Collectors.toList()); + .stream() + .filter(node -> { + if (!onlyDataNodes) { + return true; + } else { + var existingNode = allNodes.stream() + .filter(n -> n.getInternalNodeName() + .equals(node)) + .findFirst(); + return existingNode.map(TreeInputNode::isDataNode) + .orElse(false); + } + }) + .map(node -> typeParser.parse(node, targetClass)) + .collect(Collectors.toList()); } else { return new ArrayList<>(); } @@ -271,13 +320,16 @@ public List selectedTreeNodesInternalNames(String internalName, private void buildFlatTree(TreeInputNode parent, List collector) { collector.add(parent); if (parent.hasChildren()) { - parent.getChildren().forEach(child -> buildFlatTree(child, collector)); + parent.getChildren() + .forEach(child -> buildFlatTree(child, collector)); } } @Override - public W getStaticPropertyByName(String internalName, Class - spType) { + public W getStaticPropertyByName( + String internalName, Class + spType + ) { return spType.cast(getStaticPropertyByName(internalName)); } @@ -286,10 +338,13 @@ public StaticProperty getStaticPropertyByName(String name) { return getStaticPropertyByName(sepaElement.getStaticProperties(), name); } - private StaticProperty getStaticPropertyByName(List staticProperties, - String name) { + private StaticProperty getStaticPropertyByName( + List staticProperties, + String name + ) { for (StaticProperty p : staticProperties) { - if (p.getInternalName().equals(name)) { + if (p.getInternalName() + .equals(name)) { return p; } else if (p.getStaticPropertyType() == StaticPropertyType.StaticPropertyGroup) { return getStaticPropertyByName(((StaticPropertyGroup) p).getStaticProperties(), name); @@ -297,7 +352,8 @@ private StaticProperty getStaticPropertyByName(List staticProper StaticProperty tmp = getStaticPropertyFromSelectedAlternative((StaticPropertyAlternatives) p); if (tmp != null) { tmp = getStaticPropertyByName(Collections.singletonList(tmp), name); - if (tmp != null && tmp.getInternalName().equals(name)) { + if (tmp != null && tmp.getInternalName() + .equals(name)) { return tmp; } } @@ -308,11 +364,11 @@ private StaticProperty getStaticPropertyByName(List staticProper private StaticProperty getStaticPropertyFromSelectedAlternative(StaticPropertyAlternatives sp) { return sp.getAlternatives() - .stream() - .filter(StaticPropertyAlternative::getSelected) - .findFirst() - .get() - .getStaticProperty(); + .stream() + .filter(StaticPropertyAlternative::getSelected) + .findFirst() + .get() + .getStaticProperty(); } @Override @@ -342,13 +398,15 @@ public List mappingPropertyValues(String staticPropertyName) { public String propertyDatatype(String runtimeName) { List eventProperties = new ArrayList<>(); for (SpDataStream is : sepaElement.getInputStreams()) { - eventProperties.addAll(is.getEventSchema().getEventProperties()); + eventProperties.addAll(is.getEventSchema() + .getEventProperties()); } Optional matchedProperty = eventProperties .stream() - .filter(ep -> ep.getRuntimeName().equals - (runtimeName)) + .filter(ep -> ep.getRuntimeName() + .equals + (runtimeName)) .findFirst(); if (matchedProperty.isPresent()) { @@ -368,7 +426,7 @@ public String propertyDatatype(String runtimeName) { @Override public List getEventPropertiesBySelector(List selectors) throws - SpRuntimeException { + SpRuntimeException { List properties = new ArrayList<>(); for (String selector : selectors) { properties.add(getEventPropertyBySelector(selector)); @@ -380,8 +438,13 @@ public List getEventPropertiesBySelector(List selectors) public EventProperty getEventPropertyBySelector(String selector) throws SpRuntimeException { SpDataStream input = getStreamBySelector(selector); - List matchedProperties = getEventProperty(selector, getStreamSelector - (selector), input.getEventSchema().getEventProperties()); + List matchedProperties = getEventProperty( + selector, + getStreamSelector + (selector), + input.getEventSchema() + .getEventProperties() + ); if (matchedProperties.size() > 0) { return matchedProperties.get(0); @@ -402,8 +465,10 @@ public String getEventPropertyTypeBySelector(String selector) throws SpRuntimeEx } - private List getEventProperty(String selector, String currentPointer, - List properties) { + private List getEventProperty( + String selector, String currentPointer, + List properties + ) { for (EventProperty property : properties) { if (makePropertyWithSelector(currentPointer, property.getRuntimeName()).equals(selector)) { return Collections.singletonList(property); @@ -422,7 +487,8 @@ private String makePropertyWithSelector(String currentPointer, String runtimeNam private SpDataStream getStreamBySelector(String selector) { String streamId = getStreamSelector(selector).substring(1); - return sepaElement.getInputStreams().get(Integer.parseInt(streamId)); + return sepaElement.getInputStreams() + .get(Integer.parseInt(streamId)); } private String getStreamSelector(String selector) { @@ -434,13 +500,19 @@ public List getNoneInputStreamEventPropertySubset(List pr List properties = new ArrayList<>(); for (SpDataStream stream : sepaElement.getInputStreams()) { properties.addAll( - getNoneInputStreamEventPropertySubset(propertySelectors, sepaElement.getInputStreams().indexOf(stream))); + getNoneInputStreamEventPropertySubset( + propertySelectors, + sepaElement.getInputStreams() + .indexOf(stream) + )); } return properties; } - private List getNoneInputStreamEventPropertySubset(List propertySelectors, - Integer streamIndex) { + private List getNoneInputStreamEventPropertySubset( + List propertySelectors, + Integer streamIndex + ) { return sepaElement .getInputStreams() .get(streamIndex) @@ -459,7 +531,11 @@ public List getInputStreamEventPropertySubset(List proper List properties = new ArrayList<>(); for (SpDataStream stream : sepaElement.getInputStreams()) { properties.addAll( - getInputStreamEventPropertySubset(propertySelectors, sepaElement.getInputStreams().indexOf(stream))); + getInputStreamEventPropertySubset( + propertySelectors, + sepaElement.getInputStreams() + .indexOf(stream) + )); } return properties; } @@ -486,36 +562,45 @@ private String getStreamIndex(Integer streamIndex) { } private String getPropertySelectorFromUnaryMapping(String staticPropertyName) { - Optional property = sepaElement.getStaticProperties().stream() - .filter(p -> p instanceof MappingPropertyUnary) - .map((p -> (MappingPropertyUnary) p)) - .filter(p -> p.getInternalName().equals(staticPropertyName)) - .findFirst(); + Optional property = sepaElement.getStaticProperties() + .stream() + .filter(p -> p instanceof MappingPropertyUnary) + .map((p -> (MappingPropertyUnary) p)) + .filter(p -> p.getInternalName() + .equals(staticPropertyName)) + .findFirst(); - return property.map(MappingPropertyUnary::getSelectedProperty).orElse(null); + return property.map(MappingPropertyUnary::getSelectedProperty) + .orElse(null); } private List getPropertySelectorsFromNaryMapping(String staticPropertyName) { - Optional property = sepaElement.getStaticProperties().stream() - .filter(p -> p instanceof MappingPropertyNary) - .map((p -> (MappingPropertyNary) p)) - .filter(p -> p.getInternalName().equals(staticPropertyName)) - .findFirst(); + Optional property = sepaElement.getStaticProperties() + .stream() + .filter(p -> p instanceof MappingPropertyNary) + .map((p -> (MappingPropertyNary) p)) + .filter(p -> p.getInternalName() + .equals(staticPropertyName)) + .findFirst(); - return property.map(MappingPropertyNary::getSelectedProperties).orElse(new ArrayList<>()); + return property.map(MappingPropertyNary::getSelectedProperties) + .orElse(new ArrayList<>()); } @Override public String selectedAlternativeInternalId(String alternativesInternalId) { - StaticPropertyAlternatives alternatives = getStaticPropertyByName(alternativesInternalId, - StaticPropertyAlternatives.class); + StaticPropertyAlternatives alternatives = getStaticPropertyByName( + alternativesInternalId, + StaticPropertyAlternatives.class + ); return alternatives .getAlternatives() .stream() .filter(StaticPropertyAlternative::getSelected) .map(StaticProperty::getInternalName) - .findFirst().get(); + .findFirst() + .get(); } @Override @@ -524,12 +609,17 @@ public List getEventPropertiesRuntimeNamesByScope(PropertyScope scope) { List properties = new ArrayList<>(); for (SpDataStream stream : sepaElement.getInputStreams()) { - int streamIndex = sepaElement.getInputStreams().indexOf(stream); + int streamIndex = sepaElement.getInputStreams() + .indexOf(stream); getEventPropertiesByScope(scope, streamIndex) .stream() .forEach(ep -> propertiesSelector.add(ep.getRuntimeName())); - properties.addAll(getEventPropertiesByScope(scope, sepaElement.getInputStreams().indexOf(stream))); + properties.addAll(getEventPropertiesByScope( + scope, + sepaElement.getInputStreams() + .indexOf(stream) + )); } return propertiesSelector; } @@ -540,12 +630,17 @@ public List getEventPropertiesSelectorByScope(PropertyScope scope) { List properties = new ArrayList<>(); for (SpDataStream stream : sepaElement.getInputStreams()) { - int streamIndex = sepaElement.getInputStreams().indexOf(stream); + int streamIndex = sepaElement.getInputStreams() + .indexOf(stream); getEventPropertiesByScope(scope, streamIndex) .stream() .forEach(ep -> propertiesSelector.add(getBySelector(ep.getRuntimeName(), streamIndex))); - properties.addAll(getEventPropertiesByScope(scope, sepaElement.getInputStreams().indexOf(stream))); + properties.addAll(getEventPropertiesByScope( + scope, + sepaElement.getInputStreams() + .indexOf(stream) + )); } return propertiesSelector; } @@ -554,7 +649,11 @@ public List getEventPropertiesSelectorByScope(PropertyScope scope) { public List getEventPropertiesByScope(PropertyScope scope) { List properties = new ArrayList<>(); for (SpDataStream stream : sepaElement.getInputStreams()) { - properties.addAll(getEventPropertiesByScope(scope, sepaElement.getInputStreams().indexOf(stream))); + properties.addAll(getEventPropertiesByScope( + scope, + sepaElement.getInputStreams() + .indexOf(stream) + )); } return properties; } @@ -567,7 +666,8 @@ private List getEventPropertiesByScope(PropertyScope scope, Integ .getEventProperties() .stream() .filter(ep -> - ep.getPropertyScope() != null && ep.getPropertyScope().equals(scope.name())) + ep.getPropertyScope() != null && ep.getPropertyScope() + .equals(scope.name())) .collect(Collectors.toList()); } } diff --git a/streampipes-sdk/src/test/java/org/apache/streampipes/sdk/extractor/AdapterParameterExtractorTest.java b/streampipes-sdk/src/test/java/org/apache/streampipes/sdk/extractor/AdapterParameterExtractorTest.java index b473cb4875..5f8ee8a992 100644 --- a/streampipes-sdk/src/test/java/org/apache/streampipes/sdk/extractor/AdapterParameterExtractorTest.java +++ b/streampipes-sdk/src/test/java/org/apache/streampipes/sdk/extractor/AdapterParameterExtractorTest.java @@ -24,11 +24,11 @@ import org.apache.streampipes.model.staticproperty.StaticPropertyAlternatives; import org.apache.streampipes.sdk.builder.adapter.AdapterConfigurationBuilder; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -64,6 +64,7 @@ public void selectedParser() throws AdapterException { List.of(parserInstance) ); - Assertions.assertEquals(parserInstance, adapterParameterExtractor.selectedParser()); + assertEquals(parserInstance, adapterParameterExtractor.selectedParser()); } + } diff --git a/ui/cypress/support/builder/AdapterBuilder.ts b/ui/cypress/support/builder/AdapterBuilder.ts index 03a5ac46ba..1bf5d2257d 100644 --- a/ui/cypress/support/builder/AdapterBuilder.ts +++ b/ui/cypress/support/builder/AdapterBuilder.ts @@ -19,7 +19,7 @@ import { UserInput } from '../model/UserInput'; import { UserInputType } from '../model/UserInputType'; import { AdapterInput } from '../model/AdapterInput'; -import { TreeNodeBuilder } from './TreeNodeBuilder'; +import { TreeNodeUserInputBuilder } from './TreeNodeUserInputBuilder'; export class AdapterBuilder { adapterInput: AdapterInput; @@ -71,7 +71,7 @@ export class AdapterBuilder { return this; } - public addTreeNode(treeNode: TreeNodeBuilder) { + public addTreeNode(treeNode: TreeNodeUserInputBuilder) { const userInput = new UserInput(); userInput.type = 'tree'; userInput.treeNode = treeNode.build(); diff --git a/ui/cypress/support/builder/TreeNodeBuilder.ts b/ui/cypress/support/builder/TreeNodeUserInputBuilder.ts similarity index 66% rename from ui/cypress/support/builder/TreeNodeBuilder.ts rename to ui/cypress/support/builder/TreeNodeUserInputBuilder.ts index 0dd04ff1c7..b25c4e1017 100644 --- a/ui/cypress/support/builder/TreeNodeBuilder.ts +++ b/ui/cypress/support/builder/TreeNodeUserInputBuilder.ts @@ -16,34 +16,41 @@ * */ -import { TreeNode } from '../model/TreeNode'; +import { TreeNodeUserInput } from '../model/TreeNodeUserInput'; -export class TreeNodeBuilder { - private readonly node: TreeNode; +export class TreeNodeUserInputBuilder { + private readonly node: TreeNodeUserInput; constructor(name: string) { - this.node = { name, children: [] }; + this.node = { name, children: [], isTextConfig: false }; } - addChildren(...childrenBuilders: TreeNodeBuilder[]): TreeNodeBuilder { + addChildren( + ...childrenBuilders: TreeNodeUserInputBuilder[] + ): TreeNodeUserInputBuilder { for (const childBuilder of childrenBuilders) { this.node.children!.push(childBuilder.build()); } return this; } + isTextConfig(): TreeNodeUserInputBuilder { + this.node.isTextConfig = true; + return this; + } + static create( name: string, - ...children: TreeNodeBuilder[] - ): TreeNodeBuilder { - const builder = new TreeNodeBuilder(name); + ...children: TreeNodeUserInputBuilder[] + ): TreeNodeUserInputBuilder { + const builder = new TreeNodeUserInputBuilder(name); if (children.length > 0) { builder.addChildren(...children); } return builder; } - build(): TreeNode { + build(): TreeNodeUserInput { return this.node; } } diff --git a/ui/cypress/support/model/TreeNode.ts b/ui/cypress/support/model/TreeNodeUserInput.ts similarity index 86% rename from ui/cypress/support/model/TreeNode.ts rename to ui/cypress/support/model/TreeNodeUserInput.ts index 49c717c0d3..cb68542e71 100644 --- a/ui/cypress/support/model/TreeNode.ts +++ b/ui/cypress/support/model/TreeNodeUserInput.ts @@ -15,7 +15,8 @@ * limitations under the License. * */ -export class TreeNode { - name: String; - children?: TreeNode[]; +export class TreeNodeUserInput { + name: string; + children?: TreeNodeUserInput[]; + isTextConfig?: boolean = false; } diff --git a/ui/cypress/support/model/UserInput.ts b/ui/cypress/support/model/UserInput.ts index 175a3abe97..b6fd38b96f 100644 --- a/ui/cypress/support/model/UserInput.ts +++ b/ui/cypress/support/model/UserInput.ts @@ -17,11 +17,11 @@ */ import { UserInputType } from './UserInputType'; -import { TreeNode } from './TreeNode'; +import { TreeNodeUserInput } from './TreeNodeUserInput'; export class UserInput { type: UserInputType; selector: string; value: string; - treeNode?: TreeNode; + treeNode?: TreeNodeUserInput; } diff --git a/ui/cypress/support/utils/ErrorMessageUtils.ts b/ui/cypress/support/utils/ErrorMessageUtils.ts new file mode 100644 index 0000000000..31aa9baa3a --- /dev/null +++ b/ui/cypress/support/utils/ErrorMessageUtils.ts @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +export class ErrorMessageUtils { + /** + * Validates that an error message is displayed and that this message contains the @param message + */ + public static containsMessage(message: string) { + cy.dataCy('exception-message-title').should('contain.text', message); + } +} diff --git a/ui/cypress/support/utils/connect/ConnectUtils.ts b/ui/cypress/support/utils/connect/ConnectUtils.ts index 67556897e9..da59e92d98 100644 --- a/ui/cypress/support/utils/connect/ConnectUtils.ts +++ b/ui/cypress/support/utils/connect/ConnectUtils.ts @@ -129,6 +129,13 @@ export class ConnectUtils { this.configureFormat(adapterInput); + ConnectUtils.finishAdapterSettings(); + } + + /** + * Clicks next on the adapter settings page + */ + public static finishAdapterSettings() { // Next Button should not be disabled cy.get('button').contains('Next').parent().should('not.be.disabled'); @@ -347,6 +354,10 @@ export class ConnectUtils { ConnectBtns.startAdapter().click(); + ConnectUtils.validateEventsInPreview(amountOfProperties); + } + + public static validateEventsInPreview(amountOfProperties: number) { // View data ConnectBtns.infoAdapter().click(); cy.get('div').contains('Values').parent().click(); diff --git a/ui/cypress/support/utils/userInput/TreeStaticPropertyUtils.ts b/ui/cypress/support/utils/userInput/TreeStaticPropertyUtils.ts index 118a562542..7fec1438ff 100644 --- a/ui/cypress/support/utils/userInput/TreeStaticPropertyUtils.ts +++ b/ui/cypress/support/utils/userInput/TreeStaticPropertyUtils.ts @@ -16,25 +16,90 @@ * */ -import { TreeNode } from '../../model/TreeNode'; +import { TreeNodeUserInput } from '../../model/TreeNodeUserInput'; export class TreeStaticPropertyUtils { + /** + * Returns the tree editor + */ + public static treeEditor() { + return cy.dataCy('tree-editor'); + } + + /** + * Return the text editor + */ + public static textEditor() { + return cy.dataCy('text-editor'); + } + + /** + * Opens the text editor + */ + public static switchToTextEditor() { + cy.dataCy('editor-mode-text').click(); + } + + /** + * Opens the tree editor + */ + public static switchToTreeEditor() { + cy.dataCy('editor-mode-tree').click(); + } + + /** + * Appends the @param text to the text editor + */ + public static typeInTextEditor(text: string) { + cy.dataCy('static-tree-input-text-editor').type(text + '{enter}'); + } + + /** + * Returns the content of the text editor + */ + public static getTextInTextEditor() { + return cy + .dataCy('static-tree-input-text-editor') + .find('.CodeMirror-line') + .invoke('text'); + } + /** * Selects the @param treeNode in the tree view. If the tree node has * children, it will expand the tree node and recursivly navigate through * the selected node. */ - public static selectTreeNode(treeNode: TreeNode) { - if (treeNode.children && treeNode.children.length > 0) { - cy.dataCy('expand-' + treeNode.name).click(); - treeNode.children.forEach(child => { - this.selectTreeNode(child); - }); + public static selectTreeNode(treeNode: TreeNodeUserInput) { + if (!treeNode.isTextConfig) { + // configure tree node + if (treeNode.children && treeNode.children.length > 0) { + TreeStaticPropertyUtils.expandNode(treeNode.name); + treeNode.children.forEach(child => { + this.selectTreeNode(child); + }); + } else { + TreeStaticPropertyUtils.selectNode(treeNode.name); + } } else { - cy.dataCy('select-' + treeNode.name).click(); + TreeStaticPropertyUtils.switchToTextEditor(); + TreeStaticPropertyUtils.typeInTextEditor(treeNode.name); } } + /** + * Expand the node with @param treeNodeName in the tree view + */ + public static expandNode(treeNodeName: string) { + cy.dataCy('expand-' + treeNodeName).click(); + } + + /** + * Select the node with @param treeNodeName in the tree view + */ + public static selectNode(treeNodeName: string) { + cy.dataCy('select-' + treeNodeName).click(); + } + /** * Removes the selected node with the identifier @param nodeIdentifier. * dataCy could not be used because often special characters are used in @@ -55,6 +120,14 @@ export class TreeStaticPropertyUtils { ); } + /** + * Validates that the amount of nodes shown in the browse tab are equal + * to @param expectedAmount. + */ + public static validateAmountOfShownBrowseNodes(expectedAmount: number) { + cy.dataCy('expand-', {}, true).should('have.length', expectedAmount); + } + /** * Validates the number of node details metadata rows displayed. */ diff --git a/ui/cypress/tests/connect/opcAdapter.spec.ts b/ui/cypress/tests/connect/opcAdapter.spec.ts deleted file mode 100644 index f7addefa16..0000000000 --- a/ui/cypress/tests/connect/opcAdapter.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { ConnectUtils } from '../../support/utils/connect/ConnectUtils'; -import { ParameterUtils } from '../../support/utils/ParameterUtils'; -import { AdapterBuilder } from '../../support/builder/AdapterBuilder'; -import { TreeNodeBuilder } from '../../support/builder/TreeNodeBuilder'; - -describe('Test OPC-UA Adapter Pull Mode', () => { - beforeEach('Setup Test', () => { - cy.initStreamPipesTest(); - }); - - it('Test OPC-UA Adapter Pull Mode', () => { - const adapterInput = getAdapterBuilder(true); - - ConnectUtils.testAdapter(adapterInput); - }); - - it('Test OPC-UA Adapter Subscription Mode', () => { - const adapterInput = getAdapterBuilder(false); - - ConnectUtils.testAdapter(adapterInput); - }); -}); - -const getAdapterBuilder = (pullMode: boolean) => { - const host: string = ParameterUtils.get('localhost', 'opcua'); - - const builder = AdapterBuilder.create('OPC_UA').setName( - 'OPC UA Test ' + (pullMode ? '(Pull)' : '(Subscription)'), - ); - - if (pullMode) { - builder.addInput('radio', 'adapter_type-pull_mode', ''); - builder.addInput('input', 'undefined-PULLING_INTERVAL-0', '1000'); - } else { - builder.addInput('radio', 'adapter_type-subscription_mode', ''); - } - - builder - .addInput('radio', 'access_mode-none', '') - .addInput('radio', 'opc_host_or_url-url', '') - .addInput( - 'input', - 'undefined-OPC_SERVER_URL-0', - 'opc.tcp://' + host + ':50000', - ); - - builder.addTreeNode( - TreeNodeBuilder.create( - 'Objects', - TreeNodeBuilder.create( - 'OpcPlc', - TreeNodeBuilder.create( - 'Telemetry', - TreeNodeBuilder.create('Basic').addChildren( - TreeNodeBuilder.create('AlternatingBoolean'), - TreeNodeBuilder.create('StepUp'), - TreeNodeBuilder.create('RandomSignedInt32'), - TreeNodeBuilder.create('RandomUnsignedInt32'), - ), - // TreeNodeBuilder.create('Anomaly') - // .addChildren( - // TreeNodeBuilder.create('DipData'), - // TreeNodeBuilder.create('NegativeTrendData'), - // TreeNodeBuilder.create('PositiveTrendData'), - // TreeNodeBuilder.create('SpikeData'), - // ), - ), - ), - ), - ); - - builder.setAutoAddTimestampPropery(); - - return builder.build(); -}; diff --git a/ui/cypress/tests/connect/opcAdapterConfiguration.spec.ts b/ui/cypress/tests/connect/opcAdapterConfiguration.spec.ts deleted file mode 100644 index 57b10bd41a..0000000000 --- a/ui/cypress/tests/connect/opcAdapterConfiguration.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { ConnectUtils } from '../../support/utils/connect/ConnectUtils'; -import { ParameterUtils } from '../../support/utils/ParameterUtils'; -import { AdapterBuilder } from '../../support/builder/AdapterBuilder'; -import { TreeNodeBuilder } from '../../support/builder/TreeNodeBuilder'; -import { StaticPropertyUtils } from '../../support/utils/userInput/StaticPropertyUtils'; -import { TreeStaticPropertyUtils } from '../../support/utils/userInput/TreeStaticPropertyUtils'; - -describe('Test OPC-UA Adapter Pull Mode', () => { - beforeEach('Setup Test', () => { - cy.initStreamPipesTest(); - }); - - it('Test OPC-UA Adapter Pull Mode', () => { - const adapterConfiguration = getAdapterBuilder(); - - // Set up initial configuration - ConnectUtils.goToConnect(); - ConnectUtils.goToNewAdapterPage(); - ConnectUtils.selectAdapter(adapterConfiguration.adapterType); - StaticPropertyUtils.input(adapterConfiguration.adapterConfiguration); - - TreeStaticPropertyUtils.validateAmountOfSelectedNodes(2); - - TreeStaticPropertyUtils.checkThatNodeIsSelectedInTree( - 'AlternatingBoolean', - ); - - // Test if node details view works - TreeStaticPropertyUtils.validateAmountOfShownNodeDetailsMetaDataRows(0); - TreeStaticPropertyUtils.showNodeDetails('StepUp'); - TreeStaticPropertyUtils.validateAmountOfShownNodeDetailsMetaDataRows( - 10, - ); - TreeStaticPropertyUtils.hideNodeDetails('StepUp'); - TreeStaticPropertyUtils.validateAmountOfShownNodeDetailsMetaDataRows(0); - - // Test if delete node works - TreeStaticPropertyUtils.removeSelectedNode( - 'ns=3\\;s=AlternatingBoolean', - ); - TreeStaticPropertyUtils.validateAmountOfSelectedNodes(1); - - // Test clear selection and reload button - TreeStaticPropertyUtils.clickClearAndReloadButton(); - TreeStaticPropertyUtils.validateAmountOfSelectedNodes(0); - }); -}); - -const getAdapterBuilder = () => { - const host: string = ParameterUtils.get('localhost', 'opcua'); - - const builder = AdapterBuilder.create('OPC_UA') - .setName('OPC UA Configuration Test') - .addInput('radio', 'adapter_type-pull_mode', '') - .addInput('input', 'undefined-PULLING_INTERVAL-0', '1000') - .addInput('radio', 'access_mode-none', '') - .addInput('radio', 'opc_host_or_url-url', '') - .addInput( - 'input', - 'undefined-OPC_SERVER_URL-0', - 'opc.tcp://' + host + ':50000', - ) - .addTreeNode( - TreeNodeBuilder.create( - 'Objects', - TreeNodeBuilder.create( - 'OpcPlc', - TreeNodeBuilder.create( - 'Telemetry', - TreeNodeBuilder.create('Basic').addChildren( - TreeNodeBuilder.create('AlternatingBoolean'), - TreeNodeBuilder.create('StepUp'), - ), - ), - ), - ), - ) - .setAutoAddTimestampPropery(); - - return builder.build(); -}; diff --git a/ui/cypress/tests/connect/opcua/opcAdapterConfiguration.spec.ts b/ui/cypress/tests/connect/opcua/opcAdapterConfiguration.spec.ts new file mode 100644 index 0000000000..6dc6624e33 --- /dev/null +++ b/ui/cypress/tests/connect/opcua/opcAdapterConfiguration.spec.ts @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { ConnectUtils } from '../../../support/utils/connect/ConnectUtils'; +import { ParameterUtils } from '../../../support/utils/ParameterUtils'; +import { AdapterBuilder } from '../../../support/builder/AdapterBuilder'; +import { TreeNodeUserInputBuilder } from '../../../support/builder/TreeNodeUserInputBuilder'; +import { StaticPropertyUtils } from '../../../support/utils/userInput/StaticPropertyUtils'; +import { TreeStaticPropertyUtils } from '../../../support/utils/userInput/TreeStaticPropertyUtils'; +import { ErrorMessageUtils } from '../../../support/utils/ErrorMessageUtils'; +import { AdapterInput } from '../../../support/model/AdapterInput'; + +describe('Test OPC-UA Adapter Configuration', () => { + beforeEach('Setup Test', () => { + cy.initStreamPipesTest(); + }); + + it('Test OPC-UA Tree Node Configuration', () => { + const adapterBuilder = getAdapterBuilder(); + adapterBuilder.addTreeNode( + TreeNodeUserInputBuilder.create( + 'Objects', + TreeNodeUserInputBuilder.create( + 'OpcPlc', + TreeNodeUserInputBuilder.create( + 'Telemetry', + TreeNodeUserInputBuilder.create('Basic').addChildren( + TreeNodeUserInputBuilder.create( + 'AlternatingBoolean', + ), + TreeNodeUserInputBuilder.create('StepUp'), + ), + ), + ), + ), + ); + + const adapterInput = adapterBuilder.build(); + setUpInitialConfiguration(adapterInput); + + TreeStaticPropertyUtils.validateAmountOfSelectedNodes(2); + + TreeStaticPropertyUtils.checkThatNodeIsSelectedInTree( + 'AlternatingBoolean', + ); + + // Test if node details view works + TreeStaticPropertyUtils.validateAmountOfShownNodeDetailsMetaDataRows(0); + TreeStaticPropertyUtils.showNodeDetails('StepUp'); + TreeStaticPropertyUtils.validateAmountOfShownNodeDetailsMetaDataRows( + 10, + ); + TreeStaticPropertyUtils.hideNodeDetails('StepUp'); + TreeStaticPropertyUtils.validateAmountOfShownNodeDetailsMetaDataRows(0); + + // Test if delete node works + TreeStaticPropertyUtils.removeSelectedNode( + 'ns=3\\;s=AlternatingBoolean', + ); + TreeStaticPropertyUtils.validateAmountOfSelectedNodes(1); + + // Test clear selection and reload button + TreeStaticPropertyUtils.clickClearAndReloadButton(); + TreeStaticPropertyUtils.validateAmountOfSelectedNodes(0); + }); + + it('Test OPC-UA Text Editor', () => { + const adapterInput = getAdapterBuilder().build(); + setUpInitialConfiguration(adapterInput); + + TreeStaticPropertyUtils.treeEditor().should('be.visible'); + TreeStaticPropertyUtils.textEditor().should('not.exist'); + + // Switch to text editor + TreeStaticPropertyUtils.switchToTextEditor(); + + // Validate that text editor is shown + TreeStaticPropertyUtils.treeEditor().should('not.exist'); + TreeStaticPropertyUtils.textEditor().should('be.visible'); + + TreeStaticPropertyUtils.typeInTextEditor('ns=3;s=StepUp'); + + // Go back to tree editor and validate nodes are selected and browse editor works + TreeStaticPropertyUtils.switchToTreeEditor(); + TreeStaticPropertyUtils.validateAmountOfSelectedNodes(1); + TreeStaticPropertyUtils.validateAmountOfShownBrowseNodes(3); + + // Check if node is selected + TreeStaticPropertyUtils.expandNode('Objects'); + TreeStaticPropertyUtils.expandNode('OpcPlc'); + TreeStaticPropertyUtils.expandNode('Telemetry'); + TreeStaticPropertyUtils.expandNode('Basic'); + TreeStaticPropertyUtils.checkThatNodeIsSelectedInTree('StepUp'); + TreeStaticPropertyUtils.selectNode('AlternatingBoolean'); + + // Go back tree view and validate that the node is still selected + TreeStaticPropertyUtils.switchToTextEditor(); + TreeStaticPropertyUtils.getTextInTextEditor().should( + 'equal', + '# Provide OPC UA Node IDs below, one per line.# Format: ' + + 'ns=;s= (e.g., ns=3;s=SampleNodeId)' + + 'ns=3;s=StepUpns=3;s=AlternatingBoolean', + ); + + TreeStaticPropertyUtils.switchToTreeEditor(); + TreeStaticPropertyUtils.validateAmountOfShownBrowseNodes(3); + }); + + it('Test OPC-UA Node does not exist', () => { + const adapterInput = getAdapterBuilder().build(); + setUpInitialConfiguration(adapterInput); + + // Switch to text editor + TreeStaticPropertyUtils.switchToTextEditor(); + TreeStaticPropertyUtils.typeInTextEditor('ns=3;s=NodeDoesNotExist'); + + ConnectUtils.finishAdapterSettings(); + + // validate that an error is shown with node id + ErrorMessageUtils.containsMessage('NodeDoesNotExist'); + }); + + it('Test OPC-UA Wrong Node Id Format', () => { + const adapterInput = getAdapterBuilder().build(); + setUpInitialConfiguration(adapterInput); + + // Switch to text editor + TreeStaticPropertyUtils.switchToTextEditor(); + TreeStaticPropertyUtils.typeInTextEditor('NoValidNodeId'); + + ConnectUtils.finishAdapterSettings(); + + // validate that an error is shown with node id + ErrorMessageUtils.containsMessage('NoValidNodeId'); + }); +}); + +const getAdapterBuilder = () => { + const host: string = ParameterUtils.get('localhost', 'opcua'); + + const builder = AdapterBuilder.create('OPC_UA') + .setName('OPC UA Configuration Test') + .addInput('radio', 'adapter_type-pull_mode', '') + .addInput('input', 'undefined-PULLING_INTERVAL-0', '1000') + .addInput('radio', 'access_mode-none', '') + .addInput('radio', 'opc_host_or_url-url', '') + .addInput( + 'input', + 'undefined-OPC_SERVER_URL-0', + 'opc.tcp://' + host + ':50000', + ) + + .setAutoAddTimestampPropery(); + + return builder; +}; + +const setUpInitialConfiguration = (adapterInput: AdapterInput) => { + ConnectUtils.goToConnect(); + ConnectUtils.goToNewAdapterPage(); + ConnectUtils.selectAdapter(adapterInput.adapterType); + StaticPropertyUtils.input(adapterInput.adapterConfiguration); +}; diff --git a/ui/cypress/tests/connect/opcua/startAndEditOpcAdapters.spec.ts b/ui/cypress/tests/connect/opcua/startAndEditOpcAdapters.spec.ts new file mode 100644 index 0000000000..4e8ccba5fe --- /dev/null +++ b/ui/cypress/tests/connect/opcua/startAndEditOpcAdapters.spec.ts @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { ConnectUtils } from '../../../support/utils/connect/ConnectUtils'; +import { ParameterUtils } from '../../../support/utils/ParameterUtils'; +import { AdapterBuilder } from '../../../support/builder/AdapterBuilder'; +import { TreeNodeUserInputBuilder } from '../../../support/builder/TreeNodeUserInputBuilder'; +import { ConnectBtns } from '../../../support/utils/connect/ConnectBtns'; +import { TreeStaticPropertyUtils } from '../../../support/utils/userInput/TreeStaticPropertyUtils'; +import { ConnectEventSchemaUtils } from '../../../support/utils/connect/ConnectEventSchemaUtils'; +import { AdapterInput } from '../../../support/model/AdapterInput'; + +describe('Test starting and editing OPC-UA Adapters in different configurations', () => { + beforeEach('Setup Test', () => { + cy.initStreamPipesTest(); + }); + + it('Create OPC-UA Adapter Tree Editor Pull Mode', () => { + const adapterInput = getAdapterBuilderWithTreeNodes(true); + startAdapterTest(adapterInput); + }); + + it('Create OPC-UA Adapter Tree Editor Subscription Mode', () => { + const adapterInput = getAdapterBuilderWithTreeNodes(false); + startAdapterTest(adapterInput); + }); + + it('Create OPC-UA Adapter Text Editor Pull Mode', () => { + const adapterInput = getAdapterBuilderWithTextNodes(true); + startAdapterTest(adapterInput); + }); + + it('Create OPC-UA Adapter Text Editor Subscription Mode', () => { + const adapterInput = getAdapterBuilderWithTextNodes(false); + startAdapterTest(adapterInput); + }); + + it('Edit OPC-UA Adapter created with Tree editor', () => { + const adapterInput = getAdapterBuilderWithTreeNodes(true); + + editAdapterTest(adapterInput); + }); + + it('Edit OPC-UA Adapter created with Text editor', () => { + const adapterInput = getAdapterBuilderWithTextNodes(true); + + editAdapterTest(adapterInput); + }); +}); + +const getAdapterBuilderWithTreeNodes = (pullMode: boolean) => { + const builder = getBaseAdapterConfigBuilder(pullMode); + builder.addTreeNode( + TreeNodeUserInputBuilder.create( + 'Objects', + TreeNodeUserInputBuilder.create( + 'OpcPlc', + TreeNodeUserInputBuilder.create( + 'Telemetry', + TreeNodeUserInputBuilder.create('Basic').addChildren( + TreeNodeUserInputBuilder.create('AlternatingBoolean'), + TreeNodeUserInputBuilder.create('StepUp'), + TreeNodeUserInputBuilder.create('RandomSignedInt32'), + TreeNodeUserInputBuilder.create('RandomUnsignedInt32'), + ), + ), + ), + ), + ); + + return builder.build(); +}; + +/** + * The start adapter test expects an adapter input with the same schema + * description for all tests. Only the opc ua related options might differ. + */ +const startAdapterTest = (adapterInput: AdapterInput) => { + ConnectUtils.testAdapter(adapterInput); + ConnectUtils.validateEventsInPreview(5); +}; + +/** + * The edit adapter test expects an adapter input with the same schema + * description for all tests. Only the opc ua related options might differ. + */ +const editAdapterTest = (adapterInput: AdapterInput) => { + ConnectUtils.testAdapter(adapterInput); + + ConnectBtns.editAdapter().click(); + + // Validate that browse nodes are shown + TreeStaticPropertyUtils.validateAmountOfShownBrowseNodes(3); + + // Remove a node and validate that resulting events do not contain the property + TreeStaticPropertyUtils.removeSelectedNode('ns=3;s=RandomUnsignedInt32'); + ConnectUtils.finishAdapterSettings(); + ConnectEventSchemaUtils.finishEventSchemaConfiguration(); + ConnectBtns.storeEditAdapter().click(); + ConnectUtils.closeAdapterPreview(); + ConnectUtils.validateEventsInPreview(4); +}; + +const getAdapterBuilderWithTextNodes = (pullMode: boolean) => { + const builder = getBaseAdapterConfigBuilder(pullMode); + builder.addTreeNode( + TreeNodeUserInputBuilder.create( + 'ns=3;s=AlternatingBoolean', + ).isTextConfig(), + ); + builder.addTreeNode( + TreeNodeUserInputBuilder.create('ns=3;s=StepUp').isTextConfig(), + ); + builder.addTreeNode( + TreeNodeUserInputBuilder.create( + 'ns=3;s=RandomSignedInt32', + ).isTextConfig(), + ); + builder.addTreeNode( + TreeNodeUserInputBuilder.create( + 'ns=3;s=RandomUnsignedInt32', + ).isTextConfig(), + ); + + return builder.build(); +}; + +const getBaseAdapterConfigBuilder = (pullMode: boolean): AdapterBuilder => { + const host: string = ParameterUtils.get('localhost', 'opcua'); + + const builder = AdapterBuilder.create('OPC_UA').setName('OPC UA Test'); + + if (pullMode) { + builder.addInput('radio', 'adapter_type-pull_mode', ''); + builder.addInput('input', 'undefined-PULLING_INTERVAL-0', '1000'); + } else { + builder.addInput('radio', 'adapter_type-subscription_mode', ''); + } + + builder + .addInput('radio', 'access_mode-none', '') + .addInput('radio', 'opc_host_or_url-url', '') + .addInput( + 'input', + 'undefined-OPC_SERVER_URL-0', + 'opc.tcp://' + host + ':50000', + ); + + builder.setAutoAddTimestampPropery(); + + return builder; +}; diff --git a/ui/projects/streampipes/shared-ui/src/lib/components/sp-exception-message/sp-exception-message.component.html b/ui/projects/streampipes/shared-ui/src/lib/components/sp-exception-message/sp-exception-message.component.html index 04493f10e3..741bc96a9f 100644 --- a/ui/projects/streampipes/shared-ui/src/lib/components/sp-exception-message/sp-exception-message.component.html +++ b/ui/projects/streampipes/shared-ui/src/lib/components/sp-exception-message/sp-exception-message.component.html @@ -30,7 +30,7 @@
{{ messageTimestamp | date: 'short' }}
-
+
{{ message.title || 'Error' }}
diff --git a/ui/src/app/core-ui/core-ui.module.ts b/ui/src/app/core-ui/core-ui.module.ts index 8b589c4b18..971206876f 100644 --- a/ui/src/app/core-ui/core-ui.module.ts +++ b/ui/src/app/core-ui/core-ui.module.ts @@ -113,6 +113,7 @@ import { StaticTreeInputButtonMenuComponent } from './static-properties/static-r import { StaticTreeInputSelectedNodesComponent } from './static-properties/static-runtime-resolvable-tree-input/static-tree-input-selected-nodes/static-tree-input-selected-nodes.component'; import { StaticTreeInputBrowseNodesComponent } from './static-properties/static-runtime-resolvable-tree-input/static-tree-input-browse-nodes/static-tree-input-browse-nodes.component'; import { StaticTreeInputNodeDetailsComponent } from './static-properties/static-runtime-resolvable-tree-input/static-tree-input-node-details/static-tree-input-node-details.component'; +import { StaticTreeInputTextEditorComponent } from './static-properties/static-runtime-resolvable-tree-input/static-tree-input-text-editor/static-tree-input-text-editor.component'; @NgModule({ imports: [ @@ -194,6 +195,7 @@ import { StaticTreeInputNodeDetailsComponent } from './static-properties/static- StaticRuntimeResolvableTreeInputComponent, StaticTreeInputBrowseNodesComponent, StaticTreeInputNodeDetailsComponent, + StaticTreeInputTextEditorComponent, StaticSlideToggleComponent, ErrorHintComponent, AddToCollectionComponent, diff --git a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-runtime-resolvable-tree-input.component.html b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-runtime-resolvable-tree-input.component.html index bcb41d33a8..a641ba547f 100644 --- a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-runtime-resolvable-tree-input.component.html +++ b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-runtime-resolvable-tree-input.component.html @@ -19,6 +19,8 @@ @@ -28,7 +30,12 @@ -
+
+ +
+ + +
diff --git a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-runtime-resolvable-tree-input.component.scss b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-runtime-resolvable-tree-input.component.scss index ac5d7522bf..9f7721e689 100644 --- a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-runtime-resolvable-tree-input.component.scss +++ b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-runtime-resolvable-tree-input.component.scss @@ -20,15 +20,6 @@ overflow-y: auto; } -.tree-normal-height { - min-height: 300px; - max-height: 300px; -} - -.tree-large-height { - min-height: 800px; -} - .sp-tree-invisible { display: none; } diff --git a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-runtime-resolvable-tree-input.component.ts b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-runtime-resolvable-tree-input.component.ts index 32f77cfb1a..d9b5f64768 100644 --- a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-runtime-resolvable-tree-input.component.ts +++ b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-runtime-resolvable-tree-input.component.ts @@ -39,8 +39,10 @@ export class StaticRuntimeResolvableTreeInputComponent { nodeDetails: TreeInputNode; + editorMode: 'tree' | 'text' = 'tree'; + @ViewChild('staticTreeInputBrowseNodesComponent') - staticTreeInputBrowseNodesComponent: StaticTreeInputBrowseNodesComponent; + private staticTreeInputBrowseNodesComponent: StaticTreeInputBrowseNodesComponent; constructor( runtimeResolvableService: RuntimeResolvableService, @@ -50,6 +52,7 @@ export class StaticRuntimeResolvableTreeInputComponent } ngOnInit(): void { + this.resetStaticPropertyStateAndReload(); if ( this.staticProperty.nodes.length === 0 && (!this.staticProperty.dependsOn || @@ -57,7 +60,7 @@ export class StaticRuntimeResolvableTreeInputComponent ) { this.loadOptionsFromRestApi(); } else if (this.staticProperty.nodes.length > 0) { - this.staticTreeInputBrowseNodesComponent.updateNodes( + this.staticTreeInputBrowseNodesComponent?.updateNodes( this.staticProperty.nodes, ); this.showOptions = true; @@ -90,11 +93,11 @@ export class StaticRuntimeResolvableTreeInputComponent } } else { this.staticProperty.nodes = staticProperty.nodes; - this.staticTreeInputBrowseNodesComponent.updateNodes( + this.staticTreeInputBrowseNodesComponent?.updateNodes( this.staticProperty.nodes, ); } - this.staticTreeInputBrowseNodesComponent.refreshTree(); + this.staticTreeInputBrowseNodesComponent?.refreshTree(); this.performValidation(); } @@ -123,7 +126,7 @@ export class StaticRuntimeResolvableTreeInputComponent afterErrorReceived() { this.staticProperty.nodes = []; - this.staticTreeInputBrowseNodesComponent.updateNodes([]); + this.staticTreeInputBrowseNodesComponent?.updateNodes([]); this.performValidation(); } @@ -135,7 +138,7 @@ export class StaticRuntimeResolvableTreeInputComponent this.staticProperty.nextBaseNodeToResolve = undefined; this.staticProperty.selectedNodesInternalNames = []; this.staticProperty.latestFetchedNodes = []; - this.staticTreeInputBrowseNodesComponent.updateNodes([]); + this.staticTreeInputBrowseNodesComponent?.updateNodes([]); this.loadOptionsFromRestApi(); } @@ -150,4 +153,23 @@ export class StaticRuntimeResolvableTreeInputComponent ); this.staticProperty.selectedNodesInternalNames.splice(index, 1); } + + changeEditorMode(mode: 'tree' | 'text') { + this.editorMode = mode; + + if (mode === 'tree') { + this.resetStaticPropertyStateAndReload(); + } + } + + /** + * The static property keeps the state of the last fetched nodes to be able + * to set the subtree to the right node. When a user switches the editor + * this state should be reset + */ + private resetStaticPropertyStateAndReload() { + this.staticProperty.latestFetchedNodes = []; + this.staticProperty.nextBaseNodeToResolve = undefined; + this.reload(); + } } diff --git a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-browse-nodes/static-tree-input-browse-nodes.component.scss b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-browse-nodes/static-tree-input-browse-nodes.component.scss index 949b23cb05..7a03232d17 100644 --- a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-browse-nodes/static-tree-input-browse-nodes.component.scss +++ b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-browse-nodes/static-tree-input-browse-nodes.component.scss @@ -17,8 +17,8 @@ */ .tree-normal-height { - min-height: 300px; - max-height: 300px; + min-height: 400px; + max-height: 400px; } .tree-large-height { diff --git a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-browse-nodes/static-tree-input-browse-nodes.component.ts b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-browse-nodes/static-tree-input-browse-nodes.component.ts index de04432f80..478c719509 100644 --- a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-browse-nodes/static-tree-input-browse-nodes.component.ts +++ b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-browse-nodes/static-tree-input-browse-nodes.component.ts @@ -78,6 +78,7 @@ export class StaticTreeInputBrowseNodesComponent implements OnInit { } updateNodes(nodes: TreeInputNode[]) { + console.log(nodes); this.dataSource.data = nodes; } diff --git a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-button-menu/static-tree-input-button-menu.component.html b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-button-menu/static-tree-input-button-menu.component.html index bf008690bb..a8d8386e10 100644 --- a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-button-menu/static-tree-input-button-menu.component.html +++ b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-button-menu/static-tree-input-button-menu.component.html @@ -16,13 +16,14 @@ ~ -->
-
+
-
+
+  Reloading nodes +
+ +
+ + Tree + Text +
diff --git a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-button-menu/static-tree-input-button-menu.component.scss b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-button-menu/static-tree-input-button-menu.component.scss new file mode 100644 index 0000000000..3e7b330409 --- /dev/null +++ b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-button-menu/static-tree-input-button-menu.component.scss @@ -0,0 +1,21 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +.reloading-nodes-label { + font-size: smaller; +} diff --git a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-button-menu/static-tree-input-button-menu.component.ts b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-button-menu/static-tree-input-button-menu.component.ts index 8249baebb3..fb0fdea41b 100644 --- a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-button-menu/static-tree-input-button-menu.component.ts +++ b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-button-menu/static-tree-input-button-menu.component.ts @@ -20,17 +20,22 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; @Component({ selector: 'sp-static-tree-input-button-menu', templateUrl: './static-tree-input-button-menu.component.html', + styleUrl: './static-tree-input-button-menu.component.scss', }) export class StaticTreeInputButtonMenuComponent { @Input() showOptions: boolean; @Input() loading: boolean; + @Input() + editorMode: 'tree' | 'text'; @Output() resetOptionsAndReload = new EventEmitter(); @Output() reload = new EventEmitter(); + @Output() + selectedEditorModeEmitter = new EventEmitter<'tree' | 'text'>(); onResetOptionsAndReload() { this.resetOptionsAndReload.emit(); @@ -39,4 +44,8 @@ export class StaticTreeInputButtonMenuComponent { onReload() { this.reload.emit(); } + + onChangeEditor(mode: 'tree' | 'text') { + this.selectedEditorModeEmitter.emit(mode); + } } diff --git a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-selected-nodes/static-tree-input-selected-nodes.component.scss b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-selected-nodes/static-tree-input-selected-nodes.component.scss new file mode 100644 index 0000000000..64122b4f23 --- /dev/null +++ b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-selected-nodes/static-tree-input-selected-nodes.component.scss @@ -0,0 +1,23 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +.selected-node { + background: var(--color-bg-1); + padding: 5px; + border: 1px solid var(--color-bg-3); +} diff --git a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-selected-nodes/static-tree-input-selected-nodes.component.ts b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-selected-nodes/static-tree-input-selected-nodes.component.ts index d81825c36f..400f56ccb4 100644 --- a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-selected-nodes/static-tree-input-selected-nodes.component.ts +++ b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-selected-nodes/static-tree-input-selected-nodes.component.ts @@ -21,7 +21,10 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; @Component({ selector: 'sp-static-tree-input-selected-nodes', templateUrl: './static-tree-input-selected-nodes.component.html', - styleUrl: '../static-runtime-resolvable-tree-input.component.scss', + styleUrls: [ + '../static-runtime-resolvable-tree-input.component.scss', + './static-tree-input-selected-nodes.component.scss', + ], }) export class StaticTreeInputSelectedNodesComponent { @Input() diff --git a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-text-editor/static-tree-input-text-editor.component.html b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-text-editor/static-tree-input-text-editor.component.html new file mode 100644 index 0000000000..8e3d97b00f --- /dev/null +++ b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-text-editor/static-tree-input-text-editor.component.html @@ -0,0 +1,26 @@ + + + + diff --git a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-text-editor/static-tree-input-text-editor.component.ts b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-text-editor/static-tree-input-text-editor.component.ts new file mode 100644 index 0000000000..66ba60a724 --- /dev/null +++ b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input-text-editor/static-tree-input-text-editor.component.ts @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Subject } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; +import { RuntimeResolvableTreeInputStaticProperty } from '@streampipes/platform-services'; + +@Component({ + selector: 'sp-static-tree-input-text-editor', + templateUrl: './static-tree-input-text-editor.component.html', +}) +export class StaticTreeInputTextEditorComponent implements OnInit { + @Input() + staticProperty: RuntimeResolvableTreeInputStaticProperty; + + @Output() + performValidationEmitter: EventEmitter = new EventEmitter(); + + editorOptions = { + mode: 'text/plain', + autoRefresh: true, + theme: 'dracula', + lineNumbers: true, + lineWrapping: true, + readOnly: false, + extraKeys: { + 'Ctrl-Space': 'autocomplete', + }, + }; + + headerText = + '# Provide OPC UA Node IDs below, one per line.\n' + + '# Format: ns=;s= (e.g., ns=3;s=SampleNodeId)\n'; + textEditor: string = ''; + + private textChangeSubject: Subject = new Subject(); + + constructor() { + this.textChangeSubject.pipe(debounceTime(500)).subscribe(value => { + this.onTextChange(value); + }); + } + + ngOnInit() { + this.textEditor = + this.headerText + + this.staticProperty.selectedNodesInternalNames.join('\n'); + } + + onTextEditorChange(value: string): void { + this.textChangeSubject.next(value); + } + + onTextChange(value: string): void { + this.staticProperty.selectedNodesInternalNames = value + .split('\n') + // remove empty lines and comments starting with # + .filter(line => line.trim() !== '' && !line.startsWith('#')); + this.performValidationEmitter.emit(); + } +}