Repository: apache/streampipes
Pull request #3225 (2024-09-12 12:07:48)

feat(#3224): Add API for programmatic adapter creation
<!--
  ~ 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.
  ~
  -->

  <!--
Thanks for contributing! Here are some tips you can follow to help us incorporate your contribution quickly and easily:
1. If this is your first time, please read our contributor guidelines:
    - https://streampipes.apache.org/community/get-involved/
    - https://cwiki.apache.org/confluence/display/STREAMPIPES/Getting+Started
2. Make sure the PR title is formatted like: `[#<GitHub issue id>] PR title ...`
3. If the PR is unfinished, add '[WIP]' in your PR title, e.g., `[WIP][#<GitHub issue id>] PR title ...`.
4. Please write your PR title to summarize what this PR proposes/fixes.
5. Link the PR to the corresponding GitHub issue (if present) in the `Development` section in the right menu bar.
6. Be sure to keep the PR description updated to reflect all changes.
7. If possible, provide a concise example to reproduce the issue for a faster review.
8. Make sure tests pass via `mvn clean install`.
9. (Optional) If the contribution is large, please file an Apache ICLA
    - http://apache.org/licenses/icla.pdf
-->

### Purpose
<!--
Please clarify what changes you are proposing and describe how those changes will address the issue.
Furthermore, describe potential consequences the changes might have.
-->

See #3224 

Example (OPC-UA Adapter):

```yaml
name: My OPC Adapter
description: Test
appId: org.apache.streampipes.connect.iiot.adapters.opcua
configuration:
  - opc_host_or_url: OPC_URL
    opc_server_url: opc.tcp://localhost:62541/milo
  - adapter_type: PULL_MODE
    pulling_interval: 1000
  - access_mode: UNAUTHENTICATED
  - available_nodes:
    - "ns=2;s=Leakage Test Station/temperature"
    - "ns=2;s=Leakage Test Station/pressure"

schema:
  temperature:
    propertyScope: measurement

enrich:
  timestamp: timestamp

transform:
  rename:
    temperature: temp

createOptions:
  start: true
  persist: true
```

### Remarks
<!--
Is there anything left we need to pay attention on?
Are there some references that might be important? E.g. links to Confluence, or discussions
on the mailing list or GitHub.
-->
PR introduces (a) breaking change(s): no
PR introduces (a) deprecation(s): no



Patch:
diff --git a/pom.xml b/pom.xml
index a9be6d64a1..b5b52c3d1a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -197,6 +197,11 @@
                 <artifactId>jackson-dataformat-xml</artifactId>
                 <version>${jackson.version}</version>
             </dependency>
+            <dependency>
+                <groupId>com.fasterxml.jackson.dataformat</groupId>
+                <artifactId>jackson-dataformat-yaml</artifactId>
+                <version>${jackson.version}</version>
+            </dependency>
             <dependency>
                 <groupId>com.fasterxml.woodstox</groupId>
                 <artifactId>woodstox-core</artifactId>
@@ -222,11 +227,6 @@
                 <artifactId>opencsv</artifactId>
                 <version>${opencsv.version}</version>
             </dependency>
-            <dependency>
-                <groupId>com.fasterxml.jackson.dataformat</groupId>
-                <artifactId>jackson-dataformat-yaml</artifactId>
-                <version>${jackson.version}</version>
-            </dependency>
             <dependency>
                 <groupId>com.squareup.okhttp3</groupId>
                 <artifactId>okhttp</artifactId>
diff --git a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/AdapterGenerationSteps.java b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/AdapterGenerationSteps.java
new file mode 100644
index 0000000000..ace506ba0e
--- /dev/null
+++ b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/AdapterGenerationSteps.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.connect.management.compact;
+
+import org.apache.streampipes.connect.management.compact.generator.AdapterBasicsGenerator;
+import org.apache.streampipes.connect.management.compact.generator.AdapterConfigGenerator;
+import org.apache.streampipes.connect.management.compact.generator.AdapterEnrichmentRuleGenerator;
+import org.apache.streampipes.connect.management.compact.generator.AdapterModelGenerator;
+import org.apache.streampipes.connect.management.compact.generator.AdapterSchemaGenerator;
+import org.apache.streampipes.connect.management.compact.generator.AdapterTransformationRuleGenerator;
+import org.apache.streampipes.connect.management.management.GuessManagement;
+
+import java.util.List;
+
+public class AdapterGenerationSteps {
+
+  public List<AdapterModelGenerator> getGenerators() {
+    return List.of(
+        new AdapterBasicsGenerator(),
+        new AdapterConfigGenerator(),
+        new AdapterSchemaGenerator(
+            new SchemaMetadataEnricher(),
+            new GuessManagement()
+        ),
+        new AdapterEnrichmentRuleGenerator(),
+        new AdapterTransformationRuleGenerator()
+    );
+  }
+}
diff --git a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/PersistPipelineHandler.java b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/PersistPipelineHandler.java
new file mode 100644
index 0000000000..f385334d05
--- /dev/null
+++ b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/PersistPipelineHandler.java
@@ -0,0 +1,112 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.connect.management.compact;
+
+import org.apache.streampipes.manager.template.PipelineTemplateManagement;
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.pipeline.PipelineOperationStatus;
+import org.apache.streampipes.model.schema.EventProperty;
+import org.apache.streampipes.model.schema.EventPropertyPrimitive;
+import org.apache.streampipes.model.staticproperty.FreeTextStaticProperty;
+import org.apache.streampipes.model.staticproperty.MappingPropertyUnary;
+import org.apache.streampipes.model.staticproperty.OneOfStaticProperty;
+import org.apache.streampipes.model.template.PipelineTemplateInvocation;
+import org.apache.streampipes.vocabulary.SO;
+
+import java.net.URI;
+import java.util.List;
+
+public class PersistPipelineHandler {
+
+  private static final String templateId = "org.apache.streampipes.manager.template.instances.DataLakePipelineTemplate";
+  private static final String configPrefix = "jsplumb_domId2";
+
+  private final PipelineTemplateManagement pipelineTemplateManagement;
+  private final String authenticatedUserSid;
+
+  public PersistPipelineHandler(PipelineTemplateManagement pipelineTemplateManagement,
+                                String authenticatedUserSid) {
+    this.pipelineTemplateManagement = pipelineTemplateManagement;
+    this.authenticatedUserSid = authenticatedUserSid;
+  }
+
+  public PipelineOperationStatus createAndStartPersistPipeline(AdapterDescription adapterDescription) {
+    var pipelineTemplateInvocation = pipelineTemplateManagement.prepareInvocation(
+        adapterDescription.getCorrespondingDataStreamElementId(),
+        templateId
+    );
+
+    applyPipelineName(pipelineTemplateInvocation, adapterDescription.getName());
+    applyDataLakeConfig(pipelineTemplateInvocation, adapterDescription);
+
+    return pipelineTemplateManagement.createAndStartPipeline(pipelineTemplateInvocation, authenticatedUserSid);
+  }
+
+  private void applyPipelineName(PipelineTemplateInvocation pipelineTemplateInvocation,
+                                 String adapterName) {
+    pipelineTemplateInvocation.setPipelineTemplateId(templateId);
+    pipelineTemplateInvocation.setKviName(adapterName);
+  }
+
+  private void applyDataLakeConfig(PipelineTemplateInvocation pipelineTemplateInvocation,
+                                   AdapterDescription adapterDescription) {
+    pipelineTemplateInvocation.getStaticProperties().forEach(sp -> {
+      if (sp.getInternalName().equalsIgnoreCase(withPrefix("db_measurement"))) {
+        ((FreeTextStaticProperty) sp).setValue(adapterDescription.getName());
+      }
+      if (sp.getInternalName().equalsIgnoreCase(withPrefix("timestamp_mapping"))) {
+        ((MappingPropertyUnary) sp).setSelectedProperty(
+            String.format("s0::%s", getTimestampField(adapterDescription)
+        ));
+      }
+      if (sp.getInternalName().equalsIgnoreCase(withPrefix("schema_update"))) {
+        ((OneOfStaticProperty) sp).getOptions().forEach(o -> {
+          if (o.getName().equals("Update schema")) {
+            o.setSelected(true);
+          }
+        });
+      }
+    });
+  }
+
+  private String withPrefix(String config) {
+    return configPrefix + config;
+  }
+
+  private String getTimestampField(AdapterDescription adapterDescription) {
+    return adapterDescription
+        .getDataStream()
+        .getEventSchema()
+        .getEventProperties()
+        .stream()
+        .filter(ep -> ep instanceof EventPropertyPrimitive)
+        .map(ep -> (EventPropertyPrimitive) ep)
+        .filter(ep -> hasTimestampType(ep.getDomainProperties()))
+        .map(EventProperty::getRuntimeName)
+        .findFirst()
+        .orElseThrow(() -> new IllegalArgumentException("Could not find timestamp field in schema"));
+  }
+
+  private boolean hasTimestampType(List<URI> semanticTypes) {
+    return semanticTypes
+        .stream()
+        .map(URI::toString)
+        .anyMatch(s -> s.equalsIgnoreCase(SO.DATE_TIME));
+  }
+}
diff --git a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/SchemaMetadataEnricher.java b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/SchemaMetadataEnricher.java
new file mode 100644
index 0000000000..1650839226
--- /dev/null
+++ b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/SchemaMetadataEnricher.java
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.connect.management.compact;
+
+import org.apache.streampipes.model.connect.adapter.compact.CompactEventProperty;
+import org.apache.streampipes.model.schema.EventProperty;
+import org.apache.streampipes.model.schema.EventPropertyPrimitive;
+import org.apache.streampipes.model.schema.PropertyScope;
+import org.apache.streampipes.vocabulary.XSD;
+
+import java.net.URI;
+import java.util.List;
+
+public class SchemaMetadataEnricher {
+
+  public void enrich(EventProperty property,
+                     CompactEventProperty propertyDef) {
+    if (propertyDef.propertyScope() != null) {
+      property.setPropertyScope(convertScope(propertyDef.propertyScope()).name());
+    }
+    if (propertyDef.label() != null) {
+      property.setLabel(propertyDef.label());
+    }
+    if (propertyDef.description() != null) {
+      property.setDescription(propertyDef.description());
+    }
+    if (property instanceof EventPropertyPrimitive && propertyDef.semanticType() != null) {
+      property.setDomainProperties(List.of(URI.create(propertyDef.semanticType())));
+    }
+  }
+
+  private PropertyScope convertScope(String scope) {
+    if (scope.equalsIgnoreCase("header")) {
+      return PropertyScope.HEADER_PROPERTY;
+    } else if (scope.equalsIgnoreCase("dimension")) {
+      return PropertyScope.DIMENSION_PROPERTY;
+    } else if (scope.equalsIgnoreCase("measurement")) {
+      return PropertyScope.MEASUREMENT_PROPERTY;
+    } else {
+      return PropertyScope.NONE;
+    }
+  }
+
+  private String convertType(String shortRuntimeType) {
+    return switch (shortRuntimeType) {
+      case "integer" -> XSD.INTEGER.toString();
+      case "boolean" -> XSD.BOOLEAN.toString();
+      case "float" -> XSD.FLOAT.toString();
+      case "double" -> XSD.DOUBLE.toString();
+      default -> XSD.STRING.toString();
+    };
+  }
+}
diff --git a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterBasicsGenerator.java b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterBasicsGenerator.java
new file mode 100644
index 0000000000..6b34ff6bb7
--- /dev/null
+++ b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterBasicsGenerator.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.connect.management.compact.generator;
+
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.connect.adapter.compact.CompactAdapter;
+import org.apache.streampipes.model.util.ElementIdGenerator;
+
+public class AdapterBasicsGenerator implements AdapterModelGenerator {
+
+  @Override
+  public void apply(AdapterDescription adapterDescription,
+                    CompactAdapter compactAdapter) {
+    adapterDescription.setName(compactAdapter.name());
+    adapterDescription.setDescription(compactAdapter.description());
+    if (compactAdapter.id() != null) {
+      adapterDescription.setElementId(compactAdapter.id());
+    } else {
+      adapterDescription.setElementId(ElementIdGenerator.makeElementId(adapterDescription));
+    }
+  }
+}
diff --git a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterConfigGenerator.java b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterConfigGenerator.java
new file mode 100644
index 0000000000..99f80c4889
--- /dev/null
+++ b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterConfigGenerator.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.connect.management.compact.generator;
+
+import org.apache.streampipes.manager.template.AdapterTemplateHandler;
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.connect.adapter.compact.CompactAdapter;
+import org.apache.streampipes.model.template.PipelineElementTemplate;
+
+public class AdapterConfigGenerator implements AdapterModelGenerator {
+
+  @Override
+  public void apply(AdapterDescription adapterDescription,
+                    CompactAdapter compactAdapter) {
+    var adapterTemplate = new PipelineElementTemplate();
+    adapterTemplate.setTemplateConfigs(compactAdapter.configuration());
+
+    new AdapterTemplateHandler(
+        adapterTemplate,
+        adapterDescription,
+        false
+    ).applyTemplateOnPipelineElement();
+  }
+}
diff --git a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterEnrichmentRuleGenerator.java b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterEnrichmentRuleGenerator.java
new file mode 100644
index 0000000000..5abb7a99ce
--- /dev/null
+++ b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterEnrichmentRuleGenerator.java
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.connect.management.compact.generator;
+
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.connect.adapter.compact.CompactAdapter;
+import org.apache.streampipes.model.connect.rules.value.AddTimestampRuleDescription;
+import org.apache.streampipes.sdk.helpers.EpProperties;
+
+public class AdapterEnrichmentRuleGenerator implements AdapterModelGenerator {
+
+  @Override
+  public void apply(AdapterDescription adapterDescription,
+                    CompactAdapter compactAdapter) throws Exception {
+    if (compactAdapter.enrich() != null) {
+      if (compactAdapter.enrich().timestamp() != null) {
+        var timestampRule = new AddTimestampRuleDescription(compactAdapter.enrich().timestamp());
+        adapterDescription.getRules().add(timestampRule);
+        adapterDescription.getEventSchema().addEventProperty(
+            EpProperties.timestampProperty("timestamp")
+        );
+      }
+    }
+  }
+}
diff --git a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterModelGenerator.java b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterModelGenerator.java
new file mode 100644
index 0000000000..9370dc5bcb
--- /dev/null
+++ b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterModelGenerator.java
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.connect.management.compact.generator;
+
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.connect.adapter.compact.CompactAdapter;
+
+public interface AdapterModelGenerator {
+
+  void apply(AdapterDescription adapterDescription,
+             CompactAdapter compactAdapter) throws Exception;
+}
diff --git a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterSchemaGenerator.java b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterSchemaGenerator.java
new file mode 100644
index 0000000000..d217d910aa
--- /dev/null
+++ b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterSchemaGenerator.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.connect.management.compact.generator;
+
+import org.apache.streampipes.commons.exceptions.NoServiceEndpointsAvailableException;
+import org.apache.streampipes.connect.management.compact.SchemaMetadataEnricher;
+import org.apache.streampipes.connect.management.management.GuessManagement;
+import org.apache.streampipes.extensions.api.connect.exception.WorkerAdapterException;
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.connect.adapter.compact.CompactAdapter;
+import org.apache.streampipes.model.connect.guess.GuessSchema;
+
+import java.io.IOException;
+
+public class AdapterSchemaGenerator implements AdapterModelGenerator {
+
+  private final SchemaMetadataEnricher enricher;
+  private final GuessManagement guessManagement;
+
+  public AdapterSchemaGenerator(SchemaMetadataEnricher enricher,
+                                GuessManagement guessManagement) {
+    this.enricher = enricher;
+    this.guessManagement = guessManagement;
+  }
+
+  @Override
+  public void apply(AdapterDescription adapterDescription,
+                    CompactAdapter compactAdapter)
+      throws WorkerAdapterException, NoServiceEndpointsAvailableException, IOException {
+
+    GuessSchema result = guessManagement.guessSchema(adapterDescription);
+    adapterDescription.getDataStream().setEventSchema(result.getEventSchema());
+
+    var schemaDef = compactAdapter.schema();
+
+    if (schemaDef != null) {
+      adapterDescription.getDataStream().getEventSchema().getEventProperties().forEach(ep -> {
+        if (schemaDef.containsKey(ep.getRuntimeName())) {
+          var compactPropertyDef = schemaDef.get(ep.getRuntimeName());
+          enricher.enrich(ep, compactPropertyDef);
+        }
+      });
+    }
+  }
+}
diff --git a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterTransformationRuleGenerator.java b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterTransformationRuleGenerator.java
new file mode 100644
index 0000000000..6e4f4f12fd
--- /dev/null
+++ b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/compact/generator/AdapterTransformationRuleGenerator.java
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.connect.management.compact.generator;
+
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.connect.adapter.compact.CompactAdapter;
+import org.apache.streampipes.model.connect.rules.TransformationRuleDescription;
+import org.apache.streampipes.model.connect.rules.schema.RenameRuleDescription;
+import org.apache.streampipes.model.connect.rules.value.UnitTransformRuleDescription;
+import org.apache.streampipes.model.schema.EventProperty;
+import org.apache.streampipes.model.schema.EventPropertyPrimitive;
+
+import java.net.URI;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+public class AdapterTransformationRuleGenerator implements AdapterModelGenerator {
+
+  @Override
+  public void apply(AdapterDescription adapterDescription,
+                    CompactAdapter compactAdapter) throws Exception {
+    if (compactAdapter.transform() != null) {
+      var transforms = compactAdapter.transform();
+      if (transforms.rename() != null) {
+        transforms.rename().forEach((key, value) -> {
+          addRule(adapterDescription, new RenameRuleDescription(key, value));
+          findProperty(adapterDescription, key).forEach(p -> p.setRuntimeName(value));
+        });
+      }
+      if (transforms.measurementUnit() != null) {
+        transforms.measurementUnit().forEach((key, value) -> {
+          var currentUnitOpt = getCurrentUnit(adapterDescription, key);
+          currentUnitOpt.ifPresent(unit -> {
+            addRule(adapterDescription, new UnitTransformRuleDescription(key, unit, value));
+            findProperty(adapterDescription, key)
+                .filter(ep -> ep instanceof EventPropertyPrimitive)
+                .map(ep -> (EventPropertyPrimitive) ep)
+                .forEach(ep -> ep.setMeasurementUnit(URI.create(value)));
+          });
+        });
+      }
+    }
+  }
+
+  private void addRule(AdapterDescription adapterDescription,
+                       TransformationRuleDescription rule) {
+    adapterDescription.getRules().add(rule);
+  }
+
+  private Optional<String> getCurrentUnit(AdapterDescription adapterDescription,
+                                          String runtimeName) {
+    return findProperty(adapterDescription, runtimeName)
+        .filter(ep -> ep instanceof EventPropertyPrimitive)
+        .map(ep -> (EventPropertyPrimitive) ep)
+        .map(EventPropertyPrimitive::getMeasurementUnit)
+        .map(URI::toString)
+        .findFirst();
+  }
+
+  private Stream<EventProperty> findProperty(AdapterDescription adapterDescription,
+                                             String runtimeName) {
+    return adapterDescription
+        .getDataStream()
+        .getEventSchema()
+        .getEventProperties()
+        .stream()
+        .filter(ep -> ep.getRuntimeName().equalsIgnoreCase(runtimeName));
+  }
+}
diff --git a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterMasterManagement.java b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterMasterManagement.java
index 4c613a24f8..62c514b1e6 100644
--- a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterMasterManagement.java
+++ b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterMasterManagement.java
@@ -64,13 +64,14 @@ public class AdapterMasterManagement {
     this.dataStreamResourceManager = dataStreamResourceManager;
   }
 
-  public String addAdapter(AdapterDescription ad,
+  public void addAdapter(AdapterDescription ad,
+                           String adapterElementId,
                            String principalSid)
       throws AdapterException {
 
     // Create elementId for adapter
     var dataStreamElementId = ElementIdGenerator.makeElementId(SpDataStream.class);
-    ad.setElementId(ElementIdGenerator.makeElementId(ad));
+    ad.setElementId(adapterElementId);
     ad.setCreatedAt(System.currentTimeMillis());
     ad.setCorrespondingDataStreamElementId(dataStreamElementId);
 
@@ -78,19 +79,15 @@ public class AdapterMasterManagement {
     var eventGrounding = GroundingUtils.createEventGrounding();
     ad.setEventGrounding(eventGrounding);
 
-    var elementId = this.adapterResourceManager.encryptAndCreate(ad);
+    this.adapterResourceManager.encryptAndCreate(ad);
 
     // Create stream
     var storedDescription = new SourcesManagement().createAdapterDataStream(ad, dataStreamElementId);
-    storedDescription.setCorrespondingAdapterId(elementId);
+    storedDescription.setCorrespondingAdapterId(adapterElementId);
     installDataSource(storedDescription, principalSid, true);
     LOG.info("Install source (source URL: {} in backend", ad.getElementId());
-
-    return ad.getElementId();
   }
 
-
-
   public AdapterDescription getAdapter(String elementId) throws AdapterException {
     List<AdapterDescription> allAdapters = adapterInstanceStorage.findAll();
 
diff --git a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/CompactAdapterManagement.java b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/CompactAdapterManagement.java
new file mode 100644
index 0000000000..a973a22d85
--- /dev/null
+++ b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/CompactAdapterManagement.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.connect.management.management;
+
+import org.apache.streampipes.connect.management.compact.generator.AdapterModelGenerator;
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.connect.adapter.compact.CompactAdapter;
+import org.apache.streampipes.storage.api.IAdapterStorage;
+import org.apache.streampipes.storage.couchdb.CouchDbStorageManager;
+
+import java.util.List;
+
+public class CompactAdapterManagement {
+
+  private final List<AdapterModelGenerator> generators;
+
+  public CompactAdapterManagement(List<AdapterModelGenerator> generators) {
+    this.generators = generators;
+  }
+
+  public AdapterDescription convertToAdapterDescription(CompactAdapter compactAdapter) throws Exception {
+
+    var adapterDescription = findAdapterDescription(compactAdapter.appId());
+
+    for (AdapterModelGenerator m : generators) {
+      m.apply(adapterDescription, compactAdapter);
+    }
+
+    return adapterDescription;
+  }
+
+  public AdapterDescription convertToAdapterDescription(CompactAdapter compactAdapter,
+                                                        AdapterDescription existingAdapter) throws Exception {
+    var adapterDescription = convertToAdapterDescription(compactAdapter);
+
+    existingAdapter.getDataStream().setEventSchema(adapterDescription.getDataStream().getEventSchema());
+    existingAdapter.setRules(adapterDescription.getRules());
+    existingAdapter.setConfig(adapterDescription.getConfig());
+    existingAdapter.setDescription(adapterDescription.getDescription());
+    existingAdapter.setName(adapterDescription.getName());
+
+    return existingAdapter;
+  }
+
+  private AdapterDescription findAdapterDescription(String appId) {
+    IAdapterStorage adapterStorage = CouchDbStorageManager.INSTANCE.getAdapterDescriptionStorage();
+    return adapterStorage.findAll()
+        .stream()
+        .filter(desc -> desc.getAppId()
+            .equals(appId))
+        .findFirst()
+        .orElseThrow(IllegalArgumentException::new);
+  }
+}
diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtil.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtil.java
index f7a3a2029d..aa49a7a5f8 100644
--- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtil.java
+++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtil.java
@@ -121,6 +121,8 @@ public class OpcUaUtil {
 
       var returnValues = response.get();
 
+      spOpcUaClient.disconnect();
+
       makeEventPreview(selectedNodes, eventPreview, fieldStatusInfos, returnValues);
 
 
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CompactAdapter.java b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CompactAdapter.java
new file mode 100644
index 0000000000..d8a0a93f29
--- /dev/null
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CompactAdapter.java
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ *
+ */
+
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.model.connect.adapter.compact;
+
+import java.util.List;
+import java.util.Map;
+
+public record CompactAdapter(
+    String id,
+    String name,
+    String description,
+    String appId,
+    List<Map<String, Object>> configuration,
+    Map<String, CompactEventProperty> schema,
+    EnrichmentConfig enrich,
+    TransformationConfig transform,
+    CreateOptions createOptions
+) {}
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CompactConfiguration.java b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CompactConfiguration.java
new file mode 100644
index 0000000000..9285ae1878
--- /dev/null
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CompactConfiguration.java
@@ -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.
+ *
+ */
+
+package org.apache.streampipes.model.connect.adapter.compact;
+
+import java.util.Map;
+
+public record CompactConfiguration(Map<String, Object> values) {}
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CompactEventProperty.java b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CompactEventProperty.java
new file mode 100644
index 0000000000..c0cdd7e6af
--- /dev/null
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CompactEventProperty.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.model.connect.adapter.compact;
+
+public record CompactEventProperty(
+    String label,
+    String description,
+    String propertyScope,
+    String semanticType
+) {
+}
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CompactTransport.java b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CompactTransport.java
new file mode 100644
index 0000000000..d28ddeb3ca
--- /dev/null
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CompactTransport.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.model.connect.adapter.compact;
+
+public record CompactTransport(String channel) {
+}
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CreateOptions.java b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CreateOptions.java
new file mode 100644
index 0000000000..6fc266ff21
--- /dev/null
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/CreateOptions.java
@@ -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.
+ *
+ */
+
+package org.apache.streampipes.model.connect.adapter.compact;
+
+public record CreateOptions(boolean persist,
+                            boolean start) {
+}
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/EnrichmentConfig.java b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/EnrichmentConfig.java
new file mode 100644
index 0000000000..7a993f3097
--- /dev/null
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/EnrichmentConfig.java
@@ -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.
+ *
+ */
+
+package org.apache.streampipes.model.connect.adapter.compact;
+
+public record EnrichmentConfig(String timestamp) {}
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/TransformationConfig.java b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/TransformationConfig.java
new file mode 100644
index 0000000000..888db3732f
--- /dev/null
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/compact/TransformationConfig.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.model.connect.adapter.compact;
+
+import java.util.Map;
+
+public record TransformationConfig(Map<String, String> rename,
+                                   Map<String, String> measurementUnit) {
+}
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/template/PipelineElementTemplate.java b/streampipes-model/src/main/java/org/apache/streampipes/model/template/PipelineElementTemplate.java
index d304b0f31e..5b5279d9a0 100644
--- a/streampipes-model/src/main/java/org/apache/streampipes/model/template/PipelineElementTemplate.java
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/template/PipelineElementTemplate.java
@@ -23,13 +23,15 @@ import org.apache.streampipes.model.shared.api.Storable;
 import com.fasterxml.jackson.annotation.JsonAlias;
 import com.google.gson.annotations.SerializedName;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
 @TsModel
 public class PipelineElementTemplate implements Storable {
 
-  Map<String, PipelineElementTemplateConfig> templateConfigs;
+  List<Map<String, Object>> templateConfigs;
 
   @JsonAlias("_id")
   private @SerializedName("_id") String elementId;
@@ -42,7 +44,7 @@ public class PipelineElementTemplate implements Storable {
 
   public PipelineElementTemplate(String templateName,
                                  String templateDescription,
-                                 Map<String, PipelineElementTemplateConfig> templateConfigs) {
+                                 List<Map<String, Object>> templateConfigs) {
     this.templateName = templateName;
     this.templateDescription = templateDescription;
     this.templateConfigs = templateConfigs;
@@ -50,6 +52,7 @@ public class PipelineElementTemplate implements Storable {
 
   public PipelineElementTemplate() {
     this.elementId = UUID.randomUUID().toString();
+    this.templateConfigs = new ArrayList<>();
   }
 
   public String getTemplateName() {
@@ -68,11 +71,11 @@ public class PipelineElementTemplate implements Storable {
     this.templateDescription = templateDescription;
   }
 
-  public Map<String, PipelineElementTemplateConfig> getTemplateConfigs() {
+  public List<Map<String, Object>> getTemplateConfigs() {
     return templateConfigs;
   }
 
-  public void setTemplateConfigs(Map<String, PipelineElementTemplateConfig> templateConfigs) {
+  public void setTemplateConfigs(List<Map<String, Object>> templateConfigs) {
     this.templateConfigs = templateConfigs;
   }
 
diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/AbstractTemplateHandler.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/AbstractTemplateHandler.java
index b53ba70420..d89561ccec 100644
--- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/AbstractTemplateHandler.java
+++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/AbstractTemplateHandler.java
@@ -20,9 +20,6 @@ package org.apache.streampipes.manager.template;
 
 import org.apache.streampipes.model.template.PipelineElementTemplate;
 
-import java.util.HashMap;
-import java.util.Map;
-
 public abstract class AbstractTemplateHandler<T> {
 
   protected T element;
@@ -39,9 +36,7 @@ public abstract class AbstractTemplateHandler<T> {
   }
 
   public T applyTemplateOnPipelineElement() {
-    Map<String, Object> configs = new HashMap<>();
-    template.getTemplateConfigs().forEach((key, value) -> configs.put(key, value.getValue()));
-    PipelineElementTemplateVisitor visitor = new PipelineElementTemplateVisitor(configs);
+    PipelineElementTemplateVisitor visitor = new PipelineElementTemplateVisitor(template.getTemplateConfigs());
     visitStaticProperties(visitor);
 
     if (overwriteNameAndDescription) {
diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineElementTemplateVisitor.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineElementTemplateVisitor.java
index a32bd5d6d3..11ec281927 100644
--- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineElementTemplateVisitor.java
+++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineElementTemplateVisitor.java
@@ -42,19 +42,21 @@ import org.apache.streampipes.user.management.encryption.SecretEncryptionManager
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
 
 public class PipelineElementTemplateVisitor implements StaticPropertyVisitor {
 
-  private final Map<String, Object> configs;
+  private final List<Map<String, Object>> configs;
 
-  public PipelineElementTemplateVisitor(Map<String, Object> configs) {
+  public PipelineElementTemplateVisitor(List<Map<String, Object>> configs) {
     this.configs = configs;
   }
 
   @Override
   public void visit(AnyStaticProperty property) {
-    if (hasKey(property)) {
-      List<String> value = getValueAsList(property);
+    if (hasConfig(property)) {
+      List<String> value = getConfigValueAsStringList(property);
       property.getOptions().forEach(option -> {
         option.setSelected(value.stream().anyMatch(v -> v.equals(option.getName())));
       });
@@ -63,19 +65,19 @@ public class PipelineElementTemplateVisitor implements StaticPropertyVisitor {
 
   @Override
   public void visit(CodeInputStaticProperty codeInputStaticProperty) {
-    if (hasKey(codeInputStaticProperty)) {
-      codeInputStaticProperty.setValue(getAsString(codeInputStaticProperty));
+    if (hasConfig(codeInputStaticProperty)) {
+      codeInputStaticProperty.setValue(getConfigValueAsString(codeInputStaticProperty));
     }
   }
 
   @Override
   public void visit(CollectionStaticProperty collectionStaticProperty) {
-    if (hasKey(collectionStaticProperty)) {
-      List<Map<String, Object>> values = getAsList(collectionStaticProperty, "members");
+    if (hasConfig(collectionStaticProperty)) {
+      List<Map<String, Object>> values = getConfigValueAsList(collectionStaticProperty);
       collectionStaticProperty.setMembers(new ArrayList<>());
       values.forEach(v -> {
         StaticProperty sp = new Cloner().staticProperty(collectionStaticProperty.getStaticPropertyTemplate());
-        PipelineElementTemplateVisitor visitor = new PipelineElementTemplateVisitor(v);
+        PipelineElementTemplateVisitor visitor = new PipelineElementTemplateVisitor(List.of(v));
         sp.accept(visitor);
         collectionStaticProperty.getMembers().add(sp);
       });
@@ -84,37 +86,37 @@ public class PipelineElementTemplateVisitor implements StaticPropertyVisitor {
 
   @Override
   public void visit(ColorPickerStaticProperty colorPickerStaticProperty) {
-    if (hasKey(colorPickerStaticProperty)) {
-      colorPickerStaticProperty.setSelectedColor(getAsString(colorPickerStaticProperty));
+    if (hasConfig(colorPickerStaticProperty)) {
+      colorPickerStaticProperty.setSelectedColor(getConfigValueAsString(colorPickerStaticProperty));
     }
   }
 
   @Override
   public void visit(FileStaticProperty fileStaticProperty) {
-    if (hasKey(fileStaticProperty)) {
-      fileStaticProperty.setLocationPath(getAsString(fileStaticProperty));
+    if (hasConfig(fileStaticProperty)) {
+      fileStaticProperty.setLocationPath(getConfigValueAsString(fileStaticProperty));
     }
   }
 
   @Override
   public void visit(FreeTextStaticProperty freeTextStaticProperty) {
-    if (hasKey(freeTextStaticProperty)) {
-      freeTextStaticProperty.setValue(getAsString(freeTextStaticProperty));
+    if (hasConfig(freeTextStaticProperty)) {
+      freeTextStaticProperty.setValue(getConfigValueAsString(freeTextStaticProperty));
     }
   }
 
   @Override
   public void visit(MappingPropertyNary mappingPropertyNary) {
-    if (hasKey(mappingPropertyNary)) {
-      var selectedProperties = getValueAsList(mappingPropertyNary);
+    if (hasConfig(mappingPropertyNary)) {
+      var selectedProperties = getConfigValueAsStringList(mappingPropertyNary);
       mappingPropertyNary.setSelectedProperties(selectedProperties);
     }
   }
 
   @Override
   public void visit(MappingPropertyUnary mappingPropertyUnary) {
-    if (hasKey(mappingPropertyUnary)) {
-      var selectedProperty = getAsString(mappingPropertyUnary);
+    if (hasConfig(mappingPropertyUnary)) {
+      var selectedProperty = getConfigValueAsString(mappingPropertyUnary);
       mappingPropertyUnary.setSelectedProperty(selectedProperty);
     }
   }
@@ -126,8 +128,8 @@ public class PipelineElementTemplateVisitor implements StaticPropertyVisitor {
 
   @Override
   public void visit(OneOfStaticProperty oneOfStaticProperty) {
-    if (hasKey(oneOfStaticProperty)) {
-      String value = getAsString(oneOfStaticProperty);
+    if (hasConfig(oneOfStaticProperty)) {
+      String value = getConfigValueAsString(oneOfStaticProperty);
       oneOfStaticProperty.getOptions().forEach(option ->
           option.setSelected(option.getName().equals(value)));
     }
@@ -135,10 +137,11 @@ public class PipelineElementTemplateVisitor implements StaticPropertyVisitor {
 
   @Override
   public void visit(SecretStaticProperty secretStaticProperty) {
-    if (hasKey(secretStaticProperty)) {
-      Map<String, Object> values = getAsMap(secretStaticProperty);
-      boolean encrypted = Boolean.parseBoolean(String.valueOf(values.get("encrypted")));
-      String value = String.valueOf(values.get("value"));
+    if (hasConfig(secretStaticProperty)) {
+      Map<String, Object> values = getConfig(secretStaticProperty);
+      boolean encrypted = values.containsKey("encrypted")
+          && Boolean.parseBoolean(String.valueOf(values.get("encrypted")));
+      String value = getConfigValueAsString(secretStaticProperty);
       if (encrypted) {
         secretStaticProperty.setValue(value);
       } else {
@@ -151,50 +154,52 @@ public class PipelineElementTemplateVisitor implements StaticPropertyVisitor {
 
   @Override
   public void visit(StaticPropertyAlternative staticPropertyAlternative) {
-    if (hasKey(staticPropertyAlternative)) {
-      Map<String, Object> values = getAsMap(staticPropertyAlternative);
-      StaticProperty property = staticPropertyAlternative.getStaticProperty();
-      staticPropertyAlternative.setSelected(Boolean.parseBoolean(String.valueOf(values.get("selected"))));
-      if (property != null) {
-        PipelineElementTemplateVisitor visitor = new PipelineElementTemplateVisitor(getAsMap(values, "staticProperty"));
-        property.accept(visitor);
-      }
+    StaticProperty property = staticPropertyAlternative.getStaticProperty();
+    if (property != null) {
+      PipelineElementTemplateVisitor visitor = new PipelineElementTemplateVisitor(configs);
+      property.accept(visitor);
     }
   }
 
   @Override
   public void visit(StaticPropertyAlternatives staticPropertyAlternatives) {
-    if (hasKey(staticPropertyAlternatives)) {
-      Map<String, Object> values = getAsMap(staticPropertyAlternatives, "alternatives");
-      staticPropertyAlternatives.getAlternatives().forEach((alternative) -> {
-        PipelineElementTemplateVisitor visitor = new PipelineElementTemplateVisitor(values);
-        alternative.accept(visitor);
-      });
+    if (hasConfig(staticPropertyAlternatives)) {
+      Map<String, Object> values = getConfig(staticPropertyAlternatives);
+      var selectedId = getConfigValueAsString(staticPropertyAlternatives);
+      staticPropertyAlternatives
+          .getAlternatives()
+          .stream()
+          .filter(a -> a.getInternalName().equalsIgnoreCase(selectedId))
+          .forEach(a -> {
+            a.setSelected(true);
+            PipelineElementTemplateVisitor visitor = new PipelineElementTemplateVisitor(List.of(values));
+            a.accept(visitor);
+          });
     }
   }
 
   @Override
   public void visit(StaticPropertyGroup staticPropertyGroup) {
-    if (hasKey(staticPropertyGroup)) {
-      Map<String, Object> values = getAsMap(staticPropertyGroup);
-      staticPropertyGroup.getStaticProperties().forEach((group) -> {
-        PipelineElementTemplateVisitor visitor =
-            new PipelineElementTemplateVisitor(getAsMap(values, "staticProperties"));
-        group.accept(visitor);
-      });
-    }
+    staticPropertyGroup.getStaticProperties().forEach(group -> {
+      PipelineElementTemplateVisitor visitor =
+          new PipelineElementTemplateVisitor(configs);
+      group.accept(visitor);
+    });
   }
 
   @Override
   public void visit(SlideToggleStaticProperty slideToggleStaticProperty) {
-    if (hasKey(slideToggleStaticProperty)) {
-      slideToggleStaticProperty.setSelected(getAsBoolean(slideToggleStaticProperty));
+    if (hasConfig(slideToggleStaticProperty)) {
+      slideToggleStaticProperty.setSelected(getConfigValueAsBoolean(slideToggleStaticProperty));
     }
   }
 
   @Override
-  public void visit(RuntimeResolvableTreeInputStaticProperty treeInputStaticProperty) {
-    // TODO support templates for tree input
+  public void visit(RuntimeResolvableTreeInputStaticProperty property) {
+    if (hasConfig(property)) {
+      List<String> values = getConfigValueAsStringList(property);
+      property.setSelectedNodesInternalNames(values);
+    }
   }
 
   @Override
@@ -202,35 +207,111 @@ public class PipelineElementTemplateVisitor implements StaticPropertyVisitor {
     // TODO not yet supported
   }
 
-  private List<String> getValueAsList(StaticProperty sp) {
-    return (List<String>) configs.get(sp.getInternalName());
+
+  private Map<String, Object> getConfig(StaticProperty sp) {
+    return getConfig(sp.getInternalName());
   }
 
-  private boolean hasKey(StaticProperty sp) {
-    return configs.getOrDefault(sp.getInternalName(), null) != null;
+  private Map<String, Object> getConfig(String key) {
+    return configs
+        .stream()
+        .filter(f -> hasKeyCaseInsensitive(key, f))
+        .findFirst()
+        .orElseThrow(() -> new IllegalArgumentException(String.format("No key found: %s", key)));
   }
 
-  private String getAsString(StaticProperty sp) {
-    return configs.get(sp.getInternalName()).toString();
+//  private List<Map<String, Object>> getConfigAsList(StaticProperty sp) {
+//    return getConfig(sp).
+//  }
+
+  private boolean hasKeyCaseInsensitive(String internalName,
+                                        Map<String, Object> templateConfig) {
+    return templateConfig
+        .entrySet()
+        .stream()
+        .anyMatch(entry -> entry.getKey().equalsIgnoreCase(internalName));
   }
 
-  private boolean getAsBoolean(StaticProperty sp) {
-    return Boolean.parseBoolean(configs.get(sp.getInternalName()).toString());
+  private boolean hasConfig(StaticProperty sp) {
+    return configs.stream().anyMatch(c -> hasKeyCaseInsensitive(sp.getInternalName(), c));
   }
 
-  private Map<String, Object> getAsMap(StaticProperty sp) {
-    return (Map<String, Object>) configs.get(sp.getInternalName());
+  private String getConfigValueAsString(StaticProperty sp) {
+    var config = getConfig(sp);
+    if (config.isEmpty()) {
+      throw new IllegalArgumentException(String.format("Could not find config for %s", sp.getInternalName()));
+    } else {
+      var caseInsensitiveKey = getCaseInsensitiveKey(config, sp.getInternalName());
+      return String.valueOf(config.get(caseInsensitiveKey));
+    }
   }
 
-  private Map<String, Object> getAsMap(StaticProperty sp, String subkey) {
-    return (Map<String, Object>) getAsMap(sp).get(subkey);
+  private boolean getConfigValueAsBoolean(StaticProperty sp) {
+    return Boolean.parseBoolean(getConfigValueAsString(sp));
   }
 
-  private Map<String, Object> getAsMap(Map<String, Object> map, String key) {
-    return (Map<String, Object>) map.get(key);
+  private List<String> getConfigValueAsStringList(StaticProperty sp) {
+    return getValueAsStringList(getConfig(sp), sp.getInternalName());
   }
 
-  private List<Map<String, Object>> getAsList(StaticProperty sp, String key) {
-    return (List<Map<String, Object>>) getAsMap(sp).get(key);
+  private String getCaseInsensitiveKey(Map<String, Object> config,
+                                       String key) {
+    return config.keySet().stream()
+        .filter(k -> k.equalsIgnoreCase(key))
+        .findFirst()
+        .orElseThrow(() -> new IllegalArgumentException("Key not found: " + key));
   }
+
+  private List<String> getValueAsStringList(Map<String, Object> config, String key) {
+    String caseInsensitiveKey = getCaseInsensitiveKey(config, key);
+
+    return Optional.ofNullable(config.get(caseInsensitiveKey))
+        .filter(value -> value instanceof List<?>)
+        .map(value -> ((List<?>) value).stream())
+        .map(s -> s.map(String.class::cast).collect(Collectors.toList()))
+        .orElseThrow(() -> new IllegalArgumentException("Value is not a List<String>"));
+  }
+
+  private List<Map<String, Object>> getConfigValueAsList(StaticProperty sp) {
+    return configs
+        .stream()
+        .filter(f -> hasKeyCaseInsensitive(sp.getInternalName(), f))
+        .findFirst()
+        .map(f -> getCaseInsensitiveList(f, sp.getInternalName()))
+        .orElseThrow(IllegalArgumentException::new);
+  }
+
+  private List<Map<String, Object>> getCaseInsensitiveList(Map<String, Object> map, String key) {
+    for (Map.Entry<String, Object> entry : map.entrySet()) {
+      if (entry.getKey().equalsIgnoreCase(key)) {
+        return (List<Map<String, Object>>) entry.getValue();
+      }
+    }
+    throw new IllegalArgumentException("Key '" + key + "' not found");
+  }
+
+
+//  private String getAsString(StaticProperty sp) {
+//    return configs.get(sp.getInternalName()).toString();
+//  }
+//
+//  private boolean getAsBoolean(StaticProperty sp) {
+//    return Boolean.parseBoolean(configs.get(sp.getInternalName()).toString());
+//  }
+//
+//  private Map<String, Object> getAsMap(StaticProperty sp) {
+//    return (Map<String, Object>) configs.get(sp.getInternalName());
+//  }
+//
+//  private Map<String, Object> getAsMap(StaticProperty sp, String subkey) {
+//    return (Map<String, Object>) getAsMap(sp).get(subkey);
+//  }
+//
+//  private Map<String, Object> getAsMap(Map<String, Object> map, String key) {
+//    return (Map<String, Object>) map.get(key);
+//  }
+//
+//  private List<Map<String, Object>> getAsList(StaticProperty sp, String key) {
+//    return (List<Map<String, Object>>) getAsMap(sp).get(key);
+//  }
 }
diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineTemplateManagement.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineTemplateManagement.java
new file mode 100644
index 0000000000..a561c5093b
--- /dev/null
+++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineTemplateManagement.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.manager.template;
+
+import org.apache.streampipes.model.SpDataStream;
+import org.apache.streampipes.model.pipeline.PipelineOperationStatus;
+import org.apache.streampipes.model.template.PipelineTemplateDescription;
+import org.apache.streampipes.model.template.PipelineTemplateInvocation;
+import org.apache.streampipes.storage.management.StorageDispatcher;
+
+import java.util.List;
+import java.util.Optional;
+
+public class PipelineTemplateManagement {
+
+  public PipelineTemplateInvocation prepareInvocation(String streamId,
+                                                      String pipelineTemplateId) {
+    SpDataStream dataStream = getDataStream(streamId);
+
+    var pipelineTemplateDescriptionOpt = getPipelineTemplateDescription(pipelineTemplateId);
+    if (pipelineTemplateDescriptionOpt.isPresent()) {
+      PipelineTemplateInvocation invocation =
+          new PipelineTemplateInvocationGenerator(
+              dataStream,
+              pipelineTemplateDescriptionOpt.get()
+          ).generateInvocation();
+      PipelineTemplateInvocation clonedInvocation = new PipelineTemplateInvocation(invocation);
+      return new PipelineTemplateInvocation(clonedInvocation);
+    } else {
+      throw new IllegalArgumentException(String.format(
+          "Could not find pipeline template with ID %s",
+          pipelineTemplateId)
+      );
+    }
+  }
+
+  public PipelineOperationStatus createAndStartPipeline(PipelineTemplateInvocation pipelineTemplateInvocation,
+                                                        String authenticatedUserSid) {
+    return new PipelineTemplateInvocationHandler(
+        authenticatedUserSid,
+        pipelineTemplateInvocation
+    ).handlePipelineInvocation();
+  }
+
+  private Optional<PipelineTemplateDescription> getPipelineTemplateDescription(String pipelineTemplateId) {
+    return new PipelineTemplateGenerator()
+        .getAllPipelineTemplates()
+        .stream()
+        .filter(pt -> pt.getAppId().equals(pipelineTemplateId))
+        .findFirst();
+  }
+
+  private SpDataStream getDataStream(String streamId) {
+    return getAllDataStreams()
+        .stream()
+        .filter(sp -> sp.getElementId().equals(streamId))
+        .findFirst()
+        .get();
+  }
+
+  private List<SpDataStream> getAllDataStreams() {
+    return StorageDispatcher.INSTANCE.getNoSqlStore().getPipelineElementDescriptionStorage().getAllDataStreams();
+  }
+}
diff --git a/streampipes-platform-services/src/main/java/org/apache/streampipes/ps/PipelineElementTemplateResource.java b/streampipes-platform-services/src/main/java/org/apache/streampipes/ps/PipelineElementTemplateResource.java
index ff3c168b5c..9c939e6cfc 100644
--- a/streampipes-platform-services/src/main/java/org/apache/streampipes/ps/PipelineElementTemplateResource.java
+++ b/streampipes-platform-services/src/main/java/org/apache/streampipes/ps/PipelineElementTemplateResource.java
@@ -71,7 +71,7 @@ public class PipelineElementTemplateResource extends AbstractRestResource {
     }
   }
 
-  @GetMapping(path = "{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+  @GetMapping(path = "{id}", produces = { MediaType.APPLICATION_JSON_VALUE, "application/yaml" })
   @Operation(summary = "Get a single pipeline element template by a given id",
              tags = {"Pipeline Element Templates"},
              responses = {
diff --git a/streampipes-rest-core-base/pom.xml b/streampipes-rest-core-base/pom.xml
index 229b706d78..a3296d9638 100644
--- a/streampipes-rest-core-base/pom.xml
+++ b/streampipes-rest-core-base/pom.xml
@@ -48,6 +48,10 @@
             <artifactId>streampipes-pipeline-management</artifactId>
             <version>0.97.0-SNAPSHOT</version>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.dataformat</groupId>
+            <artifactId>jackson-dataformat-yaml</artifactId>
+        </dependency>
     </dependencies>
     <build>
         <plugins>
diff --git a/streampipes-rest-core-base/src/main/java/org/apache/streampipes/rest/core/base/impl/converter/YamlConfiguration.java b/streampipes-rest-core-base/src/main/java/org/apache/streampipes/rest/core/base/impl/converter/YamlConfiguration.java
new file mode 100644
index 0000000000..3dcafa7b33
--- /dev/null
+++ b/streampipes-rest-core-base/src/main/java/org/apache/streampipes/rest/core/base/impl/converter/YamlConfiguration.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.rest.core.base.impl.converter;
+
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.List;
+
+@Configuration
+public class YamlConfiguration implements WebMvcConfigurer {
+  private static final MediaType MEDIA_TYPE_YAML = MediaType.valueOf("application/yaml");
+  private static final MediaType MEDIA_TYPE_YML = MediaType.valueOf("application/yml");
+
+  @Override
+  public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
+    configurer
+        .mediaType(MEDIA_TYPE_YML.getSubtype(), MEDIA_TYPE_YML)
+        .mediaType(MEDIA_TYPE_YAML.getSubtype(), MEDIA_TYPE_YAML);
+  }
+
+  @Override
+  public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
+    YAMLMapper yamlMapper = new YAMLMapper();
+
+    MappingJackson2HttpMessageConverter yamlConverter = new MappingJackson2HttpMessageConverter(yamlMapper);
+    yamlConverter.setSupportedMediaTypes(
+        List.of(
+            MEDIA_TYPE_YML,
+            MEDIA_TYPE_YAML
+        )
+    );
+    converters.add(yamlConverter);
+  }
+}
diff --git a/streampipes-rest/pom.xml b/streampipes-rest/pom.xml
index aac6c5cf55..43f2d74594 100644
--- a/streampipes-rest/pom.xml
+++ b/streampipes-rest/pom.xml
@@ -101,6 +101,10 @@
             <groupId>io.swagger.core.v3</groupId>
             <artifactId>swagger-annotations-jakarta</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.dataformat</groupId>
+            <artifactId>jackson-dataformat-yaml</artifactId>
+        </dependency>
 
         <!-- Test dependencies -->
         <dependency>
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/GenericStorageResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/GenericStorageResource.java
index 9f74d72056..61bcfd7cb9 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/GenericStorageResource.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/GenericStorageResource.java
@@ -83,7 +83,7 @@ public class GenericStorageResource extends AbstractAuthGuardedRestResource {
       path = "/{appDocName}/find",
       produces = MediaType.APPLICATION_JSON_VALUE,
       consumes = MediaType.APPLICATION_JSON_VALUE)
-  @PreAuthorize(AuthConstants.HAS_WRITE_GENERIC_STORAGE_PRIVILEGE)
+  @PreAuthorize(AuthConstants.HAS_READ_GENERIC_STORAGE_PRIVILEGE)
   public ResponseEntity<List<Map<String, Object>>> find(@PathVariable(APP_DOC_NAME) String appDocName,
                                                         @RequestBody Map<String, Object> query) {
     try {
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineTemplate.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineTemplate.java
index 5957fe01e2..ee041f5232 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineTemplate.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineTemplate.java
@@ -17,14 +17,11 @@
  */
 package org.apache.streampipes.rest.impl;
 
-import org.apache.streampipes.manager.template.PipelineTemplateGenerator;
-import org.apache.streampipes.manager.template.PipelineTemplateInvocationGenerator;
-import org.apache.streampipes.manager.template.PipelineTemplateInvocationHandler;
+import org.apache.streampipes.manager.template.PipelineTemplateManagement;
 import org.apache.streampipes.model.SpDataStream;
 import org.apache.streampipes.model.SpDataStreamContainer;
 import org.apache.streampipes.model.message.Notifications;
 import org.apache.streampipes.model.pipeline.PipelineOperationStatus;
-import org.apache.streampipes.model.template.PipelineTemplateDescription;
 import org.apache.streampipes.model.template.PipelineTemplateInvocation;
 import org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource;
 import org.apache.streampipes.rest.shared.exception.SpMessageException;
@@ -41,12 +38,17 @@ import org.springframework.web.bind.annotation.RestController;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Optional;
 
 @RestController
 @RequestMapping("/api/v2/pipeline-templates")
 public class PipelineTemplate extends AbstractAuthGuardedRestResource {
 
+  private final PipelineTemplateManagement pipelineTemplateManagement;
+
+  public PipelineTemplate() {
+    this.pipelineTemplateManagement = new PipelineTemplateManagement();
+  }
+
   @GetMapping(path = "/streams", produces = MediaType.APPLICATION_JSON_VALUE)
   public ResponseEntity<SpDataStreamContainer> getAvailableDataStreams() {
     List<SpDataStream> sources = getPipelineElementRdfStorage().getAllDataStreams();
@@ -65,17 +67,9 @@ public class PipelineTemplate extends AbstractAuthGuardedRestResource {
   public ResponseEntity<PipelineTemplateInvocation> getPipelineTemplateInvocation(
       @RequestParam(value = "streamId", required = false) String streamId,
       @RequestParam(value = "templateId") String pipelineTemplateId) {
-    SpDataStream dataStream = getDataStream(streamId);
-    var pipelineTemplateDescriptionOpt = getPipelineTemplateDescription(pipelineTemplateId);
-    if (pipelineTemplateDescriptionOpt.isPresent()) {
-      PipelineTemplateInvocation invocation =
-          new PipelineTemplateInvocationGenerator(
-              dataStream,
-              pipelineTemplateDescriptionOpt.get()
-          ).generateInvocation();
-      PipelineTemplateInvocation clonedInvocation = new PipelineTemplateInvocation(invocation);
-      return ok(new PipelineTemplateInvocation(clonedInvocation));
-    } else {
+    try {
+      return ok(pipelineTemplateManagement.prepareInvocation(streamId, pipelineTemplateId));
+    } catch (IllegalArgumentException e) {
       throw new SpMessageException(HttpStatus.BAD_REQUEST, Notifications.error(
           String.format(
               "Could not create pipeline template %s - did you install all pipeline elements?",
@@ -90,30 +84,10 @@ public class PipelineTemplate extends AbstractAuthGuardedRestResource {
   public ResponseEntity<PipelineOperationStatus> generatePipeline(
       @RequestBody PipelineTemplateInvocation pipelineTemplateInvocation) {
 
-    PipelineOperationStatus status = new PipelineTemplateInvocationHandler(
-        getAuthenticatedUserSid(),
-        pipelineTemplateInvocation
-    ).handlePipelineInvocation();
+    var status = pipelineTemplateManagement.createAndStartPipeline(
+        pipelineTemplateInvocation,
+        getAuthenticatedUserSid()
+    );
     return ok(status);
   }
-
-  private Optional<PipelineTemplateDescription> getPipelineTemplateDescription(String pipelineTemplateId) {
-    return new PipelineTemplateGenerator()
-        .getAllPipelineTemplates()
-        .stream()
-        .filter(pt -> pt.getAppId().equals(pipelineTemplateId))
-        .findFirst();
-  }
-
-  private List<SpDataStream> getAllDataStreams() {
-    return getPipelineElementRdfStorage().getAllDataStreams();
-  }
-
-  private SpDataStream getDataStream(String streamId) {
-    return getAllDataStreams()
-        .stream()
-        .filter(sp -> sp.getElementId().equals(streamId))
-        .findFirst()
-        .get();
-  }
 }
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AdapterResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AdapterResource.java
index 086c2050e8..0961a8c5c4 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AdapterResource.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AdapterResource.java
@@ -30,6 +30,7 @@ import org.apache.streampipes.model.connect.adapter.PipelineUpdateInfo;
 import org.apache.streampipes.model.message.Message;
 import org.apache.streampipes.model.message.Notifications;
 import org.apache.streampipes.model.monitoring.SpLogMessage;
+import org.apache.streampipes.model.util.ElementIdGenerator;
 import org.apache.streampipes.resource.management.PermissionResourceManager;
 import org.apache.streampipes.resource.management.SpResourceManager;
 import org.apache.streampipes.rest.security.AuthConstants;
@@ -78,12 +79,13 @@ public class AdapterResource extends AbstractAdapterResource<AdapterMasterManage
     var principalSid = getAuthenticatedUserSid();
     var username = getAuthenticatedUsername();
     String adapterId;
-    LOG.info("User: " + username + " starts adapter " + adapterDescription.getElementId());
+    LOG.info("User: {} starts adapter {}", username, adapterDescription.getElementId());
 
     try {
-      adapterId = managementService.addAdapter(adapterDescription, principalSid);
+      adapterId = ElementIdGenerator.makeElementId(adapterDescription);
+      managementService.addAdapter(adapterDescription, adapterId, principalSid);
     } catch (AdapterException e) {
-      LOG.error("Error while starting adapter with id " + adapterDescription.getAppId(), e);
+      LOG.error("Error while starting adapter with id {}", adapterDescription.getAppId(), e);
       return ok(Notifications.error(e.getMessage()));
     }
 
@@ -91,36 +93,33 @@ public class AdapterResource extends AbstractAdapterResource<AdapterMasterManage
     return ok(Notifications.success(adapterId));
   }
 
-  @PutMapping(
-      produces = MediaType.APPLICATION_JSON_VALUE,
-      consumes = MediaType.APPLICATION_JSON_VALUE)
+  @PutMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
   @PreAuthorize(AuthConstants.HAS_WRITE_ADAPTER_PRIVILEGE)
   public ResponseEntity<? extends Message> updateAdapter(@RequestBody AdapterDescription adapterDescription) {
     var updateManager = new AdapterUpdateManagement(managementService);
     try {
       updateManager.updateAdapter(adapterDescription);
     } catch (AdapterException e) {
-      LOG.error("Error while updating adapter with id " + adapterDescription.getElementId(), e);
+      LOG.error("Error while updating adapter with id {}", adapterDescription.getElementId(), e);
       return ok(Notifications.error(e.getMessage()));
     }
 
     return ok(Notifications.success(adapterDescription.getElementId()));
   }
 
-  @PutMapping(
-      path = "pipeline-migration-preflight",
-      consumes = MediaType.APPLICATION_JSON_VALUE,
-      produces = MediaType.APPLICATION_JSON_VALUE)
+  @PutMapping(path = "pipeline-migration-preflight", consumes = MediaType.APPLICATION_JSON_VALUE,
+              produces = MediaType.APPLICATION_JSON_VALUE)
   @PreAuthorize(AuthConstants.HAS_WRITE_ADAPTER_PRIVILEGE)
   public ResponseEntity<List<PipelineUpdateInfo>> performPipelineMigrationPreflight(
-      @RequestBody AdapterDescription adapterDescription) {
+      @RequestBody AdapterDescription adapterDescription
+  ) {
     var updateManager = new AdapterUpdateManagement(managementService);
     var migrations = updateManager.checkPipelineMigrations(adapterDescription);
 
     return ok(migrations);
   }
 
-  @GetMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+  @GetMapping(path = "/{id}", produces = { MediaType.APPLICATION_JSON_VALUE, "application/yaml" })
   @PreAuthorize(AuthConstants.HAS_READ_ADAPTER_PRIVILEGE)
   public ResponseEntity<?> getAdapter(@PathVariable("id") String adapterId) {
 
@@ -129,7 +128,7 @@ public class AdapterResource extends AbstractAdapterResource<AdapterMasterManage
 
       return ok(adapterDescription);
     } catch (AdapterException e) {
-      LOG.error("Error while getting adapter with id " + adapterId, e);
+      LOG.error("Error while getting adapter with id {}", adapterId, e);
       return fail();
     }
   }
@@ -141,7 +140,7 @@ public class AdapterResource extends AbstractAdapterResource<AdapterMasterManage
       managementService.stopStreamAdapter(adapterId);
       return ok(Notifications.success("Adapter started"));
     } catch (AdapterException e) {
-      LOG.error("Could not stop adapter with id " + adapterId, e);
+      LOG.error("Could not stop adapter with id {}", adapterId, e);
       return serverError(SpLogMessage.from(e));
     }
   }
@@ -153,7 +152,7 @@ public class AdapterResource extends AbstractAdapterResource<AdapterMasterManage
       managementService.startStreamAdapter(adapterId);
       return ok(Notifications.success("Adapter stopped"));
     } catch (AdapterException e) {
-      LOG.error("Could not start adapter with id " + adapterId, e);
+      LOG.error("Could not start adapter with id {}", adapterId, e);
       return serverError(SpLogMessage.from(e));
     }
   }
@@ -164,22 +163,26 @@ public class AdapterResource extends AbstractAdapterResource<AdapterMasterManage
                                          @RequestParam(value = "deleteAssociatedPipelines", defaultValue = "false")
                                          boolean deleteAssociatedPipelines) {
     List<String> pipelinesUsingAdapter = getPipelinesUsingAdapter(elementId);
-    IPipelineStorage pipelineStorageAPI = StorageDispatcher.INSTANCE.getNoSqlStore().getPipelineStorageAPI();
+    IPipelineStorage pipelineStorageAPI = StorageDispatcher.INSTANCE.getNoSqlStore()
+                                                                    .getPipelineStorageAPI();
 
     if (pipelinesUsingAdapter.isEmpty()) {
       try {
         managementService.deleteAdapter(elementId);
         return ok(Notifications.success("Adapter with id: " + elementId + " is deleted."));
       } catch (AdapterException e) {
-        LOG.error("Error while deleting adapter with id " + elementId, e);
+        LOG.error("Error while deleting adapter with id {}", elementId, e);
         return ok(Notifications.error(e.getMessage()));
       }
     } else if (!deleteAssociatedPipelines) {
-      List<String> namesOfPipelinesUsingAdapter =
-          pipelinesUsingAdapter.stream().map(pipelineId -> pipelineStorageAPI.getElementById(pipelineId).getName())
-              .collect(
-                  Collectors.toList());
-      return ResponseEntity.status(HttpStatus.SC_CONFLICT).body(String.join(", ", namesOfPipelinesUsingAdapter));
+      List<String> namesOfPipelinesUsingAdapter = pipelinesUsingAdapter
+          .stream()
+          .map(pipelineId -> pipelineStorageAPI.getElementById(
+                                                   pipelineId)
+                                               .getName())
+          .collect(Collectors.toList());
+      return ResponseEntity.status(HttpStatus.SC_CONFLICT)
+                           .body(String.join(", ", namesOfPipelinesUsingAdapter));
     } else {
       PermissionResourceManager permissionResourceManager = new PermissionResourceManager();
       // find out the names of pipelines that have an owner and the owner is not the current user
@@ -200,15 +203,17 @@ public class AdapterResource extends AbstractAdapterResource<AdapterMasterManage
             PipelineManager.deletePipeline(pipelineId);
           }
           managementService.deleteAdapter(elementId);
-          return ok(Notifications.success(
-              "Adapter with id: " + elementId + " and all pipelines using the adapter are deleted."));
+          return ok(Notifications.success("Adapter with id: " + elementId
+                                              + " and all pipelines using the adapter are deleted."));
         } catch (Exception e) {
-          LOG.error("Error while deleting adapter with id " + elementId + " and all pipelines using the adapter", e);
+          LOG.error("Error while deleting adapter with id "
+                        + elementId + " and all pipelines using the adapter", e);
           return ok(Notifications.error(e.getMessage()));
         }
       } else {
         // otherwise, hint the user the names of pipelines using the adapter but not owned by the user
-        return ResponseEntity.status(HttpStatus.SC_CONFLICT).body(String.join(", ", namesOfPipelinesNotOwnedByUser));
+        return ResponseEntity.status(HttpStatus.SC_CONFLICT)
+                             .body(String.join(", ", namesOfPipelinesNotOwnedByUser));
       }
     }
   }
@@ -220,7 +225,8 @@ public class AdapterResource extends AbstractAdapterResource<AdapterMasterManage
       return ok(managementService.getAllAdapterInstances());
     } catch (AdapterException e) {
       LOG.error("Error while getting all adapters", e);
-      return ResponseEntity.status(500).build();
+      return ResponseEntity.status(500)
+                           .build();
     }
   }
 
@@ -229,10 +235,9 @@ public class AdapterResource extends AbstractAdapterResource<AdapterMasterManage
   }
 
   private List<String> getPipelinesUsingAdapter(String adapterId) {
-    return StorageDispatcher.INSTANCE
-        .getNoSqlStore()
-        .getPipelineStorageAPI()
-        .getPipelinesUsingAdapter(adapterId);
+    return StorageDispatcher.INSTANCE.getNoSqlStore()
+                                     .getPipelineStorageAPI()
+                                     .getPipelinesUsingAdapter(adapterId);
   }
 
 }
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/CompactAdapterResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/CompactAdapterResource.java
new file mode 100644
index 0000000000..da76a0eff3
--- /dev/null
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/CompactAdapterResource.java
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.rest.impl.connect;
+
+import org.apache.streampipes.commons.exceptions.connect.AdapterException;
+import org.apache.streampipes.commons.prometheus.adapter.AdapterMetricsManager;
+import org.apache.streampipes.connect.management.compact.AdapterGenerationSteps;
+import org.apache.streampipes.connect.management.compact.PersistPipelineHandler;
+import org.apache.streampipes.connect.management.management.AdapterMasterManagement;
+import org.apache.streampipes.connect.management.management.AdapterUpdateManagement;
+import org.apache.streampipes.connect.management.management.CompactAdapterManagement;
+import org.apache.streampipes.manager.template.PipelineTemplateManagement;
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.connect.adapter.compact.CompactAdapter;
+import org.apache.streampipes.model.message.Notifications;
+import org.apache.streampipes.resource.management.SpResourceManager;
+import org.apache.streampipes.rest.security.AuthConstants;
+import org.apache.streampipes.rest.shared.exception.BadRequestException;
+import org.apache.streampipes.storage.management.StorageDispatcher;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/v2/connect/compact-adapters")
+public class CompactAdapterResource extends AbstractAdapterResource<AdapterMasterManagement> {
+
+  private static final Logger LOG = LoggerFactory.getLogger(CompactAdapterResource.class);
+  private final AdapterGenerationSteps adapterGenerationSteps;
+  private final AdapterUpdateManagement adapterUpdateManagement;
+
+  public CompactAdapterResource() {
+    super(() -> new AdapterMasterManagement(
+        StorageDispatcher.INSTANCE.getNoSqlStore()
+            .getAdapterInstanceStorage(),
+        new SpResourceManager().manageAdapters(),
+        new SpResourceManager().manageDataStreams(),
+        AdapterMetricsManager.INSTANCE.getAdapterMetrics()
+    ));
+    this.adapterGenerationSteps = new AdapterGenerationSteps();
+    this.adapterUpdateManagement = new AdapterUpdateManagement(managementService);
+  }
+
+  @PostMapping(
+      consumes = {
+          MediaType.APPLICATION_JSON_VALUE,
+          "application/yaml",
+          "application/yml"
+      }
+  )
+  @PreAuthorize(AuthConstants.HAS_WRITE_ADAPTER_PRIVILEGE)
+  public ResponseEntity<?> addAdapterCompact(
+      @RequestBody CompactAdapter compactAdapter
+  ) throws Exception {
+
+    var adapterDescription = getGeneratedAdapterDescription(compactAdapter);
+    var principalSid = getAuthenticatedUserSid();
+
+    try {
+      var adapterId = adapterDescription.getElementId();
+      managementService.addAdapter(adapterDescription, adapterId, principalSid);
+      if (compactAdapter.createOptions() != null) {
+        if (compactAdapter.createOptions().persist()) {
+          var storedAdapter = managementService.getAdapter(adapterId);
+          var status = new PersistPipelineHandler(
+              new PipelineTemplateManagement(),
+              getAuthenticatedUserSid()
+          ).createAndStartPersistPipeline(storedAdapter);
+        }
+        if (compactAdapter.createOptions().start()) {
+          managementService.startStreamAdapter(adapterId);
+        }
+      }
+      return ok(Notifications.success(adapterId));
+    } catch (AdapterException e) {
+      LOG.error("Error while starting adapter with id {}", adapterDescription.getAppId(), e);
+      return ok(Notifications.error(e.getMessage()));
+    }
+  }
+
+  @PutMapping(
+      path = "{id}",
+      consumes = {
+          MediaType.APPLICATION_JSON_VALUE,
+          "application/yaml",
+          "application/yml"
+      }
+  )
+  @PreAuthorize(AuthConstants.HAS_WRITE_ADAPTER_PRIVILEGE)
+  public ResponseEntity<?> updateAdapterCompact(
+      @PathVariable("id") String elementId,
+      @RequestBody CompactAdapter compactAdapter
+  ) throws Exception {
+
+    var existingAdapter = managementService.getAdapter(elementId);
+    if (existingAdapter != null) {
+      var adapterDescription = getGeneratedAdapterDescription(compactAdapter, existingAdapter);
+
+      try {
+        adapterUpdateManagement.updateAdapter(adapterDescription);
+      } catch (AdapterException e) {
+        LOG.error("Error while updating adapter with id {}", adapterDescription.getElementId(), e);
+        return ok(Notifications.error(e.getMessage()));
+      }
+
+      return ok(Notifications.success(adapterDescription.getElementId()));
+    } else {
+      throw new BadRequestException(String.format("Adapter with id %s not found", elementId));
+    }
+  }
+
+  private AdapterDescription getGeneratedAdapterDescription(CompactAdapter compactAdapter) throws Exception {
+    var generators = adapterGenerationSteps.getGenerators();
+    return new CompactAdapterManagement(generators).convertToAdapterDescription(compactAdapter);
+  }
+
+  private AdapterDescription getGeneratedAdapterDescription(CompactAdapter compactAdapter,
+                                                            AdapterDescription existingAdapter) throws Exception {
+    var generators = adapterGenerationSteps.getGenerators();
+    return new CompactAdapterManagement(generators).convertToAdapterDescription(compactAdapter, existingAdapter);
+  }
+}
diff --git a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/utils/Utils.java b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/utils/Utils.java
index 0e4ece90c5..65a3d10c53 100644
--- a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/utils/Utils.java
+++ b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/utils/Utils.java
@@ -55,7 +55,7 @@ public class Utils {
   }
 
   public static CouchDbClient getCouchDbPipelineElementTemplateClient() {
-    return getCouchDbGsonClient("pipelineelementtemplate");
+    return getCouchDbGsonClient("pipelineelementtemplate-v2");
   }
 
   public static CouchDbClient getCouchDbPipelineCanvasMetadataClient() {
diff --git a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authorization/RoleManager.java b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authorization/RoleManager.java
index 4b6e9b5b08..57aeed142d 100644
--- a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authorization/RoleManager.java
+++ b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authorization/RoleManager.java
@@ -42,7 +42,8 @@ public class RoleManager {
         Role.createDefaultRole(DefaultRole.Constants.ROLE_SERVICE_ADMIN_VALUE, "Service Admin", List.of()),
         Role.createDefaultRole(DefaultRole.Constants.ROLE_DASHBOARD_ADMIN_VALUE, "Dashboard Admin", List.of(
             DefaultPrivilege.Constants.PRIVILEGE_READ_DASHBOARD_VALUE,
-            DefaultPrivilege.Constants.PRIVILEGE_WRITE_DASHBOARD_VALUE
+            DefaultPrivilege.Constants.PRIVILEGE_WRITE_DASHBOARD_VALUE,
+            DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_VALUE
         )),
         Role.createDefaultRole(DefaultRole.Constants.ROLE_DASHBOARD_USER_VALUE, "Dashboard User", List.of(
             DefaultPrivilege.Constants.PRIVILEGE_READ_DASHBOARD_VALUE,
@@ -52,11 +53,14 @@ public class RoleManager {
         Role.createDefaultRole(DefaultRole.Constants.ROLE_PIPELINE_ADMIN_VALUE, "Pipeline Admin", List.of(
             DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_VALUE,
             DefaultPrivilege.Constants.PRIVILEGE_WRITE_PIPELINE_VALUE,
-            DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_ELEMENT_VALUE
+            DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_ELEMENT_VALUE,
+            DefaultPrivilege.Constants.PRIVILEGE_READ_FILES_VALUE,
+            DefaultPrivilege.Constants.PRIVILEGE_WRITE_FILES_VALUE
         )),
         Role.createDefaultRole(DefaultRole.Constants.ROLE_PIPELINE_USER_VALUE, "Pipeline User", List.of(
             DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_VALUE,
-            DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_ELEMENT_VALUE
+            DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_ELEMENT_VALUE,
+            DefaultPrivilege.Constants.PRIVILEGE_READ_FILES_VALUE
         )),
         Role.createDefaultRole(DefaultRole.Constants.ROLE_ASSET_ADMIN_VALUE, "Asset Admin", List.of(
             DefaultPrivilege.Constants.PRIVILEGE_READ_ASSETS_VALUE,
@@ -83,7 +87,9 @@ public class RoleManager {
         )),
         Role.createDefaultRole(DefaultRole.Constants.ROLE_CONNECT_ADMIN_VALUE, "Connect Admin", List.of(
             DefaultPrivilege.Constants.PRIVILEGE_WRITE_ADAPTER_VALUE,
-            DefaultPrivilege.Constants.PRIVILEGE_READ_ADAPTER_VALUE
+            DefaultPrivilege.Constants.PRIVILEGE_READ_ADAPTER_VALUE,
+            DefaultPrivilege.Constants.PRIVILEGE_READ_FILES_VALUE,
+            DefaultPrivilege.Constants.PRIVILEGE_WRITE_FILES_VALUE
         ))
     );
   }
diff --git a/ui/cypress/support/utils/FileManagementUtils.ts b/ui/cypress/support/utils/FileManagementUtils.ts
index fab66c9277..d35c383ff9 100644
--- a/ui/cypress/support/utils/FileManagementUtils.ts
+++ b/ui/cypress/support/utils/FileManagementUtils.ts
@@ -19,7 +19,7 @@
 export class FileManagementUtils {
     public static addFile(filePath: string) {
         // Go to StreamPipes file management
-        cy.visit('#/files');
+        cy.visit('#/configuration/files');
 
         // Open file upload dialog
         cy.dataCy('sp-open-file-upload-dialog').click();
@@ -32,7 +32,7 @@ export class FileManagementUtils {
 
     public static deleteFile() {
         // Go to StreamPipes file management
-        cy.visit('#/files');
+        cy.visit('#/configuration/files');
         // Check if file was uploaded and delete it
         cy.dataCy('delete').should('have.length', 1);
         cy.dataCy('delete').click();
@@ -44,7 +44,7 @@ export class FileManagementUtils {
         const downloadsFolder = Cypress.config('downloadsFolder');
 
         // Go to StreamPipes file management
-        cy.visit('#/files');
+        cy.visit('#/configuration/files');
         // Check if file was uploaded
         cy.dataCy('download').should('have.length', 1);
 
diff --git a/ui/deployment/app-routing.module.mst b/ui/deployment/app-routing.module.mst
index a09b0076f7..5ee435c816 100644
--- a/ui/deployment/app-routing.module.mst
+++ b/ui/deployment/app-routing.module.mst
@@ -39,6 +39,7 @@ import { RegistrationAllowedCanActivateGuard } from './_guards/registration-allo
 import { RestorePasswordAllowedCanActivateGuard } from './_guards/restore-password-allowed.can-activate.guard';
 import { SetNewPasswordComponent } from './login/components/set-new-password/set-new-password.component';
 import { ActivateAccountComponent } from './login/components/activate-account/activate-account.component';
+import { UserPrivilege } from './_enums/user-privilege.enum';
 
 {{#modulesActive}}
 {{#componentImport}}
@@ -60,10 +61,10 @@ const routes: Routes = [
       { path: '', component: HomeComponent, canActivate: [ConfiguredCanActivateGuard] },
       {{#modulesActive}}
       {{#componentImport}}
-      { path: '{{{link}}}', component: {{{component}}}, data: { authPageNames: [{{{pageNames}}}]}},
+      { path: '{{{link}}}', component: {{{component}}}, data: { privileges: {{{privileges}}}, authPageNames: [{{{pageNames}}}]}},
       {{/componentImport}}
       {{^componentImport}}
-      { path: '{{{link}}}', data: { authPageNames: [{{{pageNames}}}]}, loadChildren: () => import('{{{path}}}').then(m => m.{{{moduleName}}})},
+      { path: '{{{link}}}', data: { privileges: {{{privileges}}}, authPageNames: [{{{pageNames}}}]}, loadChildren: () => import('{{{path}}}').then(m => m.{{{moduleName}}})},
       {{/componentImport}}
       {{/modulesActive}}
 
diff --git a/ui/deployment/base-navigation.component.mst b/ui/deployment/base-navigation.component.mst
index 74f30ff3e8..2c82f29916 100644
--- a/ui/deployment/base-navigation.component.mst
+++ b/ui/deployment/base-navigation.component.mst
@@ -22,6 +22,7 @@ import { PageName } from '../../_enums/page-name.enum';
 import { AuthService } from '../../services/auth.service';
 import { CurrentUserService } from '@streampipes/shared-ui';
 import { AppConstants } from '../../services/app.constants';
+import { UserPrivilege } from '../../_enums/user-privilege.enum';
 
 export abstract class BaseNavigationComponent {
 
@@ -36,32 +37,18 @@ export abstract class BaseNavigationComponent {
       title: 'Home',
       icon: 'home',
       pagesNames: [PageName.HOME],
+      privileges: [],
       visible: false
     },
     {{#modulesActive}}
-    {{^admin}}
     {
         link: '{{{link}}}',
         title: '{{{title}}}',
         icon: '{{{icon}}}',
         pageNames: [{{{pageNames}}}],
+        privileges: {{{privileges}}},
         visible: false
     },
-    {{/admin}}
-    {{/modulesActive}}
-  ];
-
-  admin = [
-    {{#modulesActive}}
-    {{#admin}}
-    {
-        link: '{{{link}}}',
-        title: '{{{title}}}',
-        icon: '{{{icon}}}',
-        pageNames: [{{{pageNames}}}],
-        visible: false
-    },
-    {{/admin}}
     {{/modulesActive}}
   ];
 
@@ -77,9 +64,11 @@ export abstract class BaseNavigationComponent {
 
     onInit() {
       this.currentUserService.user$.subscribe(user => {
-        this.menu.forEach(m => m.visible = this.isNavItemVisible(m.pageNames));
-        this.admin.forEach(m => m.visible = this.isNavItemVisible(m.pageNames));
-        this.notificationsVisible = this.isNavItemVisible([PageName.NOTIFICATIONS]);
+        this.menu.forEach(m => m.visible = this.isNavItemVisible(m.privileges));
+        this.notificationsVisible = this.isNavItemVisible([
+        UserPrivilege.PRIVILEGE_READ_PIPELINE,
+        UserPrivilege.PRIVILEGE_WRITE_PIPELINE
+        ]);
       });
       this.activePage = this.router.url.replace('/', '');
       this.activePageName = this.getPageTitle(this.activePage);
@@ -96,9 +85,8 @@ export abstract class BaseNavigationComponent {
     }
 
     getPageTitle(path) {
-      const allMenuItems = this.menu.concat(this.admin);
       let currentTitle = this.appConstants.APP_NAME;
-      allMenuItems.forEach(m => {
+      this.menu.forEach(m => {
         if (m.link === path) {
           currentTitle = m.title;
         }
@@ -120,8 +108,8 @@ export abstract class BaseNavigationComponent {
       this.activePageName = this.getPageTitle(this.activePage);
     }
 
-    public isNavItemVisible(pageNames?: PageName[]): boolean {
-      return this.authService.isAnyAccessGranted(pageNames);
+    public isNavItemVisible(privileges: string[]): boolean {
+      return this.authService.isAnyAccessGranted(privileges);
     }
 
 }
diff --git a/ui/deployment/dev/config.yml b/ui/deployment/dev/config.yml
index 19dd9ae045..de2530292a 100644
--- a/ui/deployment/dev/config.yml
+++ b/ui/deployment/dev/config.yml
@@ -24,5 +24,4 @@ modules:
     - spDashboard
     - spDataExplorer
     - spAssets
-    - spFiles
     - spConfiguration
diff --git a/ui/deployment/home.service.mst b/ui/deployment/home.service.mst
index 9a6a6de7a9..6d79e7631a 100644
--- a/ui/deployment/home.service.mst
+++ b/ui/deployment/home.service.mst
@@ -26,7 +26,7 @@ import {
     DataViewDataExplorerService, PipelineElementService,
     PipelineService
 } from '@streampipes/platform-services';
-import { UserRole } from '../_enums/user-role.enum';
+import { UserPrivilege } from '../_enums/user-privilege.enum';
 
 @Injectable({providedIn: 'root'})
 export class HomeService {
@@ -40,7 +40,7 @@ export class HomeService {
     }
 
     getFilteredServiceLinks() {
-        return this.getServiceLinks().filter(s => this.authService.isAnyAccessGranted(s.pageNames));
+        return this.getServiceLinks().filter(s => this.authService.isAnyAccessGranted(s.privileges));
     }
 
     getServiceLinks(): ServiceLink[] {
@@ -51,6 +51,7 @@ export class HomeService {
                description: "{{description}}",
                icon: "{{{icon}}}",
                pageNames: [{{{pageNames}}}],
+               privileges: {{{privileges}}},
                link: {
                    newWindow: false,
                    value: "{{{link}}}"
@@ -74,6 +75,10 @@ export class HomeService {
                description: "The notification module lets you view all notifications generated by pipelines.",
                icon: "chat",
                pageNames: [PageName.NOTIFICATIONS],
+               privileges: [
+                UserPrivilege.PRIVILEGE_READ_PIPELINE,
+                UserPrivilege.PRIVILEGE_WRITE_PIPELINE
+               ],
                link: {
                    newWindow: false,
                    value: "notifications"
diff --git a/ui/deployment/modules.yml b/ui/deployment/modules.yml
index e9e37891f3..44bf959acf 100644
--- a/ui/deployment/modules.yml
+++ b/ui/deployment/modules.yml
@@ -24,7 +24,7 @@ spAssets:
     title: 'Asset Management'
     description: 'Manage assets which help assigning pipelines, dashboards and adapters to real-world objects such as machines and plants.'
     icon: 'precision_manufacturing'
-    admin: true
+    privileges: '[UserPrivilege.PRIVILEGE_READ_ASSETS, UserPrivilege.PRIVILEGE_WRITE_ASSETS]'
     pageNames: 'PageName.ASSETS'
     showStatusBox: false
 spConnect:
@@ -38,7 +38,7 @@ spConnect:
     title: 'Connect'
     description: 'StreamPipes Connect lets you easily connect new data sources for a variety of data formats and protocols.'
     icon: 'power'
-    admin: False
+    privileges: '[UserPrivilege.PRIVILEGE_READ_ADAPTER, UserPrivilege.PRIVILEGE_WRITE_ADAPTER]'
     pageNames: 'PageName.CONNECT'
     showStatusBox: true
     statusBox:
@@ -47,8 +47,8 @@ spConnect:
         title: 'Adapters'
         createTitle: 'New adapter'
         dataFns: '[this.adapterService.getAdapters()]'
-        viewRoles: '[UserRole.ROLE_CONNECT_ADMIN]'
-        createRoles: '[UserRole.ROLE_CONNECT_ADMIN]'
+        viewRoles: '[UserPrivilege.PRIVILEGE_READ_ADAPTER, UserPrivilege.PRIVILEGE_WRITE_ADAPTER]'
+        createRoles: '[UserPrivilege.PRIVILEGE_WRITE_ADAPTER]'
 spPipelines:
     componentImport: False
     moduleName: 'PipelinesModule'
@@ -60,8 +60,8 @@ spPipelines:
     title: 'Pipelines'
     description: 'All created pipelines are available in the pipeline view, where you can also start and stop pipelines.'
     icon: 'play_arrow'
-    admin: False
     pageNames: 'PageName.PIPELINE_OVERVIEW'
+    privileges: '[UserPrivilege.PRIVILEGE_READ_PIPELINE, UserPrivilege.PRIVILEGE_WRITE_PIPELINE]'
     showStatusBox: true
     statusBox:
         link: "['pipelines']"
@@ -69,8 +69,8 @@ spPipelines:
         title: 'Pipelines'
         createTitle: 'New pipeline'
         dataFns: '[this.pipelineService.getPipelines()]'
-        viewRoles: '[UserRole.ROLE_PIPELINE_ADMIN, UserRole.ROLE_PIPELINE_USER]'
-        createRoles: '[UserRole.ROLE_PIPELINE_ADMIN]'
+        viewRoles: '[UserPrivilege.PRIVILEGE_READ_PIPELINE, UserPrivilege.PRIVILEGE_WRITE_PIPELINE]'
+        createRoles: '[UserPrivilege.PRIVILEGE_WRITE_PIPELINE]'
 spConfiguration:
     componentImport: False
     moduleName: 'ConfigurationModule'
@@ -83,7 +83,7 @@ spConfiguration:
     description: 'In the configuration module, basic StreamPipes settings and services can be configured.'
     icon: 'settings'
     pageNames: 'PageName.SETTINGS'
-    admin: True
+    privileges: '[UserPrivilege.PRIVILEGE_WRITE_ASSETS, UserPrivilege.PRIVILEGE_WRITE_LABELS, UserPrivilege.PRIVILEGE_READ_FILES, UserPrivilege.PRIVILEGE_WRITE_FILES]'
     showStatusBox: false
 spAppOverview:
     componentImport: False
@@ -96,8 +96,8 @@ spAppOverview:
     title: 'Apps'
     description: 'The app overview lets you access additional plugins.'
     icon: 'apps'
-    admin: false
     pageNames: 'PageName.APPS'
+    privileges: '[UserPrivilege.PRIVILEGE_READ_APPS, UserPrivilege.PRIVILEGE_WRITE_APPS]'
     showStatusBox: false
 spDashboard:
     componentImport: False
@@ -110,8 +110,8 @@ spDashboard:
     title: 'Dashboard'
     description: 'The live dashboard visualizes data in real-time.'
     icon: 'insert_chart'
-    admin: false
     pageNames: 'PageName.DASHBOARD'
+    privileges: '[UserPrivilege.PRIVILEGE_READ_DASHBOARD, UserPrivilege.PRIVILEGE_WRITE_DASHBOARD]'
     showStatusBox: true
     statusBox:
         link: "['dashboard']"
@@ -119,8 +119,8 @@ spDashboard:
         title: 'Dashboards'
         createTitle: 'New dashboard'
         dataFns: '[this.dashboardService.getDashboards()]'
-        viewRoles: '[UserRole.ROLE_DASHBOARD_ADMIN, UserRole.ROLE_DASHBOARD_USER]'
-        createRoles: '[UserRole.ROLE_DASHBOARD_ADMIN]'
+        viewRoles: '[UserPrivilege.PRIVILEGE_READ_DASHBOARD, UserPrivilege.PRIVILEGE_WRITE_DASHBOARD]'
+        createRoles: '[UserPrivilege.PRIVILEGE_WRITE_DASHBOARD]'
 spDataExplorer:
     componentImport: False
     moduleName: 'DataExplorerModule'
@@ -132,8 +132,8 @@ spDataExplorer:
     title: 'Data Explorer'
     description: 'The data explorer lets you visually inspect historical data from your connected sources.'
     icon: 'search'
-    admin: false
     pageNames: 'PageName.DATA_EXPLORER'
+    privileges: '[UserPrivilege.PRIVILEGE_READ_DATA_EXPLORER_VIEW, UserPrivilege.PRIVILEGE_WRITE_DATA_EXPLORER_VIEW]'
     showStatusBox: true
     statusBox:
         link: "['dataexplorer']"
@@ -141,19 +141,5 @@ spDataExplorer:
         title: 'Data Views'
         createTitle: 'New data view'
         dataFns: '[this.dataViewService.getDataViews()]'
-        viewRoles: '[UserRole.ROLE_DATA_EXPLORER_ADMIN, UserRole.ROLE_DATA_EXPLORER_USER]'
-        createRoles: '[UserRole.ROLE_DATA_EXPLORER_ADMIN]'
-spFiles:
-    componentImport: False
-    moduleName: 'FilesModule'
-    component: 'FilesComponent'
-    componentPath: './files/files.component'
-    path: './files/files.module'
-    link: 'files'
-    url: '/files'
-    title: 'File Management'
-    description: 'The file management module lets you upload and manage files that are used by adapters or pipeline elements.'
-    icon: 'folder'
-    admin: true
-    pageNames: 'PageName.FILE_UPLOAD'
-    showStatusBox: false
+        viewRoles: '[UserPrivilege.PRIVILEGE_READ_DATA_EXPLORER_VIEW, UserPrivilege.PRIVILEGE_WRITE_DATA_EXPLORER_VIEW]'
+        createRoles: '[UserPrivilege.PRIVILEGE_WRITE_DATA_EXPLORER_VIEW]'
diff --git a/ui/deployment/prebuild.js b/ui/deployment/prebuild.js
index 01c6865a4b..30a7c6772e 100644
--- a/ui/deployment/prebuild.js
+++ b/ui/deployment/prebuild.js
@@ -73,6 +73,7 @@ for (let module of config.modules) {
         path: modules[module]['path'],
         link: modules[module]['link'],
         pageNames: modules[module]['pageNames'],
+        privileges: modules[module]['privileges'],
         url: modules[module]['url'],
         title: modules[module]['title'],
         icon: modules[module]['icon'],
diff --git a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
index 5229982e53..7c9650ec20 100644
--- a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
+++ b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
@@ -20,7 +20,7 @@
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2024-09-03 17:38:51.
+// Generated using typescript-generator version 3.2.1263 on 2024-09-10 17:11:03.
 
 export class NamedStreamPipesEntity implements Storable {
     '@class':
@@ -2681,7 +2681,7 @@ export class PipelineElementTemplate implements Storable {
     couchDbRev: string;
     elementId: string;
     rev: string;
-    templateConfigs: { [index: string]: PipelineElementTemplateConfig };
+    templateConfigs: { [index: string]: any }[];
     templateDescription: string;
     templateName: string;
 
@@ -2698,8 +2698,8 @@ export class PipelineElementTemplate implements Storable {
         instance.couchDbRev = data.couchDbRev;
         instance.elementId = data.elementId;
         instance.rev = data.rev;
-        instance.templateConfigs = __getCopyObjectFn(
-            PipelineElementTemplateConfig.fromData,
+        instance.templateConfigs = __getCopyArrayFn(
+            __getCopyObjectFn(__identity<any>()),
         )(data.templateConfigs);
         instance.templateDescription = data.templateDescription;
         instance.templateName = data.templateName;
@@ -2707,26 +2707,6 @@ export class PipelineElementTemplate implements Storable {
     }
 }
 
-export class PipelineElementTemplateConfig {
-    displayed: boolean;
-    editable: boolean;
-    value: any;
-
-    static fromData(
-        data: PipelineElementTemplateConfig,
-        target?: PipelineElementTemplateConfig,
-    ): PipelineElementTemplateConfig {
-        if (!data) {
-            return data;
-        }
-        const instance = target || new PipelineElementTemplateConfig();
-        instance.displayed = data.displayed;
-        instance.editable = data.editable;
-        instance.value = data.value;
-        return instance;
-    }
-}
-
 export class PipelineElementValidationInfo {
     level: ValidationInfoLevel;
     message: string;
diff --git a/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser.component.html b/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser.component.html
index 17baed9a64..b84999e3c6 100644
--- a/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser.component.html
+++ b/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser.component.html
@@ -17,61 +17,65 @@
   -->
 
 <div fxLayout="row" fxFlex="100">
-    <div [fxFlex]="expanded ? browserWidth : '75px'">
-        <sp-basic-view [showBackLink]="false">
-            <div nav fxLayoutAlign="start center" fxFlex="100">
-                @if (assetBrowserData) {
-                    <sp-asset-browser-toolbar
-                        fxFlex="100"
+    @if (showAssetBrowser) {
+        <div [fxFlex]="expanded ? browserWidth : '75px'">
+            <sp-basic-view [showBackLink]="false">
+                <div nav fxLayoutAlign="start center" fxFlex="100">
+                    @if (assetBrowserData) {
+                        <sp-asset-browser-toolbar
+                            fxFlex="100"
+                            [assetBrowserData]="assetBrowserData"
+                            [expanded]="expanded"
+                            (toggleExpanded)="toggleExpanded($event)"
+                        >
+                        </sp-asset-browser-toolbar>
+                    }
+                </div>
+                @if (assetBrowserData?.assets?.length > 0 && expanded) {
+                    <sp-asset-browser-hierarchy
+                        [allResourcesAlias]="allResourcesAlias"
                         [assetBrowserData]="assetBrowserData"
-                        [expanded]="expanded"
-                        (toggleExpanded)="toggleExpanded($event)"
+                        [assetSelectionMode]="assetSelectionMode"
+                        [resourceCount]="resourceCount"
+                        [filteredAssetLinkType]="filteredAssetLinkType"
+                        (selectedAssetEmitter)="applyAssetFilter($event)"
+                        class="asset-hierarchy"
                     >
-                    </sp-asset-browser-toolbar>
-                }
-            </div>
-            @if (assetBrowserData?.assets?.length > 0 && expanded) {
-                <sp-asset-browser-hierarchy
-                    [allResourcesAlias]="allResourcesAlias"
-                    [assetBrowserData]="assetBrowserData"
-                    [assetSelectionMode]="assetSelectionMode"
-                    [resourceCount]="resourceCount"
-                    [filteredAssetLinkType]="filteredAssetLinkType"
-                    (selectedAssetEmitter)="applyAssetFilter($event)"
-                    class="asset-hierarchy"
-                >
-                </sp-asset-browser-hierarchy>
-            } @else if (assetBrowserData?.assets?.length === 0 && expanded) {
-                <div
-                    fxLayoutAlign="center center"
-                    fxLayout="column"
-                    fxFlex="100"
-                >
-                    <span class="asset-creation-hint"
-                        >No assets found - use assets to better organize
-                        resources!</span
+                    </sp-asset-browser-hierarchy>
+                } @else if (
+                    assetBrowserData?.assets?.length === 0 && expanded
+                ) {
+                    <div
+                        fxLayoutAlign="center center"
+                        fxLayout="column"
+                        fxFlex="100"
                     >
-                    <button
-                        mat-button
-                        color="accent"
-                        class="mt-10"
-                        (click)="navigateToAssetManagement()"
+                        <span class="asset-creation-hint"
+                            >No assets found - use assets to better organize
+                            resources!</span
+                        >
+                        <button
+                            mat-button
+                            color="accent"
+                            class="mt-10"
+                            (click)="navigateToAssetManagement()"
+                        >
+                            Manage assets
+                        </button>
+                    </div>
+                }
+                @if (!expanded) {
+                    <div
+                        *ngIf="!expanded"
+                        class="asset-hierarchy asset-browser-text"
+                        fxLayoutAlign="center center"
                     >
-                        Manage assets
-                    </button>
-                </div>
-            }
-            @if (!expanded) {
-                <div
-                    *ngIf="!expanded"
-                    class="asset-hierarchy asset-browser-text"
-                    fxLayoutAlign="center center"
-                >
-                    Asset Browser
-                </div>
-            }
-        </sp-basic-view>
-    </div>
+                        Asset Browser
+                    </div>
+                }
+            </sp-basic-view>
+        </div>
+    }
     <div fxFlex>
         <ng-content> </ng-content>
     </div>
diff --git a/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser.component.ts b/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser.component.ts
index a1a4aa96ba..f9c027d136 100644
--- a/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser.component.ts
+++ b/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser.component.ts
@@ -29,6 +29,7 @@ import { AssetBrowserData } from './asset-browser.model';
 import { Subscription } from 'rxjs';
 import { SpAsset } from '@streampipes/platform-services';
 import { Router } from '@angular/router';
+import { CurrentUserService } from '../../services/current-user.service';
 
 @Component({
     selector: 'sp-asset-browser',
@@ -68,20 +69,28 @@ export class AssetBrowserComponent implements OnInit, OnDestroy {
     expandedSub: Subscription;
 
     expanded = true;
+    showAssetBrowser = false;
 
     constructor(
         private assetBrowserService: SpAssetBrowserService,
         private router: Router,
+        private currentUserService: CurrentUserService,
     ) {}
 
     ngOnInit(): void {
-        this.assetBrowserDataSub =
-            this.assetBrowserService.assetData$.subscribe(assetData => {
-                this.assetBrowserData = assetData;
-            });
-        this.expandedSub = this.assetBrowserService.expanded$.subscribe(
-            expanded => (this.expanded = expanded),
-        );
+        this.showAssetBrowser = this.currentUserService.hasAnyRole([
+            'PRIVILEGE_READ_ASSETS',
+            'PRIVILEGE_WRITE_ASSETS',
+        ]);
+        if (this.showAssetBrowser) {
+            this.assetBrowserDataSub =
+                this.assetBrowserService.assetData$.subscribe(assetData => {
+                    this.assetBrowserData = assetData;
+                });
+            this.expandedSub = this.assetBrowserService.expanded$.subscribe(
+                expanded => (this.expanded = expanded),
+            );
+        }
     }
 
     toggleExpanded(event: boolean): void {
diff --git a/ui/projects/streampipes/shared-ui/src/lib/services/current-user.service.ts b/ui/projects/streampipes/shared-ui/src/lib/services/current-user.service.ts
index 8bbfa7beb3..2eb4e5b051 100644
--- a/ui/projects/streampipes/shared-ui/src/lib/services/current-user.service.ts
+++ b/ui/projects/streampipes/shared-ui/src/lib/services/current-user.service.ts
@@ -41,4 +41,21 @@ export class CurrentUserService {
     public getCurrentUser(): UserInfo {
         return this.user$.getValue() || this.jwtTokenStorageService.getUser();
     }
+
+    public hasRole(role: string): boolean {
+        const roles = this.getCurrentUser().roles;
+        return roles.includes('ROLE_ADMIN') || roles.includes(role);
+    }
+
+    public hasAnyRole(roles: string[]): boolean {
+        if (Array.isArray(roles)) {
+            return roles.reduce(
+                (aggregator: false, role: string) =>
+                    aggregator || this.hasRole(role),
+                false,
+            );
+        }
+
+        return false;
+    }
 }
diff --git a/ui/src/app/_enums/user-privilege.enum.ts b/ui/src/app/_enums/user-privilege.enum.ts
index 53ec9a5f2e..499a954bed 100644
--- a/ui/src/app/_enums/user-privilege.enum.ts
+++ b/ui/src/app/_enums/user-privilege.enum.ts
@@ -19,31 +19,24 @@
 export enum UserPrivilege {
     PRIVILEGE_READ_PIPELINE = 'PRIVILEGE_READ_PIPELINE',
     PRIVILEGE_WRITE_PIPELINE = 'PRIVILEGE_WRITE_PIPELINE',
-    PRIVILEGE_DELETE_PIPELINE = 'PRIVILEGE_DELETE_PIPELINE',
-
-    PRIVILEGE_READ_DATA_EXPLORER_VIEW = 'PRIVILEGE_READ_DATA_EXPLORER_VIEW',
-    PRIVILEGE_WRITE_DATA_EXPLORER_VIEW = 'PRIVILEGE_WRITE_DATA_EXPLORER_VIEW',
-    PRIVILEGE_DELETE_DATA_EXPLORER_VIEW = 'PRIVILEGE_DELETE_DATA_EXPLORER_VIEW',
-
-    PRIVILEGE_READ_DASHBOARD = 'PRIVILEGE_READ_DASHBOARD',
-    PRIVILEGE_WRITE_DASHBOARD = 'PRIVILEGE_WRITE_DASHBOARD',
-    PRIVILEGE_DELETE_DASHBOARD = 'PRIVILEGE_DELETE_DASHBOARD',
 
     PRIVILEGE_READ_ADAPTER = 'PRIVILEGE_READ_ADAPTER',
     PRIVILEGE_WRITE_ADAPTER = 'PRIVILEGE_WRITE_ADAPTER',
-    PRIVILEGE_DELETE_ADAPTER = 'PRIVILEGE_DELETE_ADAPTER',
 
     PRIVILEGE_READ_PIPELINE_ELEMENT = 'PRIVILEGE_READ_PIPELINE_ELEMENT',
     PRIVILEGE_WRITE_PIPELINE_ELEMENT = 'PRIVILEGE_WRITE_PIPELINE_ELEMENT',
-    PRIVILEGE_DELETE_PIPELINE_ELEMENT = 'PRIVILEGE_DELETE_PIPELINE_ELEMENT',
+
+    PRIVILEGE_READ_DASHBOARD = 'PRIVILEGE_READ_DASHBOARD',
+    PRIVILEGE_WRITE_DASHBOARD = 'PRIVILEGE_WRITE_DASHBOARD',
+
+    PRIVILEGE_READ_DATA_EXPLORER_VIEW = 'PRIVILEGE_READ_DATA_EXPLORER_VIEW',
+    PRIVILEGE_WRITE_DATA_EXPLORER_VIEW = 'PRIVILEGE_WRITE_DATA_EXPLORER_VIEW',
 
     PRIVILEGE_READ_DATA_EXPLORER_WIDGET = 'PRIVILEGE_READ_DATA_EXPLORER_WIDGET',
     PRIVILEGE_WRITE_DATA_EXPLORER_WIDGET = 'PRIVILEGE_WRITE_DATA_EXPLORER_WIDGET',
-    PRIVILEGE_DELETE_DATA_EXPLORER_WIDGET = 'PRIVILEGE_DELETE_DATA_EXPLORER_WIDGET',
 
     PRIVILEGE_READ_DASHBOARD_WIDGET = 'PRIVILEGE_READ_DASHBOARD_WIDGET',
     PRIVILEGE_WRITE_DASHBOARD_WIDGET = 'PRIVILEGE_WRITE_DASHBOARD_WIDGET',
-    PRIVILEGE_DELETE_DASHBOARD_WIDGET = 'PRIVILEGE_DELETE_DASHBOARD_WIDGET',
 
     PRIVILEGE_READ_APPS = 'PRIVILEGE_READ_APPS',
     PRIVILEGE_WRITE_APPS = 'PRIVILEGE_WRITE_APPS',
@@ -52,5 +45,10 @@ export enum UserPrivilege {
 
     PRIVILEGE_READ_FILES = 'PRIVILEGE_READ_FILES',
     PRIVILEGE_WRITE_FILES = 'PRIVILEGE_WRITE_FILES',
-    PRIVILEGE_DELETE_FILES = 'PRIVILEGE_DELETE_FILES',
+
+    PRIVILEGE_READ_ASSETS = 'PRIVILEGE_READ_ASSETS',
+    PRIVILEGE_WRITE_ASSETS = 'PRIVILEGE_WRITE_ASSETS',
+
+    PRIVILEGE_READ_LABELS = 'PRIVILEGE_READ_LABELS',
+    PRIVILEGE_WRITE_LABELS = 'PRIVILEGE_WRITE_LABELS',
 }
diff --git a/ui/src/app/_guards/page-auth.can-active.guard.ts b/ui/src/app/_guards/page-auth.can-active.guard.ts
index fab5cecf00..0f3ebc4468 100644
--- a/ui/src/app/_guards/page-auth.can-active.guard.ts
+++ b/ui/src/app/_guards/page-auth.can-active.guard.ts
@@ -18,7 +18,6 @@
 
 import { ActivatedRouteSnapshot } from '@angular/router';
 import { AuthService } from '../services/auth.service';
-import { PageName } from '../_enums/page-name.enum';
 import { Injectable } from '@angular/core';
 
 @Injectable()
@@ -26,8 +25,8 @@ export class PageAuthGuard {
     constructor(private authService: AuthService) {}
 
     canActivateChild(activatedRouteSnapshot: ActivatedRouteSnapshot): boolean {
-        const pageNames: PageName[] = activatedRouteSnapshot.data.authPageNames;
+        const privileges: string[] = activatedRouteSnapshot.data.privileges;
 
-        return this.authService.isAnyAccessGranted(pageNames, true);
+        return this.authService.isAnyAccessGranted(privileges, true);
     }
 }
diff --git a/ui/src/app/assets/components/asset-overview/asset-overview.component.html b/ui/src/app/assets/components/asset-overview/asset-overview.component.html
index 22451587b9..6c6b0fb2db 100644
--- a/ui/src/app/assets/components/asset-overview/asset-overview.component.html
+++ b/ui/src/app/assets/components/asset-overview/asset-overview.component.html
@@ -24,6 +24,7 @@
         class="pl-10"
     >
         <button
+            *ngIf="hasWritePrivilege"
             mat-button
             mat-raised-button
             color="accent"
@@ -100,6 +101,7 @@
                                     <i class="material-icons">search</i>
                                 </button>
                                 <button
+                                    *ngIf="hasWritePrivilege"
                                     color="accent"
                                     mat-icon-button
                                     matTooltip="Edit asset"
@@ -111,6 +113,7 @@
                                 </button>
 
                                 <button
+                                    *ngIf="hasWritePrivilege"
                                     color="accent"
                                     mat-icon-button
                                     matTooltip="Delete asset"
diff --git a/ui/src/app/assets/components/asset-overview/asset-overview.component.ts b/ui/src/app/assets/components/asset-overview/asset-overview.component.ts
index 88a5b3f79f..386a03157d 100644
--- a/ui/src/app/assets/components/asset-overview/asset-overview.component.ts
+++ b/ui/src/app/assets/components/asset-overview/asset-overview.component.ts
@@ -24,6 +24,7 @@ import {
     SpAssetModel,
 } from '@streampipes/platform-services';
 import {
+    CurrentUserService,
     DialogService,
     PanelType,
     SpAssetBrowserService,
@@ -33,6 +34,7 @@ import { SpAssetRoutes } from '../../assets.routes';
 import { Router } from '@angular/router';
 import { SpCreateAssetDialogComponent } from '../../dialog/create-asset/create-asset-dialog.component';
 import { IdGeneratorService } from '../../../core-services/id-generator/id-generator.service';
+import { UserPrivilege } from '../../../_enums/user-privilege.enum';
 
 @Component({
     selector: 'sp-asset-overview',
@@ -47,6 +49,8 @@ export class SpAssetOverviewComponent implements OnInit {
     dataSource: MatTableDataSource<SpAssetModel> =
         new MatTableDataSource<SpAssetModel>();
 
+    hasWritePrivilege = false;
+
     constructor(
         private genericStorageService: GenericStorageService,
         private breadcrumbService: SpBreadcrumbService,
@@ -54,9 +58,13 @@ export class SpAssetOverviewComponent implements OnInit {
         private router: Router,
         private idGeneratorService: IdGeneratorService,
         private assetBrowserService: SpAssetBrowserService,
+        private currentUserService: CurrentUserService,
     ) {}
 
     ngOnInit(): void {
+        this.hasWritePrivilege = this.currentUserService.hasRole(
+            UserPrivilege.PRIVILEGE_WRITE_ASSETS,
+        );
         this.breadcrumbService.updateBreadcrumb(
             this.breadcrumbService.getRootLink(SpAssetRoutes.BASE),
         );
diff --git a/ui/src/app/configuration/configuration-tabs.service.ts b/ui/src/app/configuration/configuration-tabs.service.ts
index e1e2db1f81..dc1187b0aa 100644
--- a/ui/src/app/configuration/configuration-tabs.service.ts
+++ b/ui/src/app/configuration/configuration-tabs.service.ts
@@ -53,6 +53,12 @@ export class SpConfigurationTabsService {
             itemLink: ['configuration', 'extensions-services'],
             roles: ['ROLE_ADMIN'],
         },
+        {
+            itemId: 'files',
+            itemTitle: 'Files',
+            itemLink: ['configuration', 'files'],
+            roles: ['PRIVILEGE_READ_FILES', 'PRIVILEGE_WRITE_FILES'],
+        },
         {
             itemId: 'labels',
             itemTitle: 'Labels',
diff --git a/ui/src/app/configuration/configuration.module.ts b/ui/src/app/configuration/configuration.module.ts
index 5b9a22c875..f24da2f959 100644
--- a/ui/src/app/configuration/configuration.module.ts
+++ b/ui/src/app/configuration/configuration.module.ts
@@ -91,6 +91,11 @@ import { EditRoleDialogComponent } from './security-configuration/edit-role-dial
 import { MatListModule } from '@angular/material/list';
 import { configurationRouteGuard } from './configuration-route.guard';
 import { ServiceConfigsItemComponent } from './extensions-service-management/extensions-service-configuration/service-configs/service-configs-item/service-configs-item.component';
+import { FilesComponent } from './files/files.component';
+import { FileOverviewComponent } from './files/file-overview/file-overview.component';
+import { FileUploadDialogComponent } from './dialog/file-upload/file-upload-dialog.component';
+import { FileRenameDialogComponent } from './dialog/file-rename/file-rename-dialog.component';
+import { MatDialogModule } from '@angular/material/dialog';
 
 @NgModule({
     imports: [
@@ -164,6 +169,11 @@ import { ServiceConfigsItemComponent } from './extensions-service-management/ext
                         component: ExtensionsServiceManagementComponent,
                         canActivate: [configurationRouteGuard],
                     },
+                    {
+                        path: 'files',
+                        component: FilesComponent,
+                        canActivate: [configurationRouteGuard],
+                    },
                     {
                         path: 'security',
                         component: SecurityConfigurationComponent,
@@ -182,6 +192,7 @@ import { ServiceConfigsItemComponent } from './extensions-service-management/ext
         CodemirrorModule,
         MatSort,
         MatListModule,
+        MatDialogModule,
     ],
     declarations: [
         ServiceConfigsComponent,
@@ -197,6 +208,10 @@ import { ServiceConfigsItemComponent } from './extensions-service-management/ext
         EditUserDialogComponent,
         EditGroupDialogComponent,
         EmailConfigurationComponent,
+        FilesComponent,
+        FileOverviewComponent,
+        FileUploadDialogComponent,
+        FileRenameDialogComponent,
         GeneralConfigurationComponent,
         ExtensionsServiceManagementComponent,
         LocationFeaturesConfigurationComponent,
diff --git a/ui/src/app/files/dialog/file-rename/file-rename-dialog.component.html b/ui/src/app/configuration/dialog/file-rename/file-rename-dialog.component.html
similarity index 100%
rename from ui/src/app/files/dialog/file-rename/file-rename-dialog.component.html
rename to ui/src/app/configuration/dialog/file-rename/file-rename-dialog.component.html
diff --git a/ui/src/app/files/dialog/file-rename/file-rename-dialog.component.scss b/ui/src/app/configuration/dialog/file-rename/file-rename-dialog.component.scss
similarity index 95%
rename from ui/src/app/files/dialog/file-rename/file-rename-dialog.component.scss
rename to ui/src/app/configuration/dialog/file-rename/file-rename-dialog.component.scss
index 5af8ee0fc0..13cbc4aacb 100644
--- a/ui/src/app/files/dialog/file-rename/file-rename-dialog.component.scss
+++ b/ui/src/app/configuration/dialog/file-rename/file-rename-dialog.component.scss
@@ -15,5 +15,3 @@
  * limitations under the License.
  *
  */
-
-@import '../../../../scss/sp/sp-dialog';
diff --git a/ui/src/app/files/dialog/file-rename/file-rename-dialog.component.ts b/ui/src/app/configuration/dialog/file-rename/file-rename-dialog.component.ts
similarity index 100%
rename from ui/src/app/files/dialog/file-rename/file-rename-dialog.component.ts
rename to ui/src/app/configuration/dialog/file-rename/file-rename-dialog.component.ts
diff --git a/ui/src/app/files/dialog/file-upload/file-upload-dialog.component.html b/ui/src/app/configuration/dialog/file-upload/file-upload-dialog.component.html
similarity index 100%
rename from ui/src/app/files/dialog/file-upload/file-upload-dialog.component.html
rename to ui/src/app/configuration/dialog/file-upload/file-upload-dialog.component.html
diff --git a/ui/src/app/files/dialog/file-upload/file-upload-dialog.component.scss b/ui/src/app/configuration/dialog/file-upload/file-upload-dialog.component.scss
similarity index 95%
rename from ui/src/app/files/dialog/file-upload/file-upload-dialog.component.scss
rename to ui/src/app/configuration/dialog/file-upload/file-upload-dialog.component.scss
index 5af8ee0fc0..13cbc4aacb 100644
--- a/ui/src/app/files/dialog/file-upload/file-upload-dialog.component.scss
+++ b/ui/src/app/configuration/dialog/file-upload/file-upload-dialog.component.scss
@@ -15,5 +15,3 @@
  * limitations under the License.
  *
  */
-
-@import '../../../../scss/sp/sp-dialog';
diff --git a/ui/src/app/files/dialog/file-upload/file-upload-dialog.component.ts b/ui/src/app/configuration/dialog/file-upload/file-upload-dialog.component.ts
similarity index 100%
rename from ui/src/app/files/dialog/file-upload/file-upload-dialog.component.ts
rename to ui/src/app/configuration/dialog/file-upload/file-upload-dialog.component.ts
diff --git a/ui/src/app/files/components/file-overview/file-overview.component.html b/ui/src/app/configuration/files/file-overview/file-overview.component.html
similarity index 100%
rename from ui/src/app/files/components/file-overview/file-overview.component.html
rename to ui/src/app/configuration/files/file-overview/file-overview.component.html
diff --git a/ui/src/app/files/components/file-overview/file-overview.component.scss b/ui/src/app/configuration/files/file-overview/file-overview.component.scss
similarity index 100%
rename from ui/src/app/files/components/file-overview/file-overview.component.scss
rename to ui/src/app/configuration/files/file-overview/file-overview.component.scss
diff --git a/ui/src/app/files/components/file-overview/file-overview.component.ts b/ui/src/app/configuration/files/file-overview/file-overview.component.ts
similarity index 100%
rename from ui/src/app/files/components/file-overview/file-overview.component.ts
rename to ui/src/app/configuration/files/file-overview/file-overview.component.ts
diff --git a/ui/src/app/files/files.component.html b/ui/src/app/configuration/files/files.component.html
similarity index 54%
rename from ui/src/app/files/files.component.html
rename to ui/src/app/configuration/files/files.component.html
index bbdd7e0319..b4b058e804 100644
--- a/ui/src/app/files/files.component.html
+++ b/ui/src/app/configuration/files/files.component.html
@@ -16,36 +16,30 @@
   ~
   -->
 
-<sp-basic-view [showBackLink]="false" [padding]="true">
-    <div
-        nav
-        fxFlex="100"
-        fxLayoutAlign="start center"
-        fxLayout="row"
-        class="pl-10"
+<sp-basic-nav-tabs [spNavigationItems]="tabs" [activeLink]="'files'">
+    <sp-split-section
+        title="Files"
+        subtitle="Upload and manage files that are used by adapters or pipeline elements."
     >
-        <button
-            mat-button
-            mat-raised-button
-            color="accent"
-            (click)="openFileUploadDialog()"
-            data-cy="sp-open-file-upload-dialog"
-            class="mr-10"
-        >
-            <i class="material-icons">cloud_upload</i>
-            &nbsp;Upload new file
-        </button>
-    </div>
+        <div fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="10px">
+            <button
+                mat-button
+                mat-raised-button
+                color="accent"
+                (click)="openFileUploadDialog()"
+                data-cy="sp-open-file-upload-dialog"
+                class="mr-10"
+            >
+                <i class="material-icons">cloud_upload</i>
+                &nbsp;Upload new file
+            </button>
+        </div>
 
-    <div fxFlex="100" fxLayout="column">
-        <sp-basic-header-title-component
-            title="Files"
-        ></sp-basic-header-title-component>
-        <div fxFlex="100" fxLayout="row" fxLayoutAlign="center start">
+        <div fxFlex="100" fxLayout="column">
             <sp-file-overview
-                fxFlex="90"
+                fxFlex="100"
                 #fileOverviewComponent
             ></sp-file-overview>
         </div>
-    </div>
-</sp-basic-view>
+    </sp-split-section>
+</sp-basic-nav-tabs>
diff --git a/ui/src/app/files/files.component.scss b/ui/src/app/configuration/files/files.component.scss
similarity index 100%
rename from ui/src/app/files/files.component.scss
rename to ui/src/app/configuration/files/files.component.scss
diff --git a/ui/src/app/files/files.component.ts b/ui/src/app/configuration/files/files.component.ts
similarity index 75%
rename from ui/src/app/files/files.component.ts
rename to ui/src/app/configuration/files/files.component.ts
index 3129ef4897..642c471bc7 100644
--- a/ui/src/app/files/files.component.ts
+++ b/ui/src/app/configuration/files/files.component.ts
@@ -21,26 +21,33 @@ import {
     DialogService,
     PanelType,
     SpBreadcrumbService,
+    SpNavigationItem,
 } from '@streampipes/shared-ui';
-import { FileUploadDialogComponent } from './dialog/file-upload/file-upload-dialog.component';
-import { SpFilesRoutes } from './files.routes';
+import { FileUploadDialogComponent } from '../dialog/file-upload/file-upload-dialog.component';
+import { SpConfigurationTabsService } from '../configuration-tabs.service';
+import { SpConfigurationRoutes } from '../configuration.routes';
 
 @Component({
     templateUrl: './files.component.html',
     styleUrls: ['./files.component.scss'],
 })
 export class FilesComponent implements OnInit {
+    tabs: SpNavigationItem[] = [];
+
     @ViewChild('fileOverviewComponent') fileOverviewComponent;
 
     constructor(
         private dialogService: DialogService,
         private breadcrumbService: SpBreadcrumbService,
+        private tabService: SpConfigurationTabsService,
     ) {}
 
     ngOnInit() {
-        this.breadcrumbService.updateBreadcrumb(
-            this.breadcrumbService.getRootLink(SpFilesRoutes.BASE),
-        );
+        this.tabs = this.tabService.getTabs();
+        this.breadcrumbService.updateBreadcrumb([
+            SpConfigurationRoutes.BASE,
+            { label: this.tabService.getTabTitle('files') },
+        ]);
     }
 
     openFileUploadDialog() {
diff --git a/ui/src/app/connect/dialog/adapter-template/adapter-template-dialog.component.ts b/ui/src/app/connect/dialog/adapter-template/adapter-template-dialog.component.ts
index 2d04328a5e..56ed70d792 100644
--- a/ui/src/app/connect/dialog/adapter-template/adapter-template-dialog.component.ts
+++ b/ui/src/app/connect/dialog/adapter-template/adapter-template-dialog.component.ts
@@ -19,7 +19,6 @@
 import { Component, Input, OnInit } from '@angular/core';
 import {
     PipelineElementTemplate,
-    PipelineElementTemplateConfig,
     PipelineElementTemplateService,
     StaticPropertyUnion,
 } from '@streampipes/platform-services';
@@ -38,7 +37,7 @@ export class SpAdapterTemplateDialogComponent implements OnInit {
     appId: string;
 
     template: PipelineElementTemplate;
-    templateConfigs: Map<string, any> = new Map();
+    templateConfigs: Map<string, any>[] = [];
 
     constructor(
         public dialogRef: DialogRef<SpAdapterTemplateDialogComponent>,
@@ -53,19 +52,18 @@ export class SpAdapterTemplateDialogComponent implements OnInit {
         this.template.templateConfigs = this.convert(this.templateConfigs);
         this.pipelineElementTemplateService
             .storePipelineElementTemplate(this.template)
-            .subscribe(result => {
+            .subscribe(() => {
                 this.dialogRef.close(true);
             });
     }
 
-    convert(templateConfigs: Map<string, any>): any {
-        const configs: { [index: string]: PipelineElementTemplateConfig } = {};
-        templateConfigs.forEach((value, key) => {
-            configs[key] = new PipelineElementTemplateConfig();
-            configs[key].editable = value.editable;
-            configs[key].displayed = value.displayed;
-            configs[key].value = value.value;
+    convert(templateConfigs: Map<string, any>[]): Record<string, any>[] {
+        return templateConfigs.map(map => {
+            const obj: Record<string, any> = {};
+            map.forEach((value, key) => {
+                obj[key] = value;
+            });
+            return obj;
         });
-        return configs;
     }
 }
diff --git a/ui/src/app/core-ui/core-ui.module.ts b/ui/src/app/core-ui/core-ui.module.ts
index 12673d1306..dc2337b5d1 100644
--- a/ui/src/app/core-ui/core-ui.module.ts
+++ b/ui/src/app/core-ui/core-ui.module.ts
@@ -115,6 +115,7 @@ import { StaticTreeInputNodeDetailsComponent } from './static-properties/static-
 import { SingleMarkerMapComponent } from './single-marker-map/single-marker-map.component';
 import { LeafletModule } from '@asymmetrik/ngx-leaflet';
 import { StaticTreeInputTextEditorComponent } from './static-properties/static-runtime-resolvable-tree-input/static-tree-input-text-editor/static-tree-input-text-editor.component';
+import { PipelineElementTemplateConfigItemComponent } from './pipeline-element-template-config/pipeline-element-template-config-item/pipeline-element-template-config-item.component';
 
 @NgModule({
     imports: [
@@ -172,6 +173,7 @@ import { StaticTreeInputTextEditorComponent } from './static-properties/static-r
         DisplayRecommendedPipe,
         ObjectPermissionDialogComponent,
         PipelineElementTemplateConfigComponent,
+        PipelineElementTemplateConfigItemComponent,
         PipelineElementTemplatePipe,
         PipelineElementRuntimeInfoComponent,
         PipelineElementDocumentationComponent,
diff --git a/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-config-item/pipeline-element-template-config-item.component.html b/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-config-item/pipeline-element-template-config-item.component.html
new file mode 100644
index 0000000000..877298dc49
--- /dev/null
+++ b/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-config-item/pipeline-element-template-config-item.component.html
@@ -0,0 +1,28 @@
+<!--
+  ~ 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.
+  ~
+  -->
+
+<div fxLayout="column">
+    <mat-checkbox
+        [checked]="selected"
+        (change)="handleSelection()"
+        name="store"
+        class="sp"
+        color="accent"
+        >Store as template
+    </mat-checkbox>
+</div>
diff --git a/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-config-item/pipeline-element-template-config-item.component.ts b/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-config-item/pipeline-element-template-config-item.component.ts
new file mode 100644
index 0000000000..41089ca5b2
--- /dev/null
+++ b/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-config-item/pipeline-element-template-config-item.component.ts
@@ -0,0 +1,59 @@
+/*
+ * 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, Input, OnInit } from '@angular/core';
+import { StaticPropertyUnion } from '@streampipes/platform-services';
+import { PipelineElementTemplateGenerator } from '../pipeline-element-template-generator';
+
+@Component({
+    selector: 'sp-pipeline-element-template-config-item',
+    templateUrl: './pipeline-element-template-config-item.component.html',
+})
+export class PipelineElementTemplateConfigItemComponent implements OnInit {
+    @Input()
+    templateConfigs: Map<string, any>[] = [];
+
+    @Input()
+    sp: StaticPropertyUnion;
+
+    selected = true;
+
+    ngOnInit(): void {
+        this.calculateSelected();
+    }
+
+    calculateSelected(): void {
+        this.selected =
+            this.templateConfigs.find(config =>
+                config.has(this.sp.internalName),
+            ) !== undefined;
+    }
+
+    handleSelection() {
+        if (this.selected) {
+            this.templateConfigs = this.templateConfigs.filter(
+                c => !c.has(this.sp.internalName),
+            );
+        } else {
+            this.templateConfigs.push(
+                new PipelineElementTemplateGenerator(this.sp).toTemplateValue(),
+            );
+        }
+        this.calculateSelected();
+    }
+}
diff --git a/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-config.component.html b/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-config.component.html
index fe9991be09..a6c9c8e2de 100644
--- a/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-config.component.html
+++ b/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-config.component.html
@@ -16,7 +16,7 @@
   ~
   -->
 
-<div class="p-15">
+<div class="p-15" *ngIf="templateConfigs">
     <div fxFlex="100" fxLayout="column">
         <h4>Basics</h4>
         <mat-form-field fxFlex color="accent">
@@ -58,16 +58,11 @@
                     {{ config.label }}
                 </div>
                 <div fxFlex="50">
-                    <div fxLayout="column">
-                        <mat-checkbox
-                            [checked]="templateConfigs.has(config.internalName)"
-                            (change)="handleSelection(config)"
-                            name="store"
-                            class="sp"
-                            color="accent"
-                            >Store as template
-                        </mat-checkbox>
-                    </div>
+                    <sp-pipeline-element-template-config-item
+                        [sp]="config"
+                        [templateConfigs]="templateConfigs"
+                    >
+                    </sp-pipeline-element-template-config-item>
                 </div>
             </div>
         </div>
diff --git a/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-config.component.ts b/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-config.component.ts
index d6c3402193..93162f6b92 100644
--- a/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-config.component.ts
+++ b/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-config.component.ts
@@ -18,8 +18,8 @@
 
 import { Component, Input, OnInit } from '@angular/core';
 import {
-    PipelineElementTemplateService,
     PipelineElementTemplate,
+    PipelineElementTemplateService,
     StaticPropertyUnion,
 } from '@streampipes/platform-services';
 import { PipelineElementTemplateGenerator } from './pipeline-element-template-generator';
@@ -34,7 +34,7 @@ export class PipelineElementTemplateConfigComponent implements OnInit {
     template: PipelineElementTemplate;
 
     @Input()
-    templateConfigs: Map<string, any>;
+    templateConfigs: Map<string, any>[] = [];
 
     @Input()
     appId: string;
@@ -42,7 +42,7 @@ export class PipelineElementTemplateConfigComponent implements OnInit {
     @Input()
     staticProperties: StaticPropertyUnion[];
 
-    existingTemplates: PipelineElementTemplate[];
+    existingTemplates: PipelineElementTemplate[] = [];
 
     constructor(
         private pipelineElementTemplateService: PipelineElementTemplateService,
@@ -52,9 +52,8 @@ export class PipelineElementTemplateConfigComponent implements OnInit {
         this.loadTemplates();
         this.template.basePipelineElementAppId = this.appId;
         this.staticProperties.forEach(sp => {
-            this.templateConfigs.set(
-                sp.internalName,
-                this.makeTemplateValue(sp),
+            this.templateConfigs.push(
+                new PipelineElementTemplateGenerator(sp).toTemplateValue(),
             );
         });
     }
@@ -67,39 +66,6 @@ export class PipelineElementTemplateConfigComponent implements OnInit {
             });
     }
 
-    handleSelection(sp: StaticPropertyUnion) {
-        if (this.templateConfigs.has(sp.internalName)) {
-            this.templateConfigs.delete(sp.internalName);
-        } else {
-            this.templateConfigs.set(
-                sp.internalName,
-                this.makeTemplateValue(sp),
-            );
-        }
-    }
-
-    makeTemplateValue(sp: StaticPropertyUnion) {
-        const config: any = {};
-        config.displayed = false;
-        config.editable = false;
-        config.value = new PipelineElementTemplateGenerator(
-            sp,
-        ).toTemplateValue();
-        return config;
-    }
-
-    toggleViewPermission(sp: StaticPropertyUnion) {
-        const config: any = this.templateConfigs.get(sp.internalName);
-        config.displayed = !config.displayed;
-        this.templateConfigs.set(sp.internalName, config);
-    }
-
-    toggleEditPermission(sp: StaticPropertyUnion) {
-        const config: any = this.templateConfigs.get(sp.internalName);
-        config.editable = !config.editable;
-        this.templateConfigs.set(sp.internalName, config);
-    }
-
     deleteTemplate(templateId: string) {
         this.pipelineElementTemplateService
             .deletePipelineElementTemplate(templateId)
diff --git a/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-generator.ts b/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-generator.ts
index d8daf86e4f..8fb67b15b7 100644
--- a/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-generator.ts
+++ b/ui/src/app/core-ui/pipeline-element-template-config/pipeline-element-template-generator.ts
@@ -24,81 +24,86 @@ import {
     FileStaticProperty,
     FreeTextStaticProperty,
     OneOfStaticProperty,
+    RuntimeResolvableTreeInputStaticProperty,
     SecretStaticProperty,
-    StaticPropertyAlternative,
+    SlideToggleStaticProperty,
     StaticPropertyAlternatives,
     StaticPropertyGroup,
     StaticPropertyUnion,
-    SlideToggleStaticProperty,
 } from '@streampipes/platform-services';
 
 export class PipelineElementTemplateGenerator {
     constructor(private sp: StaticPropertyUnion) {}
 
-    public toTemplateValue(): any {
+    public toTemplateValue(): Map<string, any> {
+        const map = new Map();
         if (this.sp instanceof FreeTextStaticProperty) {
-            return this.sp.value;
+            map.set(this.sp.internalName, this.sp.value);
         } else if (this.sp instanceof OneOfStaticProperty) {
-            return this.sp.options.find(o => o.selected)
-                ? this.sp.options.find(o => o.selected).name
-                : '';
+            const selectedOption = this.sp.options.find(o => o.selected);
+            if (selectedOption !== undefined) {
+                map.set(this.sp.internalName, selectedOption.name);
+            }
         } else if (this.sp instanceof ColorPickerStaticProperty) {
-            return this.sp.selectedColor;
+            map.set(this.sp.internalName, this.sp.selectedColor);
         } else if (this.sp instanceof SecretStaticProperty) {
-            return { encrypted: this.sp.encrypted, value: this.sp.value };
+            map.set(this.sp.internalName, this.sp.value);
+            map.set('encrypted', this.sp.encrypted);
         } else if (this.sp instanceof AnyStaticProperty) {
-            return this.sp.options.filter(o => o.selected).map(o => o.name);
+            map.set(
+                this.sp.internalName,
+                this.sp.options.filter(o => o.selected).map(o => o.name),
+            );
         } else if (this.sp instanceof CodeInputStaticProperty) {
-            return this.sp.value;
+            map.set(this.sp.internalName, this.sp.value);
         } else if (this.sp instanceof SlideToggleStaticProperty) {
-            return this.sp.selected;
+            map.set(this.sp.internalName, this.sp.selected);
         } else if (this.sp instanceof CollectionStaticProperty) {
-            return {
-                members: this.addListEntry(this.sp.members),
-            };
+            map.set(this.sp.internalName, this.addListEntry(this.sp.members));
         } else if (this.sp instanceof FileStaticProperty) {
-            return this.sp.locationPath;
+            map.set(this.sp.internalName, this.sp.locationPath);
         } else if (this.sp instanceof StaticPropertyAlternatives) {
-            return {
-                alternatives: this.addNestedEntry(this.sp.alternatives),
-            };
-        } else if (this.sp instanceof StaticPropertyAlternative) {
-            const sp = this.sp.staticProperty
-                ? this.addNestedEntry([this.sp.staticProperty])
-                : {};
-            return {
-                selected: this.sp.selected,
-                staticProperty: sp,
-            };
+            const selectedAlternative = this.sp.alternatives.find(
+                a => a.selected,
+            );
+            if (selectedAlternative !== undefined) {
+                map.set(this.sp.internalName, selectedAlternative.internalName);
+                const alternative = new PipelineElementTemplateGenerator(
+                    selectedAlternative.staticProperty,
+                ).toTemplateValue();
+                alternative.forEach((value, key) => {
+                    map.set(key, value);
+                });
+            }
         } else if (this.sp instanceof StaticPropertyGroup) {
-            return {
-                staticProperties: this.addNestedEntry(this.sp.staticProperties),
-            };
+            return this.addNestedEntry(this.sp.staticProperties);
+        } else if (
+            this.sp instanceof RuntimeResolvableTreeInputStaticProperty
+        ) {
+            map.set(this.sp.internalName, this.sp.selectedNodesInternalNames);
         }
-    }
-
-    addEntry(sp: StaticPropertyUnion) {
-        const entry = {};
-        entry[sp.internalName] = new PipelineElementTemplateGenerator(
-            sp,
-        ).toTemplateValue();
-        return entry;
+        return map;
     }
 
     addListEntry(staticProperties: StaticPropertyUnion[]) {
         const values = [];
         staticProperties.forEach(sp => {
-            values.push(this.addEntry(sp));
+            values.push(
+                new PipelineElementTemplateGenerator(sp).toTemplateValue(),
+            );
         });
         return values;
     }
 
     addNestedEntry(staticProperties: StaticPropertyUnion[]) {
-        const entry = {};
+        const entry = new Map<string, any>();
         staticProperties.forEach(sp => {
-            entry[sp.internalName] = new PipelineElementTemplateGenerator(
+            const groupEntries = new PipelineElementTemplateGenerator(
                 sp,
             ).toTemplateValue();
+            groupEntries.forEach((value, key) => {
+                entry.set(key, value);
+            });
         });
         return entry;
     }
diff --git a/ui/src/app/core-ui/static-properties/static-file-input/static-file-input.component.ts b/ui/src/app/core-ui/static-properties/static-file-input/static-file-input.component.ts
index e181e438fe..600058ad46 100644
--- a/ui/src/app/core-ui/static-properties/static-file-input/static-file-input.component.ts
+++ b/ui/src/app/core-ui/static-properties/static-file-input/static-file-input.component.ts
@@ -19,15 +19,15 @@
 import { Component, EventEmitter, OnInit, Output } from '@angular/core';
 import { HttpEventType, HttpResponse } from '@angular/common/http';
 import {
+    FileMetadata,
     FilesService,
     FileStaticProperty,
-    FileMetadata,
 } from '@streampipes/platform-services';
 import { ConfigurationInfo } from '../../../connect/model/ConfigurationInfo';
 import { AbstractValidatedStaticPropertyRenderer } from '../base/abstract-validated-static-property';
 import { UntypedFormControl, ValidatorFn, Validators } from '@angular/forms';
 import { MatDialog, MatDialogRef } from '@angular/material/dialog';
-import { FileRenameDialogComponent } from '../../../files/dialog/file-rename/file-rename-dialog.component';
+import { FileRenameDialogComponent } from '../../../configuration/dialog/file-rename/file-rename-dialog.component';
 
 @Component({
     selector: 'sp-static-file-input',
diff --git a/ui/src/app/core/components/iconbar/iconbar.component.html b/ui/src/app/core/components/iconbar/iconbar.component.html
index 873dc19adf..85d6238171 100644
--- a/ui/src/app/core/components/iconbar/iconbar.component.html
+++ b/ui/src/app/core/components/iconbar/iconbar.component.html
@@ -16,16 +16,11 @@
   ~
   -->
 
-<div style="padding-top: 0px" fxFlex="100" fxLayout="column">
+<div style="padding-top: 0" fxFlex="100" fxLayout="column">
     <div *ngFor="let item of menu">
         <div
             *ngIf="item.visible"
-            style="
-                min-width: 0px;
-                padding: 0px;
-                padding-top: 5px;
-                padding-bottom: 5px;
-            "
+            style="min-width: 0; padding: 5px 0"
             [ngClass]="
                 item.link === activePage
                     ? 'sp-navbar-item-selected'
@@ -52,37 +47,4 @@
             </button>
         </div>
     </div>
-
-    <mat-divider
-        style="border-top-color: var(--color-navigation-text)"
-    ></mat-divider>
-    <div *ngFor="let item of admin">
-        <div
-            [ngClass]="
-                item.link === activePage
-                    ? 'sp-navbar-item-selected'
-                    : 'sp-navbar-item'
-            "
-            style="padding-top: 5px; padding-bottom: 5px"
-            *ngIf="item.visible"
-        >
-            <button
-                mat-icon-button
-                class="md-icon-button button-margin-iconbar iconbar-size"
-                (click)="go(item.link)"
-                matTooltip="{{ item.title }}"
-                matTooltipPosition="right"
-            >
-                <mat-icon
-                    [ngClass]="
-                        item.link === activePage
-                            ? 'sp-navbar-icon-selected'
-                            : 'sp-navbar-icon'
-                    "
-                    data-cy="navigation-icon"
-                    >{{ item.icon }}</mat-icon
-                >
-            </button>
-        </div>
-    </div>
 </div>
diff --git a/ui/src/app/core/components/iconbar/iconbar.component.ts b/ui/src/app/core/components/iconbar/iconbar.component.ts
index dbfef7651b..7230e58cb5 100644
--- a/ui/src/app/core/components/iconbar/iconbar.component.ts
+++ b/ui/src/app/core/components/iconbar/iconbar.component.ts
@@ -19,9 +19,7 @@
 import { Component, OnInit } from '@angular/core';
 import { BaseNavigationComponent } from '../base-navigation.component';
 import { Router } from '@angular/router';
-import { NotificationCountService } from '../../../services/notification-count-service';
 import { AuthService } from '../../../services/auth.service';
-import { RestApi } from '../../../services/rest-api.service';
 import { AppConstants } from '../../../services/app.constants';
 import { CurrentUserService } from '@streampipes/shared-ui';
 
@@ -38,8 +36,6 @@ export class IconbarComponent
         router: Router,
         authService: AuthService,
         currentUserService: CurrentUserService,
-        public notificationCountService: NotificationCountService,
-        private restApi: RestApi,
         appConstants: AppConstants,
     ) {
         super(authService, currentUserService, router, appConstants);
diff --git a/ui/src/app/data-explorer/components/widgets/status/config/status-widget-config.component.html b/ui/src/app/data-explorer/components/widgets/status/config/status-widget-config.component.html
new file mode 100644
index 0000000000..518f3fc085
--- /dev/null
+++ b/ui/src/app/data-explorer/components/widgets/status/config/status-widget-config.component.html
@@ -0,0 +1,158 @@
+<!--
+  ~ 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.
+  ~
+  -->
+
+<sp-visualization-config-outer
+    [configurationValid]="
+        currentlyConfiguredWidget.visualizationConfig.configurationValid
+    "
+>
+    <sp-configuration-box title="Settings">
+        <div fxLayout="column" fxLayoutGap="10px">
+            <div
+                fxLayout="row"
+                fxLayoutGap="10px"
+                fxLayoutAlign="start center"
+                fxFlex="100"
+            >
+                <small fxFlex="30">Select Value Type</small>
+                <mat-radio-group
+                    [(ngModel)]="
+                        currentlyConfiguredWidget.visualizationConfig
+                            .selectedDataType
+                    "
+                    (change)="selectDataType($event.source.value)"
+                >
+                    <mat-radio-button [value]="'number'"
+                        >Numeric Value</mat-radio-button
+                    >
+                    <mat-radio-button [value]="'boolean'"
+                        >Boolean Value</mat-radio-button
+                    >
+                </mat-radio-group>
+            </div>
+
+            <div
+                *ngIf="
+                    currentlyConfiguredWidget.visualizationConfig
+                        .selectedDataType === 'number'
+                "
+                fxLayout="column"
+                fxLayoutGap="10px"
+            >
+                <div
+                    fxLayout="row"
+                    fxLayoutGap="10px"
+                    fxLayoutAlign="start center"
+                    fxFlex="100"
+                >
+                    <small
+                        fxFlex="30"
+                        matTooltip="Interval in seconds in which an event must arrive"
+                        matTooltipPosition="above"
+                        >Interval [sec]</small
+                    >
+                    <mat-form-field color="accent" appearance="outline" fxFlex>
+                        <input
+                            type="number"
+                            [(ngModel)]="
+                                currentlyConfiguredWidget.visualizationConfig
+                                    .selectedInterval
+                            "
+                            matInput
+                            (input)="selectInterval($event.target.value)"
+                            min="0"
+                        />
+                    </mat-form-field>
+                </div>
+                <div
+                    fxLayout="row"
+                    fxLayoutGap="10px"
+                    fxLayoutAlign="start center"
+                    fxFlex="100"
+                >
+                    <small>Show Last Seen Timestamp</small>
+                    <mat-checkbox
+                        color="accent"
+                        [(ngModel)]="
+                            currentlyConfiguredWidget.visualizationConfig
+                                .selectedLastSeen
+                        "
+                        (ngModelChange)="showLastSeen($event)"
+                    >
+                    </mat-checkbox>
+                </div>
+            </div>
+
+            <div
+                *ngIf="
+                    currentlyConfiguredWidget.visualizationConfig
+                        .selectedDataType === 'boolean'
+                "
+                fxLayout="column"
+                fxLayoutGap="10px"
+            >
+                <div
+                    fxLayout="row"
+                    fxLayoutGap="10px"
+                    fxLayoutAlign="start center"
+                    fxFlex="100"
+                >
+                    <small fxFlex="30">Field</small>
+                    <sp-select-property
+                        [availableProperties]="fieldProvider.booleanFields"
+                        [selectedProperty]="
+                            currentlyConfiguredWidget.visualizationConfig
+                                .selectedBooleanFieldToObserve
+                        "
+                        (changeSelectedProperty)="
+                            selectBooleanFieldToObserve($event)
+                        "
+                        fxFlex
+                    ></sp-select-property>
+                </div>
+
+                <div
+                    fxLayout="row"
+                    fxLayoutGap="10px"
+                    fxLayoutAlign="start center"
+                    fxFlex="100"
+                >
+                    <small fxFlex="30">Select Mapping</small>
+                    <mat-radio-group
+                        [(ngModel)]="
+                            currentlyConfiguredWidget.visualizationConfig
+                                .selectedMappingGreenTrue
+                        "
+                        (change)="selectMappingGreenTrue($event.source.value)"
+                    >
+                        <mat-radio-button [value]="true">
+                            <span class="color-box green-box"></span> True
+                            <span class="spacing"></span>
+                            <span class="color-box red-box"></span> False
+                        </mat-radio-button>
+                        <mat-radio-button [value]="false">
+                            <span class="color-box red-box"></span> True
+                            <span class="spacing"></span>
+                            <span class="color-box green-box"></span> False
+                        </mat-radio-button>
+                    </mat-radio-group>
+                </div>
+            </div>
+        </div>
+    </sp-configuration-box>
+</sp-visualization-config-outer>
diff --git a/ui/src/app/data-explorer/components/widgets/status/config/status-widget-config.component.scss b/ui/src/app/data-explorer/components/widgets/status/config/status-widget-config.component.scss
new file mode 100644
index 0000000000..c3105ba0e4
--- /dev/null
+++ b/ui/src/app/data-explorer/components/widgets/status/config/status-widget-config.component.scss
@@ -0,0 +1,38 @@
+/*
+ *   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.
+ */
+
+.color-box {
+    display: inline-block;
+    width: 12px;
+    height: 12px;
+    margin-right: 5px;
+    border-radius: 50%;
+}
+.spacing {
+    margin-left: 15px;
+}
+.green-box {
+    background-color: green;
+}
+
+.red-box {
+    background-color: red;
+}
+
+.checkbox-container {
+    margin-top: 20px;
+}
diff --git a/ui/src/app/data-explorer/components/widgets/status/config/status-widget-config.component.ts b/ui/src/app/data-explorer/components/widgets/status/config/status-widget-config.component.ts
new file mode 100644
index 0000000000..f671616d16
--- /dev/null
+++ b/ui/src/app/data-explorer/components/widgets/status/config/status-widget-config.component.ts
@@ -0,0 +1,91 @@
+/*
+ * 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 } from '@angular/core';
+import { BaseWidgetConfig } from '../../base/base-widget-config';
+import { WidgetConfigurationService } from '../../../../services/widget-configuration.service';
+import {
+    StatusWidgetModel,
+    StatusVisConfig,
+} from '../model/status-widget.model';
+import { DataExplorerFieldProviderService } from '../../../../services/data-explorer-field-provider-service';
+import { DataExplorerField } from '@streampipes/platform-services';
+
+@Component({
+    selector: 'sp-data-explorer-status-widget-config',
+    templateUrl: './status-widget-config.component.html',
+    styleUrls: ['./status-widget-config.component.scss'],
+})
+export class StatusWidgetConfigComponent extends BaseWidgetConfig<
+    StatusWidgetModel,
+    StatusVisConfig
+> {
+    constructor(
+        widgetConfigurationService: WidgetConfigurationService,
+        fieldService: DataExplorerFieldProviderService,
+    ) {
+        super(widgetConfigurationService, fieldService);
+    }
+
+    selectDataType(selectedDataType: string): void {
+        this.currentlyConfiguredWidget.visualizationConfig.selectedDataType =
+            selectedDataType;
+        this.triggerViewRefresh();
+    }
+
+    selectInterval(selectedInterval: number): void {
+        this.currentlyConfiguredWidget.visualizationConfig.selectedInterval =
+            selectedInterval;
+        this.triggerViewRefresh();
+    }
+
+    showLastSeen(selectedLastSeen: boolean): void {
+        this.currentlyConfiguredWidget.visualizationConfig.showLastSeen =
+            selectedLastSeen;
+        this.triggerViewRefresh();
+    }
+
+    selectBooleanFieldToObserve(
+        selectedBooleanFieldToObserve: DataExplorerField,
+    ): void {
+        this.currentlyConfiguredWidget.visualizationConfig.selectedBooleanFieldToObserve =
+            selectedBooleanFieldToObserve;
+        this.triggerViewRefresh();
+    }
+
+    selectMappingGreenTrue(selectedMappingGreenTrue: boolean): void {
+        this.currentlyConfiguredWidget.visualizationConfig.selectedMappingGreenTrue =
+            selectedMappingGreenTrue;
+        this.triggerViewRefresh();
+    }
+
+    protected applyWidgetConfig(config: StatusVisConfig): void {
+        config.selectedBooleanFieldToObserve =
+            this.fieldService.getSelectedField(
+                config.selectedBooleanFieldToObserve,
+                this.fieldProvider.allFields,
+                () => this.fieldProvider.allFields[0],
+            );
+        this.currentlyConfiguredWidget.visualizationConfig.selectedInterval ??= 5;
+        this.currentlyConfiguredWidget.visualizationConfig.selectedMappingGreenTrue ??=
+            true;
+    }
+    protected requiredFieldsForChartPresent(): boolean {
+        return true;
+    }
+}
diff --git a/ui/src/app/data-explorer/components/widgets/status/model/status-widget.model.ts b/ui/src/app/data-explorer/components/widgets/status/model/status-widget.model.ts
new file mode 100644
index 0000000000..73bb53e69d
--- /dev/null
+++ b/ui/src/app/data-explorer/components/widgets/status/model/status-widget.model.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 {
+    DataExplorerDataConfig,
+    DataExplorerWidgetModel,
+    DataExplorerField,
+} from '@streampipes/platform-services';
+import { DataExplorerVisConfig } from '../../../../models/dataview-dashboard.model';
+
+export interface StatusVisConfig extends DataExplorerVisConfig {
+    selectedDataType: string;
+    selectedInterval: number;
+    showLastSeen: boolean;
+    selectedBooleanFieldToObserve: DataExplorerField;
+    selectedMappingGreenTrue: boolean;
+}
+
+export interface StatusWidgetModel extends DataExplorerWidgetModel {
+    dataConfig: DataExplorerDataConfig;
+    visualizationConfig: StatusVisConfig;
+}
diff --git a/ui/src/app/data-explorer/components/widgets/status/status-widget.component.html b/ui/src/app/data-explorer/components/widgets/status/status-widget.component.html
new file mode 100644
index 0000000000..ad01626a9f
--- /dev/null
+++ b/ui/src/app/data-explorer/components/widgets/status/status-widget.component.html
@@ -0,0 +1,60 @@
+<!--
+  ~   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.
+  -->
+
+<div
+    fxFlex="100"
+    fxLayoutAlign="center center"
+    fxLayout="column"
+    class="main-panel"
+    [ngStyle]="{
+        background: dataExplorerWidget.baseAppearanceConfig.backgroundColor,
+        color: dataExplorerWidget.baseAppearanceConfig.textColor,
+        overflowX: 'auto'
+    }"
+>
+    <sp-no-data-in-date-range
+        *ngIf="showNoDataInDateRange"
+        [viewDateRange]="timeSettings"
+        class="h-50"
+    >
+    </sp-no-data-in-date-range>
+
+    <div fxLayoutAlign="center center">
+        <div
+            class="tl-container"
+            [ngStyle]="{
+                width: containerWidth + 'px',
+                height: containerHeight + 'px'
+            }"
+        >
+            <div
+                class="light"
+                [ngClass]="{
+                    'light-red': !active,
+                    'light-green': active
+                }"
+                [ngStyle]="{
+                    width: lightWidth + 'px',
+                    height: lightHeight + 'px'
+                }"
+            ></div>
+        </div>
+    </div>
+    <div *ngIf="selectedDataType === 'number' && showLastSeen">
+        <h5>Last seen: {{ lastTimestamp | date: 'short' }}</h5>
+    </div>
+</div>
diff --git a/ui/src/app/data-explorer/components/widgets/status/status-widget.component.scss b/ui/src/app/data-explorer/components/widgets/status/status-widget.component.scss
new file mode 100644
index 0000000000..ac656663e5
--- /dev/null
+++ b/ui/src/app/data-explorer/components/widgets/status/status-widget.component.scss
@@ -0,0 +1,57 @@
+/*
+ *   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.
+ */
+
+.h-100 {
+    height: 100%;
+}
+
+.tl-container {
+    background-color: #222;
+    display: flex;
+    align-items: center;
+    flex-direction: column;
+    padding: 20px;
+    border-radius: 10px;
+}
+
+.light {
+    border-radius: 50%;
+    background-color: #3d3535;
+    background: repeating-linear-gradient(#333, #443 5px);
+}
+
+.light-red,
+.light-green {
+    box-shadow: 0 0 40px;
+    z-index: 1;
+}
+
+.light-red {
+    background: repeating-linear-gradient(#f00, #e00 5px);
+    box-shadow: 0 0 40px #f00;
+}
+
+.light-green {
+    background: repeating-linear-gradient(#0d0, #0c0 5px);
+    box-shadow: 0 0 40px #0d0;
+}
+
+.title-panel {
+    font-size: 20px;
+    height: 30px;
+    margin: 10px 0;
+}
diff --git a/ui/src/app/data-explorer/components/widgets/status/status-widget.component.ts b/ui/src/app/data-explorer/components/widgets/status/status-widget.component.ts
new file mode 100644
index 0000000000..abb41e549c
--- /dev/null
+++ b/ui/src/app/data-explorer/components/widgets/status/status-widget.component.ts
@@ -0,0 +1,164 @@
+/*
+ * 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, OnInit } from '@angular/core';
+import { BaseDataExplorerWidgetDirective } from '../base/base-data-explorer-widget.directive';
+import { StatusWidgetModel } from './model/status-widget.model';
+import {
+    DataExplorerField,
+    SpQueryResult,
+} from '@streampipes/platform-services';
+
+@Component({
+    selector: 'sp-data-explorer-status-widget',
+    templateUrl: './status-widget.component.html',
+    styleUrls: ['./status-widget.component.scss'],
+})
+export class StatusWidgetComponent
+    extends BaseDataExplorerWidgetDirective<StatusWidgetModel>
+    implements OnInit
+{
+    width: number;
+    height: number;
+
+    row: any[][];
+    header: string[];
+    fieldIndex = -1;
+
+    containerWidth: number;
+    containerHeight: number;
+
+    lightWidth: number;
+    lightHeight: number;
+
+    selectedDataType: string;
+    selectedInterval: number;
+    showLastSeen: boolean;
+    selectedBooleanFieldToObserve: DataExplorerField;
+    selectedMappingGreenTrue: boolean;
+
+    lastTimestamp = 0;
+    active: boolean;
+
+    ngOnInit(): void {
+        super.ngOnInit();
+        this.onResize(
+            this.gridsterItemComponent.width - this.widthOffset,
+            this.gridsterItemComponent.height - this.heightOffset,
+        );
+        this.updateSettings();
+    }
+
+    updateSettings(): void {
+        this.selectedDataType =
+            this.dataExplorerWidget.visualizationConfig.selectedDataType;
+        this.selectedInterval =
+            this.dataExplorerWidget.visualizationConfig.selectedInterval;
+        this.showLastSeen =
+            this.dataExplorerWidget.visualizationConfig.showLastSeen;
+        this.selectedBooleanFieldToObserve =
+            this.dataExplorerWidget.visualizationConfig.selectedBooleanFieldToObserve;
+        this.selectedMappingGreenTrue =
+            this.dataExplorerWidget.visualizationConfig.selectedMappingGreenTrue;
+    }
+
+    getNumericalStatus(): void {
+        const timestamp = new Date().getTime();
+        this.active =
+            this.lastTimestamp >= timestamp - this.selectedInterval * 1000;
+    }
+
+    getBooleanStatus(): void {
+        if (this.selectedMappingGreenTrue) {
+            this.active = this.row[0][this.fieldIndex];
+        } else {
+            this.active = !this.row[0][this.fieldIndex];
+        }
+    }
+
+    booleanFieldToObserve(): void {
+        this.fieldIndex = this.header.indexOf(
+            this.selectedBooleanFieldToObserve.runtimeName,
+        );
+    }
+
+    refreshView(): void {
+        this.updateSettings();
+        if (this.row !== undefined && this.row.length > 0) {
+            if (this.selectedDataType == 'boolean') {
+                this.getBooleanStatus();
+            } else {
+                this.getNumericalStatus();
+            }
+        }
+    }
+
+    beforeDataFetched(): void {
+        this.setShownComponents(false, false, true, false);
+    }
+
+    onDataReceived(spQueryResult: SpQueryResult[]): void {
+        if (
+            spQueryResult.length > 0 &&
+            spQueryResult[0].allDataSeries.length > 0
+        ) {
+            this.header = spQueryResult[0].allDataSeries[0].headers;
+            this.row = spQueryResult[0].allDataSeries[0].rows;
+            this.lastTimestamp = spQueryResult[0].allDataSeries[0].rows[0][0];
+
+            if (this.selectedDataType == 'number') {
+                this.getNumericalStatus();
+            } else if (this.selectedDataType == 'boolean') {
+                this.booleanFieldToObserve();
+                this.getBooleanStatus();
+            }
+            this.setShownComponents(false, true, false, false);
+        } else {
+            this.setShownComponents(true, false, false, false);
+        }
+    }
+
+    onResize(width: number, heigth: number): void {
+        this.containerHeight = heigth * 0.3;
+        this.containerWidth = this.containerHeight;
+        this.lightWidth = this.containerHeight;
+        this.lightHeight = this.lightWidth;
+    }
+
+    handleUpdatedFields(
+        addedFields: DataExplorerField[],
+        removedFields: DataExplorerField[],
+    ) {
+        const updatedFields = this.fieldUpdateService.updateFieldSelection(
+            [
+                this.dataExplorerWidget.visualizationConfig
+                    .selectedBooleanFieldToObserve,
+            ],
+            {
+                addedFields,
+                removedFields,
+                fieldProvider: this.fieldProvider,
+            },
+            () => true,
+        );
+
+        this.selectedBooleanFieldToObserve = updatedFields[0];
+        this.booleanFieldToObserve();
+        this.refreshView();
+    }
+}
diff --git a/ui/src/app/data-explorer/components/widgets/traffic-light/traffic-light-widget.component.ts b/ui/src/app/data-explorer/components/widgets/traffic-light/traffic-light-widget.component.ts
index 80e3490ddc..99df13f09f 100644
--- a/ui/src/app/data-explorer/components/widgets/traffic-light/traffic-light-widget.component.ts
+++ b/ui/src/app/data-explorer/components/widgets/traffic-light/traffic-light-widget.component.ts
@@ -110,8 +110,6 @@ export class TrafficLightWidgetComponent
                             (this.selectedWarningRange / 100)
                 );
             } else {
-                console.log(value);
-
                 return (
                     value <=
                     this.selectedThreshold +
@@ -125,13 +123,13 @@ export class TrafficLightWidgetComponent
         return !this.exceedsThreshold(value) && !this.isInWarningRange(value);
     }
 
-    public refreshView(): void {
+    refreshView(): void {
         this.updateSettings();
         this.fieldToObserve();
         this.getTrafficLightColor();
     }
 
-    public beforeDataFetched(): void {
+    beforeDataFetched(): void {
         this.setShownComponents(false, false, true, false);
     }
 
@@ -141,12 +139,19 @@ export class TrafficLightWidgetComponent
         );
     }
 
-    public onDataReceived(spQueryResult: SpQueryResult[]): void {
-        this.header = spQueryResult[0].allDataSeries[0].headers;
-        this.row = spQueryResult[0].allDataSeries[0].rows;
-        this.fieldToObserve();
-        this.getTrafficLightColor();
-        this.setShownComponents(false, true, false, false);
+    onDataReceived(spQueryResult: SpQueryResult[]): void {
+        if (
+            spQueryResult.length > 0 &&
+            spQueryResult[0].allDataSeries.length > 0
+        ) {
+            this.header = spQueryResult[0].allDataSeries[0].headers;
+            this.row = spQueryResult[0].allDataSeries[0].rows;
+            this.fieldToObserve();
+            this.getTrafficLightColor();
+            this.setShownComponents(false, true, false, false);
+        } else {
+            this.setShownComponents(true, false, false, false);
+        }
     }
 
     onResize(width: number, heigth: number) {
diff --git a/ui/src/app/data-explorer/data-explorer.module.ts b/ui/src/app/data-explorer/data-explorer.module.ts
index 09b8f33e75..c3070b45b5 100644
--- a/ui/src/app/data-explorer/data-explorer.module.ts
+++ b/ui/src/app/data-explorer/data-explorer.module.ts
@@ -48,6 +48,8 @@ import { DataExplorerDashboardWidgetComponent } from './components/widget/data-e
 import { ImageWidgetComponent } from './components/widgets/image/image-widget.component';
 import { TrafficLightWidgetComponent } from './components/widgets/traffic-light/traffic-light-widget.component';
 import { TrafficLightWidgetConfigComponent } from './components/widgets/traffic-light/config/traffic-light-widget-config.component';
+import { StatusWidgetComponent } from './components/widgets/status/status-widget.component';
+import { StatusWidgetConfigComponent } from './components/widgets/status/config/status-widget-config.component';
 import { TableWidgetComponent } from './components/widgets/table/table-widget.component';
 import { AggregateConfigurationComponent } from './components/widgets/utils/aggregate-configuration/aggregate-configuration.component';
 import { LoadDataSpinnerComponent } from './components/widgets/utils/load-data-spinner/load-data-spinner.component';
@@ -251,6 +253,8 @@ import { GaugeWidgetConfigComponent } from './components/widgets/gauge/config/ga
         TableWidgetConfigComponent,
         TrafficLightWidgetComponent,
         TrafficLightWidgetConfigComponent,
+        StatusWidgetComponent,
+        StatusWidgetConfigComponent,
         MapWidgetConfigComponent,
         MapWidgetComponent,
         HeatmapWidgetConfigComponent,
diff --git a/ui/src/app/data-explorer/registry/data-explorer-widget-registry.ts b/ui/src/app/data-explorer/registry/data-explorer-widget-registry.ts
index c2d486bfd6..efb221b3b6 100644
--- a/ui/src/app/data-explorer/registry/data-explorer-widget-registry.ts
+++ b/ui/src/app/data-explorer/registry/data-explorer-widget-registry.ts
@@ -54,6 +54,8 @@ import { GaugeWidgetConfigComponent } from '../components/widgets/gauge/config/g
 import { GaugeWidgetModel } from '../components/widgets/gauge/model/gauge-widget.model';
 import { TrafficLightWidgetConfigComponent } from '../components/widgets/traffic-light/config/traffic-light-widget-config.component';
 import { TrafficLightWidgetComponent } from '../components/widgets/traffic-light/traffic-light-widget.component';
+import { StatusWidgetConfigComponent } from '../components/widgets/status/config/status-widget-config.component';
+import { StatusWidgetComponent } from '../components/widgets/status/status-widget.component';
 
 @Injectable({ providedIn: 'root' })
 export class DataExplorerWidgetRegistry {
@@ -92,6 +94,12 @@ export class DataExplorerWidgetRegistry {
                 widgetConfigurationComponent: TrafficLightWidgetConfigComponent,
                 widgetComponent: TrafficLightWidgetComponent,
             },
+            {
+                id: 'status',
+                label: 'Status',
+                widgetConfigurationComponent: StatusWidgetConfigComponent,
+                widgetComponent: StatusWidgetComponent,
+            },
             {
                 id: 'map',
                 label: 'Map',
diff --git a/ui/src/app/editor/dialog/customize/customize.component.ts b/ui/src/app/editor/dialog/customize/customize.component.ts
index 88ade59a1a..ae2d73e9ea 100644
--- a/ui/src/app/editor/dialog/customize/customize.component.ts
+++ b/ui/src/app/editor/dialog/customize/customize.component.ts
@@ -36,7 +36,6 @@ import {
     DataSinkInvocation,
     EventSchema,
     PipelineElementTemplate,
-    PipelineElementTemplateConfig,
     PipelineElementTemplateService,
 } from '@streampipes/platform-services';
 import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
@@ -82,7 +81,7 @@ export class CustomizeComponent implements OnInit, AfterViewInit {
     selectedTemplate: any = false;
     templateMode = false;
     template: PipelineElementTemplate;
-    templateConfigs: Map<string, any> = new Map();
+    templateConfigs: Map<string, any>[] = [];
 
     constructor(
         private dialogRef: DialogRef<CustomizeComponent>,
@@ -190,15 +189,14 @@ export class CustomizeComponent implements OnInit, AfterViewInit {
             });
     }
 
-    convert(templateConfigs: Map<string, any>): any {
-        const configs: { [index: string]: PipelineElementTemplateConfig } = {};
-        templateConfigs.forEach((value, key) => {
-            configs[key] = new PipelineElementTemplateConfig();
-            configs[key].editable = value.editable;
-            configs[key].displayed = value.displayed;
-            configs[key].value = value.value;
+    convert(templateConfigs: Map<string, any>[]): Record<string, any>[] {
+        return templateConfigs.map(map => {
+            const obj: Record<string, any> = {};
+            map.forEach((value, key) => {
+                obj[key] = value;
+            });
+            return obj;
         });
-        return configs;
     }
 
     cancelTemplateMode() {
diff --git a/ui/src/app/files/files.module.ts b/ui/src/app/files/files.module.ts
index 20794c27cb..991569618d 100644
--- a/ui/src/app/files/files.module.ts
+++ b/ui/src/app/files/files.module.ts
@@ -24,16 +24,14 @@ import { MatGridListModule } from '@angular/material/grid-list';
 import { MatIconModule } from '@angular/material/icon';
 import { MatDividerModule } from '@angular/material/divider';
 import { MatListModule } from '@angular/material/list';
-import { FilesComponent } from './files.component';
+import { FilesComponent } from '../configuration/files/files.component';
 import { MatTabsModule } from '@angular/material/tabs';
 import { CoreUiModule } from '../core-ui/core-ui.module';
-import { FileUploadDialogComponent } from './dialog/file-upload/file-upload-dialog.component';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { MatFormFieldModule } from '@angular/material/form-field';
 import { MatProgressBarModule } from '@angular/material/progress-bar';
 import { MatInputModule } from '@angular/material/input';
 import { ServicesModule } from '../services/services.module';
-import { FileOverviewComponent } from './components/file-overview/file-overview.component';
 import { MatTableModule } from '@angular/material/table';
 import { MatPaginatorModule } from '@angular/material/paginator';
 import { MatChipsModule } from '@angular/material/chips';
@@ -41,7 +39,6 @@ import { MatTooltipModule } from '@angular/material/tooltip';
 import { PlatformServicesModule } from '@streampipes/platform-services';
 import { RouterModule } from '@angular/router';
 import { SharedUiModule } from '@streampipes/shared-ui';
-import { FileRenameDialogComponent } from './dialog/file-rename/file-rename-dialog.component';
 import { MatDialogModule } from '@angular/material/dialog';
 
 @NgModule({
@@ -80,12 +77,7 @@ import { MatDialogModule } from '@angular/material/dialog';
         ]),
         MatDialogModule,
     ],
-    declarations: [
-        FilesComponent,
-        FileOverviewComponent,
-        FileUploadDialogComponent,
-        FileRenameDialogComponent,
-    ],
+    declarations: [],
     providers: [],
 })
 export class FilesModule {}
diff --git a/ui/src/app/home/models/home.model.ts b/ui/src/app/home/models/home.model.ts
index d27ae16a27..7bea8078c3 100644
--- a/ui/src/app/home/models/home.model.ts
+++ b/ui/src/app/home/models/home.model.ts
@@ -18,7 +18,6 @@
 
 import { Observable } from 'rxjs';
 import { PageName } from '../../_enums/page-name.enum';
-import { UserRole } from '../../_enums/user-role.enum';
 
 export interface StatusBox {
     link: string[];
@@ -26,8 +25,8 @@ export interface StatusBox {
     title: string;
     createTitle: string;
     dataFns: Observable<any>[];
-    viewRoles: UserRole[];
-    createRoles: UserRole[];
+    viewRoles: string[];
+    createRoles: string[];
     icon: string;
 }
 
@@ -41,6 +40,7 @@ export interface ServiceLink {
     description: string;
     icon: string;
     pageNames: PageName[];
+    privileges: string[];
     link: Link;
     showStatusBox: boolean;
     statusBox?: StatusBox;
diff --git a/ui/src/app/pipelines/pipelines.component.html b/ui/src/app/pipelines/pipelines.component.html
index 1c1847df3a..9538b01fc1 100644
--- a/ui/src/app/pipelines/pipelines.component.html
+++ b/ui/src/app/pipelines/pipelines.component.html
@@ -34,6 +34,7 @@
                 mat-button
                 mat-raised-button
                 color="accent"
+                *ngIf="hasPipelineWritePrivileges"
                 (click)="navigateToPipelineEditor()"
                 data-cy="pipelines-navigate-to-editor"
             >
diff --git a/ui/src/app/services/auth.service.ts b/ui/src/app/services/auth.service.ts
index c48cff00f9..a3da9a4fdb 100644
--- a/ui/src/app/services/auth.service.ts
+++ b/ui/src/app/services/auth.service.ts
@@ -23,10 +23,9 @@ import { JwtHelperService } from '@auth0/angular-jwt';
 import { filter, map, switchMap } from 'rxjs/operators';
 import { Router } from '@angular/router';
 import { LoginService } from '../login/services/login.service';
-import { PageName } from '../_enums/page-name.enum';
 import {
-    JwtTokenStorageService,
     CurrentUserService,
+    JwtTokenStorageService,
 } from '@streampipes/shared-ui';
 
 @Injectable({ providedIn: 'root' })
@@ -148,94 +147,23 @@ export class AuthService {
             });
     }
 
-    getUserRoles(): string[] {
-        return this.currentUserService.getCurrentUser().roles;
-    }
-
     public hasRole(role: string): boolean {
-        return (
-            this.getUserRoles().includes('ROLE_ADMIN') ||
-            this.getUserRoles().includes(role)
-        );
+        return this.currentUserService.hasRole(role);
     }
 
     public hasAnyRole(roles: string[]): boolean {
-        if (Array.isArray(roles)) {
-            return roles.reduce(
-                (aggregator: false, role: string) =>
-                    aggregator || this.hasRole(role),
-                false,
-            );
-        }
-
-        return false;
+        return this.currentUserService.hasAnyRole(roles);
     }
 
-    isAnyAccessGranted(pageNames: PageName[], redirect?: boolean): boolean {
-        if (!pageNames || pageNames.length === 0) {
+    isAnyAccessGranted(privileges: string[], redirect?: boolean): boolean {
+        if (!privileges || privileges.length === 0) {
             return true;
         }
 
-        const result = pageNames.some(pageName =>
-            this.isAccessGranted(pageName),
-        );
+        const result = this.hasAnyRole(privileges);
         if (!result && redirect) {
             this.router.navigate(['']);
         }
         return result;
     }
-
-    isAccessGranted(pageName: PageName) {
-        if (this.hasRole('ROLE_ADMIN')) {
-            return true;
-        }
-        switch (pageName) {
-            case PageName.HOME:
-                return true;
-            case PageName.PIPELINE_EDITOR:
-                return this.hasAnyRole(['ROLE_PIPELINE_ADMIN']);
-            case PageName.PIPELINE_OVERVIEW:
-                return this.hasAnyRole([
-                    'ROLE_PIPELINE_ADMIN',
-                    'ROLE_PIPELINE_USER',
-                ]);
-            case PageName.CONNECT:
-                return this.hasAnyRole(['ROLE_CONNECT_ADMIN']);
-            case PageName.DASHBOARD:
-                return this.hasAnyRole([
-                    'ROLE_DASHBOARD_USER',
-                    'ROLE_DASHBOARD_ADMIN',
-                ]);
-            case PageName.DATA_EXPLORER:
-                return this.hasAnyRole([
-                    'ROLE_DATA_EXPLORER_ADMIN',
-                    'ROLE_DATA_EXPLORER_USER',
-                ]);
-            case PageName.APPS:
-                return this.hasAnyRole(['ROLE_APP_USER']);
-            case PageName.FILE_UPLOAD:
-                return this.hasAnyRole([
-                    'ROLE_CONNECT_ADMIN',
-                    'ROLE_PIPELINE_ADMIN',
-                ]);
-            case PageName.INSTALL_PIPELINE_ELEMENTS:
-                return this.hasAnyRole(['ROLE_ADMIN']);
-            case PageName.NOTIFICATIONS:
-                return this.hasAnyRole(['ROLE_PIPELINE_ADMIN']);
-            case PageName.ASSETS:
-                return this.hasAnyRole([
-                    'ROLE_ADMIN',
-                    'PRIVILEGE_READ_ASSETS',
-                    'PRIVILEGE_WRITE_ASSETS',
-                ]);
-            case PageName.SETTINGS:
-                return this.hasAnyRole([
-                    'ROLE_ADMIN',
-                    'PRIVILEGE_WRITE_ASSETS',
-                    'PRIVILEGE_WRITE_LABELS',
-                ]);
-            default:
-                return true;
-        }
-    }
 }


Test patch:
diff --git a/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/compact/generator/AdapterSchemaGeneratorTest.java b/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/compact/generator/AdapterSchemaGeneratorTest.java
new file mode 100644
index 0000000000..ef123ae80d
--- /dev/null
+++ b/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/compact/generator/AdapterSchemaGeneratorTest.java
@@ -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.
+ *
+ */
+
+package org.apache.streampipes.connect.management.management.compact.generator;
+
+import org.apache.streampipes.connect.management.compact.SchemaMetadataEnricher;
+import org.apache.streampipes.connect.management.compact.generator.AdapterSchemaGenerator;
+import org.apache.streampipes.connect.management.management.GuessManagement;
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.connect.adapter.compact.CompactAdapter;
+import org.apache.streampipes.model.connect.adapter.compact.CompactEventProperty;
+import org.apache.streampipes.model.connect.guess.GuessSchema;
+import org.apache.streampipes.model.schema.EventPropertyPrimitive;
+import org.apache.streampipes.model.schema.EventSchema;
+import org.apache.streampipes.model.schema.PropertyScope;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class AdapterSchemaGeneratorTest {
+
+  @Test
+  public void testAdapterSchemaGenerator() throws Exception {
+    var compactAdapter = mock(CompactAdapter.class);
+    var guessMgmt = mock(GuessManagement.class);
+
+    var epDef = new CompactEventProperty(
+        "label",
+        "description",
+        "dimension",
+        "semanticType"
+
+    );
+    var ep = new EventPropertyPrimitive();
+    var schema = new EventSchema(List.of(ep));
+    var adapterDescription = new AdapterDescription();
+    var guessedSchema = new GuessSchema();
+
+    ep.setRuntimeName("a");
+    guessedSchema.setEventSchema(schema);
+
+    when(guessMgmt.guessSchema(adapterDescription)).thenReturn(guessedSchema);
+    when(compactAdapter.schema()).thenReturn(Map.of("a", epDef));
+
+    new AdapterSchemaGenerator(new SchemaMetadataEnricher(), guessMgmt).apply(adapterDescription, compactAdapter);
+    var properties = adapterDescription.getEventSchema().getEventProperties();
+    assertEquals(1, properties.size());
+    assertEquals(
+        "label",
+        properties.get(0).getLabel()
+    );
+    assertEquals("description", properties.get(0).getDescription());
+    assertEquals(PropertyScope.DIMENSION_PROPERTY.name(), properties.get(0).getPropertyScope());
+    assertEquals("semanticType", properties.get(0).getDomainProperties().get(0).toString());
+  }
+}
diff --git a/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/compact/generator/AdapterTransformationRuleGeneratorTest.java b/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/compact/generator/AdapterTransformationRuleGeneratorTest.java
new file mode 100644
index 0000000000..e158ac3a27
--- /dev/null
+++ b/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/compact/generator/AdapterTransformationRuleGeneratorTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.connect.management.management.compact.generator;
+
+import org.apache.streampipes.connect.management.compact.generator.AdapterTransformationRuleGenerator;
+import org.apache.streampipes.model.SpDataStream;
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.connect.adapter.compact.CompactAdapter;
+import org.apache.streampipes.model.connect.adapter.compact.TransformationConfig;
+import org.apache.streampipes.model.schema.EventPropertyPrimitive;
+import org.apache.streampipes.model.schema.EventSchema;
+
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class AdapterTransformationRuleGeneratorTest {
+
+  @Test
+  public void testCompactAdapterTransformationRuleRename() throws Exception {
+    var compactAdapter = mock(CompactAdapter.class);
+    var rename = Map.of("a", "b");
+    var ep = new EventPropertyPrimitive();
+    ep.setRuntimeName("a");
+    var schema = new EventSchema(List.of(ep));
+    var stream = new SpDataStream();
+    stream.setEventSchema(schema);
+    var adapterDescription = new AdapterDescription();
+    adapterDescription.setDataStream(stream);
+    when(compactAdapter.transform()).thenReturn(new TransformationConfig(rename, Map.of()));
+
+    new AdapterTransformationRuleGenerator().apply(adapterDescription, compactAdapter);
+    assertEquals(
+        "b",
+        adapterDescription.getDataStream().getEventSchema().getEventProperties().get(0).getRuntimeName()
+    );
+    assertEquals(1, adapterDescription.getRules().size());
+  }
+
+  @Test
+  public void testCompactAdapterTransformationRuleMeasurementUnit() throws Exception {
+    var compactAdapter = mock(CompactAdapter.class);
+    var ep = new EventPropertyPrimitive();
+    var measurementUnit = "https://streampipes.apache.org";
+    var targetMeasurementUnit = "https://streampipes.apache.org/docs";
+    ep.setRuntimeName("a");
+    ep.setMeasurementUnit(URI.create(measurementUnit));
+    var schema = new EventSchema(List.of(ep));
+    var stream = new SpDataStream();
+    stream.setEventSchema(schema);
+    var adapterDescription = new AdapterDescription();
+    adapterDescription.setDataStream(stream);
+    when(compactAdapter.transform()).thenReturn(
+        new TransformationConfig(
+            Map.of(),
+            Map.of("a", targetMeasurementUnit))
+    );
+
+    new AdapterTransformationRuleGenerator().apply(adapterDescription, compactAdapter);
+    var transformedEp = adapterDescription.getDataStream().getEventSchema().getEventProperties().get(0);
+    assertEquals(EventPropertyPrimitive.class, transformedEp.getClass());
+    assertEquals(
+        "a",
+        transformedEp.getRuntimeName()
+    );
+    assertEquals(
+        targetMeasurementUnit,
+        ((EventPropertyPrimitive) transformedEp).getMeasurementUnit().toString()
+    );
+    assertEquals(1, adapterDescription.getRules().size());
+  }
+}
diff --git a/streampipes-extensions/streampipes-processors-enricher-jvm/src/test/java/org/apache/streampipes/processors/enricher/jvm/processor/expression/MathExpressionProcessorTest.java b/streampipes-extensions/streampipes-processors-enricher-jvm/src/test/java/org/apache/streampipes/processors/enricher/jvm/processor/expression/MathExpressionProcessorTest.java
index bf69c18890..f977d0e226 100644
--- a/streampipes-extensions/streampipes-processors-enricher-jvm/src/test/java/org/apache/streampipes/processors/enricher/jvm/processor/expression/MathExpressionProcessorTest.java
+++ b/streampipes-extensions/streampipes-processors-enricher-jvm/src/test/java/org/apache/streampipes/processors/enricher/jvm/processor/expression/MathExpressionProcessorTest.java
@@ -53,19 +53,16 @@ public class MathExpressionProcessorTest {
         "result2", 20.2
     ));
 
-    var config = Map.of("members", List.of(Map.of(
-            MathExpressionProcessor.ENRICHED_FIELDS,
-            Map.of("staticProperties", Map.of(
+    var config =
+        List.of(Map.of(
                 MathExpressionProcessor.FIELD_NAME, "result1",
                 MathExpressionProcessor.EXPRESSION, "temperature+12"
-            ))),
-        Map.of(
-            MathExpressionProcessor.ENRICHED_FIELDS,
-            Map.of("staticProperties", Map.of(
+            ),
+            Map.of(
                 MathExpressionProcessor.FIELD_NAME, "result2",
                 MathExpressionProcessor.EXPRESSION, "temperature*flowrate"
-            )))
-    ));
+            )
+        );
 
     var configuration = TestConfiguration
         .builder()
diff --git a/streampipes-integration-tests/src/test/java/org/apache/streampipes/integration/adapters/KafkaAdapterTester.java b/streampipes-integration-tests/src/test/java/org/apache/streampipes/integration/adapters/KafkaAdapterTester.java
index d58611a5e4..75883a244a 100644
--- a/streampipes-integration-tests/src/test/java/org/apache/streampipes/integration/adapters/KafkaAdapterTester.java
+++ b/streampipes-integration-tests/src/test/java/org/apache/streampipes/integration/adapters/KafkaAdapterTester.java
@@ -32,14 +32,12 @@ import org.apache.streampipes.model.staticproperty.Option;
 import org.apache.streampipes.model.staticproperty.RuntimeResolvableOneOfStaticProperty;
 import org.apache.streampipes.model.staticproperty.StaticPropertyAlternatives;
 import org.apache.streampipes.model.template.PipelineElementTemplate;
-import org.apache.streampipes.model.template.PipelineElementTemplateConfig;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -70,13 +68,10 @@ public class KafkaAdapterTester extends AdapterTesterBase {
             .getConfig()
             .get(4))
             .setOptions(list);
-    Map<String, PipelineElementTemplateConfig> configs = new HashMap<>();
-    configs.put(KafkaConnectUtils.HOST_KEY,
-      new PipelineElementTemplateConfig(true, false, kafkaContainer.getBrokerHost()));
-    configs.put(KafkaConnectUtils.PORT_KEY,
-      new PipelineElementTemplateConfig(true, false, kafkaContainer.getBrokerPort()));
-    configs.put(KafkaConnectUtils.TOPIC_KEY,
-      new PipelineElementTemplateConfig(true, true, TOPIC));
+    List<Map<String, Object>> configs = new ArrayList<>();
+    configs.add(Map.of(KafkaConnectUtils.HOST_KEY, kafkaContainer.getBrokerHost()));
+    configs.add(Map.of(KafkaConnectUtils.PORT_KEY, kafkaContainer.getBrokerPort()));
+    configs.add(Map.of(KafkaConnectUtils.TOPIC_KEY, TOPIC));
     var template = new PipelineElementTemplate("name", "description", configs);
 
 
diff --git a/streampipes-integration-tests/src/test/java/org/apache/streampipes/integration/adapters/MqttAdapterTester.java b/streampipes-integration-tests/src/test/java/org/apache/streampipes/integration/adapters/MqttAdapterTester.java
index 89eb52f814..4b94590f35 100644
--- a/streampipes-integration-tests/src/test/java/org/apache/streampipes/integration/adapters/MqttAdapterTester.java
+++ b/streampipes-integration-tests/src/test/java/org/apache/streampipes/integration/adapters/MqttAdapterTester.java
@@ -31,13 +31,13 @@ import org.apache.streampipes.messaging.mqtt.MqttPublisher;
 import org.apache.streampipes.model.grounding.MqttTransportProtocol;
 import org.apache.streampipes.model.staticproperty.StaticPropertyAlternatives;
 import org.apache.streampipes.model.template.PipelineElementTemplate;
-import org.apache.streampipes.model.template.PipelineElementTemplateConfig;
+
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.jetbrains.annotations.NotNull;
 
-import java.util.HashMap;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -64,11 +64,9 @@ public class MqttAdapterTester extends AdapterTesterBase {
 
     IAdapterConfiguration configuration = new MqttProtocol().declareConfig();
 
-    Map<String, PipelineElementTemplateConfig> configs = new HashMap<>();
-    configs.put(MqttConnectUtils.TOPIC,
-        new PipelineElementTemplateConfig(true, true, TOPIC));
-    configs.put(MqttConnectUtils.BROKER_URL,
-        new PipelineElementTemplateConfig(true, false, mosquittoContainer.getBrokerUrl()));
+    List<Map<String, Object>> configs = new ArrayList<>();
+    configs.add(Map.of(MqttConnectUtils.TOPIC, TOPIC));
+    configs.add(Map.of(MqttConnectUtils.BROKER_URL, mosquittoContainer.getBrokerUrl()));
 
     var template = new PipelineElementTemplate("name", "description", configs);
 
diff --git a/streampipes-integration-tests/src/test/java/org/apache/streampipes/integration/adapters/PulsarAdapterTester.java b/streampipes-integration-tests/src/test/java/org/apache/streampipes/integration/adapters/PulsarAdapterTester.java
index 79da691017..421e380d30 100644
--- a/streampipes-integration-tests/src/test/java/org/apache/streampipes/integration/adapters/PulsarAdapterTester.java
+++ b/streampipes-integration-tests/src/test/java/org/apache/streampipes/integration/adapters/PulsarAdapterTester.java
@@ -27,7 +27,6 @@ import org.apache.streampipes.integration.utils.Utils;
 import org.apache.streampipes.manager.template.AdapterTemplateHandler;
 import org.apache.streampipes.model.staticproperty.StaticPropertyAlternatives;
 import org.apache.streampipes.model.template.PipelineElementTemplate;
-import org.apache.streampipes.model.template.PipelineElementTemplateConfig;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -35,7 +34,7 @@ import org.apache.pulsar.client.api.Producer;
 import org.apache.pulsar.client.api.PulsarClient;
 import org.apache.pulsar.client.api.PulsarClientException;
 
-import java.util.HashMap;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -58,15 +57,11 @@ public class PulsarAdapterTester extends AdapterTesterBase {
   public IAdapterConfiguration prepareAdapter() {
     IAdapterConfiguration configuration = new PulsarProtocol().declareConfig();
 
-    Map<String, PipelineElementTemplateConfig> configs = new HashMap<>();
-    configs.put(PulsarProtocol.PULSAR_BROKER_HOST,
-        new PipelineElementTemplateConfig(true, true, pulsarContainer.getBrokerHost()));
-    configs.put(PulsarProtocol.PULSAR_BROKER_PORT,
-        new PipelineElementTemplateConfig(true, false, pulsarContainer.getBrokerPort()));
-    configs.put(PulsarProtocol.PULSAR_TOPIC,
-        new PipelineElementTemplateConfig(true, false, TOPIC));
-    configs.put(PulsarProtocol.PULSAR_SUBSCRIPTION_NAME,
-        new PipelineElementTemplateConfig(true, false, "test-sub"));
+    List<Map<String, Object>> configs = new ArrayList<>();
+    configs.add(Map.of(PulsarProtocol.PULSAR_BROKER_HOST, pulsarContainer.getBrokerHost()));
+    configs.add(Map.of(PulsarProtocol.PULSAR_BROKER_PORT, pulsarContainer.getBrokerPort()));
+    configs.add(Map.of(PulsarProtocol.PULSAR_TOPIC, TOPIC));
+    configs.add(Map.of(PulsarProtocol.PULSAR_SUBSCRIPTION_NAME, "test-sub"));
 
     var template = new PipelineElementTemplate("name", "description", configs);
 
diff --git a/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/template/PipelineElementTemplateVisitorTest.java b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/template/PipelineElementTemplateVisitorTest.java
index 672817786c..2c9a805da5 100644
--- a/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/template/PipelineElementTemplateVisitorTest.java
+++ b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/template/PipelineElementTemplateVisitorTest.java
@@ -18,15 +18,29 @@
 
 package org.apache.streampipes.manager.template;
 
+import org.apache.streampipes.model.staticproperty.AnyStaticProperty;
+import org.apache.streampipes.model.staticproperty.CollectionStaticProperty;
+import org.apache.streampipes.model.staticproperty.ColorPickerStaticProperty;
+import org.apache.streampipes.model.staticproperty.FreeTextStaticProperty;
 import org.apache.streampipes.model.staticproperty.MappingPropertyNary;
+import org.apache.streampipes.model.staticproperty.OneOfStaticProperty;
+import org.apache.streampipes.model.staticproperty.Option;
+import org.apache.streampipes.model.staticproperty.SecretStaticProperty;
+import org.apache.streampipes.model.staticproperty.StaticPropertyAlternatives;
+import org.apache.streampipes.model.staticproperty.StaticPropertyGroup;
+import org.apache.streampipes.sdk.helpers.Alternatives;
+import org.apache.streampipes.sdk.helpers.Labels;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
-import java.util.HashMap;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -37,12 +51,12 @@ class PipelineElementTemplateVisitorTest {
   private static final String PROPERTY_NAME = "propertyName";
 
   private PipelineElementTemplateVisitor visitor;
-  private Map<String, Object> configs;
+  private List<Map<String, Object>> configs;
   private MappingPropertyNary mappingPropertyNary;
 
   @BeforeEach
   void setUp() {
-    configs = new HashMap<>();
+    configs = new ArrayList<>();
     visitor = new PipelineElementTemplateVisitor(configs);
     mappingPropertyNary = mock(MappingPropertyNary.class);
     when(mappingPropertyNary.getInternalName()).thenReturn(PROPERTY_NAME);
@@ -51,7 +65,7 @@ class PipelineElementTemplateVisitorTest {
   @Test
   void visitMappingPropertyNary_SetsSelectedPropertiesWhenKeyExists() {
     List<String> expectedValues = List.of("value1", "value2");
-    configs.put(PROPERTY_NAME, expectedValues);
+    configs.add(Map.of(PROPERTY_NAME, expectedValues));
     visitor.visit(mappingPropertyNary);
 
     verify(mappingPropertyNary).setSelectedProperties(expectedValues);
@@ -67,9 +81,138 @@ class PipelineElementTemplateVisitorTest {
   @Test
   void visitMappingPropertyNary_HandlesEmptySelectedPropertiesCorrectly() {
     List<String> expectedValues = List.of();
-    configs.put(PROPERTY_NAME, expectedValues);
+    configs.add(Map.of(PROPERTY_NAME, expectedValues));
     visitor.visit(mappingPropertyNary);
 
     verify(mappingPropertyNary).setSelectedProperties(expectedValues);
   }
+
+  @Test
+  void anyStaticPropertyTest() {
+    configs.add(Map.of(PROPERTY_NAME, List.of("option1", "option2")));
+    var anyStaticProperty = new AnyStaticProperty(PROPERTY_NAME, "", "");
+    anyStaticProperty.setOptions(List.of(
+            new Option("option1"),
+            new Option("option2")
+        )
+    );
+    visitor.visit(anyStaticProperty);
+    assertEquals(2, anyStaticProperty.getOptions().size());
+    assertTrue(anyStaticProperty.getOptions().get(0).isSelected());
+    assertTrue(anyStaticProperty.getOptions().get(1).isSelected());
+  }
+
+  @Test
+  void colorPickerStaticPropertyTest() {
+    configs.add(Map.of(PROPERTY_NAME, "red"));
+    var colorPickerStaticProperty = new ColorPickerStaticProperty(PROPERTY_NAME, "", "");
+
+    visitor.visit(colorPickerStaticProperty);
+    assertEquals("red", colorPickerStaticProperty.getSelectedColor());
+  }
+
+  @Test
+  void simpleCollectionStaticPropertyTest() {
+    var freeTextId = "free_text";
+    configs.add(Map.of(PROPERTY_NAME, List.of(
+        Map.of(freeTextId, "A"),
+        Map.of(freeTextId, "B"),
+        Map.of(freeTextId, "C")))
+    );
+    var collectionProperty = new CollectionStaticProperty(
+        PROPERTY_NAME,
+        "",
+        "",
+        new FreeTextStaticProperty(freeTextId, "", "")
+    );
+
+    visitor.visit(collectionProperty);
+    assertEquals(3, collectionProperty.getMembers().size());
+    assertEquals(FreeTextStaticProperty.class, collectionProperty.getMembers().get(0).getClass());
+    assertEquals("A", ((FreeTextStaticProperty) collectionProperty.getMembers().get(0)).getValue());
+  }
+
+  @Test
+  void secretStaticPropertyTest() {
+    configs.add(Map.of(PROPERTY_NAME, "secret", "encrypted", true));
+    var secretStaticProperty = new SecretStaticProperty(PROPERTY_NAME, "", "");
+
+    visitor.visit(secretStaticProperty);
+    assertEquals("secret", secretStaticProperty.getValue());
+    assertEquals(true, secretStaticProperty.getEncrypted());
+  }
+
+  @Test
+  void alternativesStaticPropertyTest() {
+    configs.add(Map.of(
+        PROPERTY_NAME, "alternatives1",
+        "free_text", "abc")
+    );
+    var staticProperty = new StaticPropertyAlternatives(PROPERTY_NAME, "", "");
+    staticProperty.setAlternatives(
+        List.of(
+            Alternatives.from(
+                Labels.withId("alternatives1"),
+                new FreeTextStaticProperty("free_text", "", "")
+            ),
+            Alternatives.from(
+                Labels.withId("alternatives2"),
+                new OneOfStaticProperty("one_of", "", "")
+            )
+        )
+    );
+
+    visitor.visit(staticProperty);
+    assertTrue(staticProperty.getAlternatives().get(0).getSelected());
+    assertFalse(staticProperty.getAlternatives().get(1).getSelected());
+    assertEquals(
+        "abc",
+        ((FreeTextStaticProperty) staticProperty.getAlternatives().get(0).getStaticProperty()).getValue()
+    );
+  }
+
+  @Test
+  void alternativesStaticPropertyWithGroupTest() {
+    configs.add(Map.of(
+        PROPERTY_NAME, "alternatives2",
+        "free_text", "abc",
+        "free_text_number", 1000)
+    );
+    var staticProperty = new StaticPropertyAlternatives(PROPERTY_NAME, "", "");
+    staticProperty.setAlternatives(
+        List.of(
+            Alternatives.from(
+                Labels.withId("alternatives1"),
+                new OneOfStaticProperty("one_of", "", "")
+            ),
+            Alternatives.from(
+                Labels.withId("alternatives2"),
+                new StaticPropertyGroup("group", "", "", List.of(
+                    new FreeTextStaticProperty("free_text", "", ""),
+                    new FreeTextStaticProperty("free_text_number", "", ""))
+                )
+            )
+        )
+    );
+
+    visitor.visit(staticProperty);
+    assertTrue(staticProperty.getAlternatives().get(1).getSelected());
+    assertFalse(staticProperty.getAlternatives().get(0).getSelected());
+
+    var group = (StaticPropertyGroup) staticProperty.getAlternatives().get(1).getStaticProperty();
+    var freeText = ((FreeTextStaticProperty) group.getStaticProperties().get(0)).getValue();
+    var freeTextNumber = ((FreeTextStaticProperty) group.getStaticProperties().get(1)).getValue();
+    assertEquals("abc", freeText);
+    assertEquals("1000", freeTextNumber);
+  }
+
+  @Test
+  void treeInputStaticPropertyTest() {
+    configs.add(Map.of(PROPERTY_NAME, "secret", "encrypted", true));
+    var secretStaticProperty = new SecretStaticProperty(PROPERTY_NAME, "", "");
+
+    visitor.visit(secretStaticProperty);
+    assertEquals("secret", secretStaticProperty.getValue());
+    assertEquals(true, secretStaticProperty.getEncrypted());
+  }
 }
diff --git a/streampipes-serializers-json/src/test/java/org/apache/streampipes/serializers/json/TestJacksonSerializer.java b/streampipes-serializers-json/src/test/java/org/apache/streampipes/serializers/json/TestJacksonSerializer.java
index 7ade4f24e6..22cee83f94 100644
--- a/streampipes-serializers-json/src/test/java/org/apache/streampipes/serializers/json/TestJacksonSerializer.java
+++ b/streampipes-serializers-json/src/test/java/org/apache/streampipes/serializers/json/TestJacksonSerializer.java
@@ -30,13 +30,19 @@ public class TestJacksonSerializer {
     Assertions.assertEquals("name", template.getTemplateName());
     Assertions.assertEquals("description", template.getTemplateDescription());
     Assertions.assertEquals(2,
-                            template.getTemplateConfigs().size());
+        template.getTemplateConfigs().size());
     Assertions.assertEquals("test-string",
-                            template.getTemplateConfigs().get("test-key").getValue());
-    Assertions.assertTrue(template.getTemplateConfigs().get("test-key").isEditable());
-    Assertions.assertTrue(template.getTemplateConfigs().get("test-key").isDisplayed());
-    Assertions.assertTrue(template.getTemplateConfigs().get("test-key-2").isEditable());
-    Assertions.assertFalse(template.getTemplateConfigs().get("test-key-2").isDisplayed());
+        findValue(template, "test-key"));
+  }
+
+  private static Object findValue(PipelineElementTemplate template,
+                                  String key) {
+    return template.getTemplateConfigs()
+        .stream()
+        .filter(t -> t.containsKey(key))
+        .map(t -> t.get(key))
+        .findFirst()
+        .orElseThrow(() -> new AssertionError("test-key not found"));
   }
 
   @Test
@@ -49,7 +55,7 @@ public class TestJacksonSerializer {
           JacksonSerializer.getObjectMapper().readValue(json, PipelineElementTemplate.class);
       assertions(template2);
       Assertions.assertEquals(2,
-                              template2.getTemplateConfigs().get("test-key-2").getValue());
+          findValue(template2, "test-key-2"));
     } catch (JsonProcessingException e) {
       e.printStackTrace();
     }
diff --git a/streampipes-storage-couchdb/src/test/java/org/apache/streampipes/storage/couchdb/serializer/TestGsonSerializer.java b/streampipes-storage-couchdb/src/test/java/org/apache/streampipes/storage/couchdb/serializer/TestGsonSerializer.java
index ffca48f131..010159fbc1 100644
--- a/streampipes-storage-couchdb/src/test/java/org/apache/streampipes/storage/couchdb/serializer/TestGsonSerializer.java
+++ b/streampipes-storage-couchdb/src/test/java/org/apache/streampipes/storage/couchdb/serializer/TestGsonSerializer.java
@@ -34,22 +34,18 @@ public class TestGsonSerializer {
     );
     Assertions.assertEquals(
         "test-string",
-        template.getTemplateConfigs()
-                .get("test-key")
-                .getValue()
+        findValue(template, "test-key")
     );
-    Assertions.assertTrue(template.getTemplateConfigs()
-                                  .get("test-key")
-                                  .isEditable());
-    Assertions.assertTrue(template.getTemplateConfigs()
-                                  .get("test-key")
-                                  .isDisplayed());
-    Assertions.assertTrue(template.getTemplateConfigs()
-                                  .get("test-key-2")
-                                  .isEditable());
-    Assertions.assertFalse(template.getTemplateConfigs()
-                                   .get("test-key-2")
-                                   .isDisplayed());
+  }
+
+  private static Object findValue(PipelineElementTemplate template,
+                           String key) {
+    return template.getTemplateConfigs()
+        .stream()
+        .filter(t -> t.containsKey(key))
+        .map(t -> t.get(key))
+        .findFirst()
+        .orElseThrow(() -> new AssertionError("test-key not found"));
   }
 
   @Test
@@ -66,9 +62,7 @@ public class TestGsonSerializer {
     assertions(template2);
     Assertions.assertEquals(
         2.0,
-        template2.getTemplateConfigs()
-                 .get("test-key-2")
-                 .getValue()
+        findValue(template2, "test-key-2")
     );
   }
 }
diff --git a/streampipes-test-utils-executors/src/main/java/org/apache/streampipes/test/executors/ProcessingElementTestExecutor.java b/streampipes-test-utils-executors/src/main/java/org/apache/streampipes/test/executors/ProcessingElementTestExecutor.java
index 2dbe384d13..a83e2c7ba0 100644
--- a/streampipes-test-utils-executors/src/main/java/org/apache/streampipes/test/executors/ProcessingElementTestExecutor.java
+++ b/streampipes-test-utils-executors/src/main/java/org/apache/streampipes/test/executors/ProcessingElementTestExecutor.java
@@ -28,7 +28,6 @@ import org.apache.streampipes.model.runtime.EventFactory;
 import org.apache.streampipes.model.runtime.SchemaInfo;
 import org.apache.streampipes.model.runtime.SourceInfo;
 import org.apache.streampipes.model.template.PipelineElementTemplate;
-import org.apache.streampipes.model.template.PipelineElementTemplateConfig;
 import org.apache.streampipes.sdk.extractor.ProcessingElementParameterExtractor;
 import org.apache.streampipes.test.generator.EventStreamGenerator;
 
@@ -36,7 +35,6 @@ import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -62,7 +60,7 @@ public class ProcessingElementTestExecutor {
     this.processor = processor;
     this.testConfiguration = testConfiguration;
     this.selectorPrefixes = testConfiguration.getPrefixes()
-                                             .iterator();
+        .iterator();
     this.invocationConfig = invocationConfig;
   }
 
@@ -70,7 +68,7 @@ public class ProcessingElementTestExecutor {
     this.processor = processor;
     this.testConfiguration = testConfiguration;
     this.selectorPrefixes = testConfiguration.getPrefixes()
-                                             .iterator();
+        .iterator();
   }
 
   /**
@@ -113,17 +111,17 @@ public class ProcessingElementTestExecutor {
 
     // Validate the output of the processor
     Mockito.verify(
-               mockCollector,
-               Mockito.times(expectedOutputEvents.size())
-           )
-           .collect(spOutputCollectorCaptor.capture());
+            mockCollector,
+            Mockito.times(expectedOutputEvents.size())
+        )
+        .collect(spOutputCollectorCaptor.capture());
     var resultingEvents = spOutputCollectorCaptor.getAllValues();
     IntStream.range(0, expectedOutputEvents.size())
-             .forEach(i -> assertEventEquality(
-                 expectedOutputEvents.get(i),
-                 resultingEvents.get(i)
-                                .getRaw()
-             ));
+        .forEach(i -> assertEventEquality(
+            expectedOutputEvents.get(i),
+            resultingEvents.get(i)
+                .getRaw()
+        ));
 
     // validate that the processor is stopped correctly
     processor.onPipelineStopped();
@@ -193,14 +191,16 @@ public class ProcessingElementTestExecutor {
         .getStaticProperties();
 
 
-    var configs = new HashMap<String, PipelineElementTemplateConfig>();
+    var configs = new ArrayList<Map<String, Object>>();
 
     staticProperties.forEach(staticProperty -> {
       var value = testConfiguration.getFieldConfiguration()
-                                   .get(staticProperty.getInternalName());
-      configs.put(
-          staticProperty.getInternalName(),
-          new PipelineElementTemplateConfig(true, true, value)
+          .get(staticProperty.getInternalName());
+      configs.add(
+          Map.of(
+              staticProperty.getInternalName(),
+              value
+          )
       );
     });
 
@@ -211,7 +211,7 @@ public class ProcessingElementTestExecutor {
 
     if (!selectorPrefixes.hasNext()) {
       selectorPrefixes = testConfiguration.getPrefixes()
-                                          .iterator();
+          .iterator();
     }
 
     String selectorPrefix = selectorPrefixes.next();
diff --git a/streampipes-test-utils/src/main/java/org/apache/streampipes/test/generator/template/PipelineElementTemplateHelpers.java b/streampipes-test-utils/src/main/java/org/apache/streampipes/test/generator/template/PipelineElementTemplateHelpers.java
index 7574b6cec9..07f1346b76 100644
--- a/streampipes-test-utils/src/main/java/org/apache/streampipes/test/generator/template/PipelineElementTemplateHelpers.java
+++ b/streampipes-test-utils/src/main/java/org/apache/streampipes/test/generator/template/PipelineElementTemplateHelpers.java
@@ -18,22 +18,17 @@
 package org.apache.streampipes.test.generator.template;
 
 import org.apache.streampipes.model.template.PipelineElementTemplate;
-import org.apache.streampipes.model.template.PipelineElementTemplateConfig;
 
-import java.util.HashMap;
+import java.util.ArrayList;
 import java.util.Map;
 
 public class PipelineElementTemplateHelpers {
 
   public static PipelineElementTemplate makePipelineElementTemplate() {
-    Map<String, PipelineElementTemplateConfig> configs = new HashMap<>();
-    configs.put("test-key", makeConfig(true, true, "test-string"));
-    configs.put("test-key-2", makeConfig(true, false, 2));
+    var configs = new ArrayList<Map<String, Object>>();
+    configs.add(Map.of("test-key", "test-string"));
+    configs.add(Map.of("test-key-2", 2));
 
     return new PipelineElementTemplate("name", "description", configs);
   }
-
-  public static PipelineElementTemplateConfig makeConfig(boolean editable, boolean displayed, Object value) {
-    return new PipelineElementTemplateConfig(editable, displayed, value);
-  }
 }
diff --git a/ui/cypress/tests/userManagement/testVariousUserRoles.smoke.spec.ts b/ui/cypress/tests/userManagement/testVariousUserRoles.smoke.spec.ts
index 5bbccae3cd..34e4624d63 100644
--- a/ui/cypress/tests/userManagement/testVariousUserRoles.smoke.spec.ts
+++ b/ui/cypress/tests/userManagement/testVariousUserRoles.smoke.spec.ts
@@ -40,7 +40,7 @@ for (var i = 0; i < testedRoles.length; i++) {
             UserUtils.goToUserConfiguration();
             cy.dataCy('navigation-icon', { timeout: 10000 }).should(
                 'have.length',
-                9,
+                8,
             );
 
             cy.dataCy('user-accounts-table-row', { timeout: 10000 }).should(
@@ -75,12 +75,12 @@ for (var i = 0; i < testedRoles.length; i++) {
             } else if (testRole == UserRole.ROLE_DASHBOARD_ADMIN) {
                 cy.dataCy('navigation-icon', { timeout: 10000 }).should(
                     'have.length',
-                    2,
+                    4,
                 );
             } else if (testRole == UserRole.ROLE_DATA_EXPLORER_ADMIN) {
                 cy.dataCy('navigation-icon', { timeout: 10000 }).should(
                     'have.length',
-                    2,
+                    4,
                 );
             } else if (testRole == UserRole.ROLE_CONNECT_ADMIN) {
                 cy.dataCy('navigation-icon', { timeout: 10000 }).should(