Repository: apache/streampipes
Pull request #3142 (2024-08-14 19:22:45)

feat(#3141): Add option to configure production sites
<!--
  ~ 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 #3141 

### 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/streampipes-connect-shared/src/main/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToOriginalSchemaConverter.java b/streampipes-connect-shared/src/main/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToOriginalSchemaConverter.java
index c35ed1a791..a7ff90b45c 100644
--- a/streampipes-connect-shared/src/main/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToOriginalSchemaConverter.java
+++ b/streampipes-connect-shared/src/main/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToOriginalSchemaConverter.java
@@ -70,7 +70,7 @@ public class ToOriginalSchemaConverter implements ITransformationRuleVisitor, Pr
 
   @Override
   public void visit(MoveRuleDescription rule) {
-    var targetRuntimeKey = rule.getNewRuntimeKey() + "." + rule.getOldRuntimeKey();
+    var targetRuntimeKey = rule.getNewRuntimeKey() + Utils.DELIMITER + rule.getOldRuntimeKey();
     var existing = new Cloner().property(findProperty(properties, targetRuntimeKey));
     var existingHierarchy = findPropertyHierarchy(this.properties, targetRuntimeKey);
     existingHierarchy.removeIf(property -> property.getRuntimeName().equals(existing.getRuntimeName()));
diff --git a/streampipes-connect-shared/src/main/java/org/apache/streampipes/connect/shared/preprocessing/utils/Utils.java b/streampipes-connect-shared/src/main/java/org/apache/streampipes/connect/shared/preprocessing/utils/Utils.java
index 50a5a5681c..9a22a11124 100644
--- a/streampipes-connect-shared/src/main/java/org/apache/streampipes/connect/shared/preprocessing/utils/Utils.java
+++ b/streampipes-connect-shared/src/main/java/org/apache/streampipes/connect/shared/preprocessing/utils/Utils.java
@@ -26,8 +26,10 @@ import java.util.List;
 
 public class Utils {
 
+  public static final String DELIMITER = "<-=>";
+
   public static String getLastKey(String s) {
-    String[] list = s.split("\\.");
+    String[] list = s.split(DELIMITER);
     if (list.length == 0) {
       return s;
     } else {
@@ -36,7 +38,7 @@ public class Utils {
   }
 
   public static List<String> toKeyArray(String s) {
-    String[] split = s.split("\\.");
+    String[] split = s.split(DELIMITER);
     if (split.length == 0) {
       return List.of(s);
     } else {
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/LocationConfig.java b/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/LocationConfig.java
new file mode 100644
index 0000000000..573e9abe70
--- /dev/null
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/LocationConfig.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.streampipes.model.configuration;
+
+import org.apache.streampipes.model.shared.annotation.TsModel;
+
+@TsModel
+public record LocationConfig(boolean locationEnabled,
+                             String tileServerUrl,
+                             String attributionText) {}
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/SpCoreConfiguration.java b/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/SpCoreConfiguration.java
index 6aad62a172..b5766bd3f6 100644
--- a/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/SpCoreConfiguration.java
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/SpCoreConfiguration.java
@@ -32,6 +32,7 @@ public class SpCoreConfiguration {
   private EmailConfig emailConfig;
   private EmailTemplateConfig emailTemplateConfig;
   private GeneralConfig generalConfig;
+  private LocationConfig locationConfig;
 
   private boolean isConfigured;
   private SpCoreConfigurationStatus serviceStatus;
@@ -40,6 +41,7 @@ public class SpCoreConfiguration {
   private String filesDir;
 
   public SpCoreConfiguration() {
+    this.locationConfig = new LocationConfig(false, "", "");
   }
 
   public String getRev() {
@@ -129,4 +131,12 @@ public class SpCoreConfiguration {
   public void setServiceStatus(SpCoreConfigurationStatus serviceStatus) {
     this.serviceStatus = serviceStatus;
   }
+
+  public LocationConfig getLocationConfig() {
+    return locationConfig;
+  }
+
+  public void setLocationConfig(LocationConfig locationConfig) {
+    this.locationConfig = locationConfig;
+  }
 }
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java
index 5bce889136..caca0dac6c 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java
@@ -83,6 +83,8 @@ public class ResetManagement {
 
     removeAllPipelineTemplates();
 
+    clearGenericStorage();
+
     logger.info("Resetting the system was completed");
   }
 
@@ -106,7 +108,7 @@ public class ResetManagement {
   private static void stopAndDeleteAllAdapters() {
     AdapterMasterManagement adapterMasterManagement = new AdapterMasterManagement(
         StorageDispatcher.INSTANCE.getNoSqlStore()
-                                  .getAdapterInstanceStorage(),
+            .getAdapterInstanceStorage(),
         new SpResourceManager().manageAdapters(),
         new SpResourceManager().manageDataStreams(),
         AdapterMetricsManager.INSTANCE.getAdapterMetrics()
@@ -152,37 +154,37 @@ public class ResetManagement {
   private static void removeAllDataViewWidgets() {
     var widgetStorage =
         StorageDispatcher.INSTANCE.getNoSqlStore()
-                                  .getDataExplorerWidgetStorage();
+            .getDataExplorerWidgetStorage();
     widgetStorage.findAll()
-                 .forEach(widget -> widgetStorage.deleteElementById(widget.getElementId()));
+        .forEach(widget -> widgetStorage.deleteElementById(widget.getElementId()));
   }
 
   private static void removeAllDataViews() {
     var dataLakeDashboardStorage =
         StorageDispatcher.INSTANCE.getNoSqlStore()
-                                  .getDataExplorerDashboardStorage();
+            .getDataExplorerDashboardStorage();
     dataLakeDashboardStorage.findAll()
-                            .forEach(dashboard -> dataLakeDashboardStorage.deleteElementById(dashboard.getElementId()));
+        .forEach(dashboard -> dataLakeDashboardStorage.deleteElementById(dashboard.getElementId()));
   }
 
   private static void removeAllDashboardWidgets() {
     var dashboardWidgetStorage =
         StorageDispatcher.INSTANCE.getNoSqlStore()
-                                  .getDashboardWidgetStorage();
+            .getDashboardWidgetStorage();
     dashboardWidgetStorage.findAll()
-                          .forEach(widget -> dashboardWidgetStorage.deleteElementById(widget.getElementId()));
+        .forEach(widget -> dashboardWidgetStorage.deleteElementById(widget.getElementId()));
   }
 
   private static void removeAllDashboards() {
     var dashboardStorage = StorageDispatcher.INSTANCE.getNoSqlStore()
-                                                                   .getDashboardStorage();
+        .getDashboardStorage();
     dashboardStorage.findAll()
-                    .forEach(dashboard -> dashboardStorage.deleteElementById(dashboard.getElementId()));
+        .forEach(dashboard -> dashboardStorage.deleteElementById(dashboard.getElementId()));
   }
 
   private static void removeAllAssets(String username) {
     IGenericStorage genericStorage = StorageDispatcher.INSTANCE.getNoSqlStore()
-                                                               .getGenericStorage();
+        .getGenericStorage();
     try {
       for (Map<String, Object> asset : genericStorage.findAll("asset-management")) {
         genericStorage.delete((String) asset.get("_id"), (String) asset.get("_rev"));
@@ -203,4 +205,24 @@ public class ResetManagement {
         .forEach(pipelineElementTemplateStorage::deleteElement);
 
   }
+
+  private static void clearGenericStorage() {
+    var appDocTypesToDelete = List.of(
+        "asset-management",
+        "asset-sites"
+    );
+    var genericStorage = StorageDispatcher.INSTANCE.getNoSqlStore().getGenericStorage();
+
+    appDocTypesToDelete.forEach(docType -> {
+      try {
+        var allDocs = genericStorage.findAll(docType);
+        for (var doc : allDocs) {
+          genericStorage.delete(doc.get("_id").toString(), doc.get("_rev").toString());
+        }
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+    });
+
+  }
 }
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/LocationConfigurationResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/LocationConfigurationResource.java
new file mode 100644
index 0000000000..7872efed0b
--- /dev/null
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/LocationConfigurationResource.java
@@ -0,0 +1,53 @@
+/*
+ * 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.admin;
+
+import org.apache.streampipes.model.configuration.LocationConfig;
+import org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource;
+import org.apache.streampipes.rest.security.AuthConstants;
+
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+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/admin/location-config")
+public class LocationConfigurationResource extends AbstractAuthGuardedRestResource {
+
+  @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+  public LocationConfig getLocationConfig() {
+    return getSpCoreConfigurationStorage().get().getLocationConfig();
+  }
+
+  @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+  @PreAuthorize(AuthConstants.IS_ADMIN_ROLE)
+  public ResponseEntity<Void> updateGeneralConfiguration(@RequestBody LocationConfig config) {
+    var storage = getSpCoreConfigurationStorage();
+    var cfg = storage.get();
+    cfg.setLocationConfig(config);
+    storage.updateElement(cfg);
+
+    return ok();
+  }
+}
diff --git a/ui/cypress/fixtures/connect/schemaRules/expected.csv b/ui/cypress/fixtures/connect/schemaRules/expected.csv
index 411fac981c..18ccb6566e 100644
--- a/ui/cypress/fixtures/connect/schemaRules/expected.csv
+++ b/ui/cypress/fixtures/connect/schemaRules/expected.csv
@@ -1,2 +1,2 @@
-count;staticPropertyName;temperature
-122.0;id1;11.0
+count;dot;staticPropertyName;temperature
+122.0;special_char_in_column_name;id1;11.0
diff --git a/ui/cypress/fixtures/connect/schemaRules/input.csv b/ui/cypress/fixtures/connect/schemaRules/input.csv
index be4e9eff3b..8514e3310d 100644
--- a/ui/cypress/fixtures/connect/schemaRules/input.csv
+++ b/ui/cypress/fixtures/connect/schemaRules/input.csv
@@ -1,2 +1,2 @@
-timestamp;count;density;temperature
-1720018277000;122.0;62.0;11
+timestamp;count;density;temperature;contains.dot
+1720018277000;122.0;62.0;11;special_char_in_column_name
diff --git a/ui/cypress/support/utils/configuration/ConfigurationUtils.ts b/ui/cypress/support/utils/configuration/ConfigurationUtils.ts
new file mode 100644
index 0000000000..f8148033e5
--- /dev/null
+++ b/ui/cypress/support/utils/configuration/ConfigurationUtils.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export class ConfigurationUtils {
+    public static goToConfigurationExport() {
+        cy.visit('#/configuration/export');
+    }
+
+    public static goToSitesConfiguration() {
+        cy.visit('#/configuration/sites');
+    }
+
+    public static goToGeneralConfiguration() {
+        cy.visit('#/configuration/general');
+    }
+}
diff --git a/ui/cypress/support/utils/configuration/SiteUtils.ts b/ui/cypress/support/utils/configuration/SiteUtils.ts
new file mode 100644
index 0000000000..59bbf0900e
--- /dev/null
+++ b/ui/cypress/support/utils/configuration/SiteUtils.ts
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export class SiteUtils {
+    public static CHECKBOX_ENABLE_LOCATION_FEATURES =
+        'sites-enable-location-features-checkbox';
+    public static INPUT_TILE_SERVER_URL = 'sites-location-config-tile-server';
+
+    public static BUTTON_MANAGE_SITES = 'sites-manage-sites-button';
+    public static BUTTON_EDIT_SITE = 'sites-edit-button';
+    public static BUTTON_DELETE_SITE = 'sites-delete-button';
+    public static BUTTON_SITE_DIALOG_SAVE = 'sites-dialog-save-button';
+    public static BUTTON_SITE_DIALOG_CANCEL = 'sites-dialog-cancel-button';
+    public static BUTTON_SITE_DIALOG_REMOVE_AREA =
+        'sites-dialog-remove-area-button';
+    public static BUTTON_SITE_DIALOG_ADD_AREA = 'sites-dialog-add-area-button';
+
+    public static INPUT_SITE_DIALOG_SITE_INPUT = 'sites-dialog-site-input';
+    public static INPUT_SITE_DIALOG_NEW_AREA_INPUT =
+        'sites-dialog-new-area-input';
+
+    public static LABEL_TABLE_NAME = 'site-table-row-label';
+    public static LABEL_TABLE_AREA = 'site-table-row-areas';
+
+    public static enableGeoFeatures(tileServerUrl: string): void {
+        cy.dataCy(SiteUtils.CHECKBOX_ENABLE_LOCATION_FEATURES).click();
+        cy.dataCy(SiteUtils.INPUT_TILE_SERVER_URL).clear().type(tileServerUrl);
+        cy.dataCy('sites-location-features-button').click();
+    }
+
+    public static createNewSite(name: string, areas: string[]): void {
+        cy.dataCy(SiteUtils.BUTTON_MANAGE_SITES).click();
+        cy.dataCy(SiteUtils.INPUT_SITE_DIALOG_SITE_INPUT, { timeout: 2000 })
+            .clear()
+            .type(name);
+
+        areas.forEach(area => {
+            cy.dataCy(SiteUtils.INPUT_SITE_DIALOG_NEW_AREA_INPUT)
+                .clear()
+                .type(area);
+            cy.dataCy(SiteUtils.BUTTON_SITE_DIALOG_ADD_AREA).click();
+        });
+
+        cy.dataCy(this.BUTTON_SITE_DIALOG_SAVE).click();
+    }
+
+    public static openEditSiteDialog() {
+        cy.dataCy(SiteUtils.BUTTON_EDIT_SITE).first().click();
+    }
+}
diff --git a/ui/cypress/support/utils/connect/ConnectBtns.ts b/ui/cypress/support/utils/connect/ConnectBtns.ts
index f31db5f0e8..52055e0ae9 100644
--- a/ui/cypress/support/utils/connect/ConnectBtns.ts
+++ b/ui/cypress/support/utils/connect/ConnectBtns.ts
@@ -111,6 +111,12 @@ export class ConnectBtns {
         });
     }
 
+    public static runtimeNameInput() {
+        return cy.dataCy('connect-edit-field-runtime-name', {
+            timeout: 10000,
+        });
+    }
+
     // ========================================================================
 
     // =====================  Format configurations  ==========================
diff --git a/ui/cypress/support/utils/connect/ConnectEventSchemaUtils.ts b/ui/cypress/support/utils/connect/ConnectEventSchemaUtils.ts
index 1e302003ec..6bcfe2b231 100644
--- a/ui/cypress/support/utils/connect/ConnectEventSchemaUtils.ts
+++ b/ui/cypress/support/utils/connect/ConnectEventSchemaUtils.ts
@@ -127,6 +127,26 @@ export class ConnectEventSchemaUtils {
         cy.dataCy('sp-save-edit-property').click();
     }
 
+    public static renameProperty(
+        fromRuntimeName: string,
+        toRuntimeName: string,
+    ) {
+        ConnectEventSchemaUtils.clickEditProperty(fromRuntimeName);
+        ConnectEventSchemaUtils.setRuntimeName(toRuntimeName);
+        ConnectBtns.saveEditProperty().click();
+    }
+
+    public static setRuntimeName(newRuntimeName: string) {
+        ConnectBtns.runtimeNameInput().clear().type(newRuntimeName);
+    }
+
+    public static validateRuntimeName(expectedRuntimeName: string) {
+        ConnectBtns.runtimeNameInput().should(
+            'have.value',
+            expectedRuntimeName,
+        );
+    }
+
     public static unitTransformation(
         propertyName: string,
         fromUnit: string,
@@ -244,13 +264,20 @@ export class ConnectEventSchemaUtils {
     }
 
     public static clickEditProperty(propertyName: string) {
-        cy.dataCy('edit-' + propertyName.toLowerCase(), {
+        cy.dataCy(`edit-${ConnectEventSchemaUtils.escape(propertyName)}`, {
             timeout: 10000,
         }).click();
-        cy.dataCy('connect-edit-field-runtime-name').should(
-            'have.value',
-            propertyName,
-            { timeout: 10000 },
-        );
+        ConnectEventSchemaUtils.validateRuntimeName(propertyName);
+    }
+
+    //
+    /**
+     * Function to escape special characters in a string for use in Cypress
+     * selectors
+     */
+    public static escape(selector: string): string {
+        return selector
+            .replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1')
+            .toLowerCase();
     }
 }
diff --git a/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts b/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts
index a8e8c8928f..a2cb61594b 100644
--- a/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts
+++ b/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts
@@ -19,11 +19,7 @@
 import { Injectable } from '@angular/core';
 import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http';
 import { Observable, of } from 'rxjs';
-import {
-    DataLakeMeasure,
-    PageResult,
-    SpQueryResult,
-} from '../model/gen/streampipes-model';
+import { DataLakeMeasure, SpQueryResult } from '../model/gen/streampipes-model';
 import { map } from 'rxjs/operators';
 import { DatalakeQueryParameters } from '../model/datalake/DatalakeQueryParameters';
 
@@ -103,32 +99,6 @@ export class DatalakeRestService {
         }
     }
 
-    getPagedData(
-        index: string,
-        itemsPerPage: number,
-        page: number,
-        columns?: string,
-        order?: string,
-    ): Observable<PageResult> {
-        const url = this.dataLakeUrl + '/measurements/' + index;
-
-        const queryParams: DatalakeQueryParameters = this.getQueryParameters(
-            columns,
-            undefined,
-            undefined,
-            page,
-            itemsPerPage,
-            undefined,
-            undefined,
-            order,
-            undefined,
-            undefined,
-        );
-
-        // @ts-ignore
-        return this.http.get<PageResult>(url, { params: queryParams });
-    }
-
     getTagValues(
         index: string,
         fieldNames: string[],
diff --git a/ui/projects/streampipes/platform-services/src/lib/apis/location-config.service.ts b/ui/projects/streampipes/platform-services/src/lib/apis/location-config.service.ts
new file mode 100644
index 0000000000..d07e8b8e13
--- /dev/null
+++ b/ui/projects/streampipes/platform-services/src/lib/apis/location-config.service.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { PlatformServicesCommons } from './commons.service';
+import { LocationConfig } from '../model/gen/streampipes-model';
+
+@Injectable({
+    providedIn: 'root',
+})
+export class LocationConfigService {
+    constructor(
+        private http: HttpClient,
+        private platformServicesCommons: PlatformServicesCommons,
+    ) {}
+
+    getLocationConfig(): Observable<LocationConfig> {
+        return this.http
+            .get(this.locationConfigPath)
+            .pipe(map(response => response as LocationConfig));
+    }
+
+    updateLocationConfig(config: LocationConfig): Observable<any> {
+        return this.http.put(this.locationConfigPath, config);
+    }
+
+    private get locationConfigPath() {
+        return `${this.platformServicesCommons.apiBasePath}/admin/location-config`;
+    }
+}
diff --git a/ui/projects/streampipes/platform-services/src/lib/model/assets/asset.model.ts b/ui/projects/streampipes/platform-services/src/lib/model/assets/asset.model.ts
index c1872f5f31..084b874214 100644
--- a/ui/projects/streampipes/platform-services/src/lib/model/assets/asset.model.ts
+++ b/ui/projects/streampipes/platform-services/src/lib/model/assets/asset.model.ts
@@ -48,15 +48,40 @@ export interface Isa95TypeDesc {
     type: Isa95Type;
 }
 
+export interface AssetLocation {
+    coordinates: LatLng;
+    zoom?: number;
+}
+
+export interface AssetSiteDesc {
+    _id: string;
+    _rev?: string;
+    appDocType: string;
+    label: string;
+    location?: AssetLocation;
+    areas: string[];
+}
+
+export interface LatLng {
+    latitude: number;
+    longitude: number;
+}
+
+export interface AssetSite {
+    siteId: string;
+    area: string;
+    hasExactLocation: boolean;
+    location?: AssetLocation;
+}
+
 export interface SpAsset {
     assetId: string;
     assetName: string;
     assetDescription: string;
-
     assetType: AssetType;
     labelIds?: string[];
     assetLinks: AssetLink[];
-
+    assetSite?: AssetSite;
     assets: SpAsset[];
 }
 
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 29b7a7f837..59568c24a9 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,11 +20,10 @@
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2024-07-29 21:03:44.
+// Generated using typescript-generator version 3.2.1263 on 2024-08-15 17:36:05.
 
 export class NamedStreamPipesEntity implements Storable {
     '@class':
-        | 'org.apache.streampipes.model.connect.grounding.ProtocolDescription'
         | 'org.apache.streampipes.model.template.PipelineTemplateDescription'
         | 'org.apache.streampipes.model.SpDataStream'
         | 'org.apache.streampipes.model.base.VersionedNamedStreamPipesEntity'
@@ -2137,6 +2136,26 @@ export class ListOutputStrategy extends OutputStrategy {
     }
 }
 
+export class LocationConfig {
+    attributionText: string;
+    locationEnabled: boolean;
+    tileServerUrl: string;
+
+    static fromData(
+        data: LocationConfig,
+        target?: LocationConfig,
+    ): LocationConfig {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new LocationConfig();
+        instance.attributionText = data.attributionText;
+        instance.locationEnabled = data.locationEnabled;
+        instance.tileServerUrl = data.tileServerUrl;
+        return instance;
+    }
+}
+
 export class MappingProperty extends StaticProperty {
     '@class':
         | 'org.apache.streampipes.model.staticproperty.MappingProperty'
@@ -2430,25 +2449,6 @@ export class Option {
     }
 }
 
-/**
- * @deprecated since 0.92.0, for removal
- */
-export class PageResult extends DataSeries {
-    page: number;
-    pageSum: number;
-
-    static fromData(data: PageResult, target?: PageResult): PageResult {
-        if (!data) {
-            return data;
-        }
-        const instance = target || new PageResult();
-        super.fromData(data, instance);
-        instance.page = data.page;
-        instance.pageSum = data.pageSum;
-        return instance;
-    }
-}
-
 export class Pipeline implements Storable {
     _id: string;
     _rev: string;
@@ -3054,35 +3054,6 @@ export class PropertyValueSpecification {
     }
 }
 
-/**
- * @deprecated since 0.93.0, for removal
- */
-export class ProtocolDescription extends NamedStreamPipesEntity {
-    '@class': 'org.apache.streampipes.model.connect.grounding.ProtocolDescription';
-    'category': string[];
-    'config': StaticPropertyUnion[];
-    'sourceType': string;
-
-    static 'fromData'(
-        data: ProtocolDescription,
-        target?: ProtocolDescription,
-    ): ProtocolDescription {
-        if (!data) {
-            return data;
-        }
-        const instance = target || new ProtocolDescription();
-        super.fromData(data, instance);
-        instance.category = __getCopyArrayFn(__identity<string>())(
-            data.category,
-        );
-        instance.config = __getCopyArrayFn(StaticProperty.fromDataUnion)(
-            data.config,
-        );
-        instance.sourceType = data.sourceType;
-        return instance;
-    }
-}
-
 export class PulsarTransportProtocol extends TransportProtocol {
     '@class': 'org.apache.streampipes.model.grounding.PulsarTransportProtocol';
 
diff --git a/ui/projects/streampipes/platform-services/src/public-api.ts b/ui/projects/streampipes/platform-services/src/public-api.ts
index ce377827b3..8e0d2d428c 100644
--- a/ui/projects/streampipes/platform-services/src/public-api.ts
+++ b/ui/projects/streampipes/platform-services/src/public-api.ts
@@ -34,6 +34,7 @@ export * from './lib/apis/functions.service';
 export * from './lib/apis/general-config.service';
 export * from './lib/apis/generic-storage.service';
 export * from './lib/apis/labels.service';
+export * from './lib/apis/location-config.service';
 export * from './lib/apis/mail-config.service';
 export * from './lib/apis/measurement-units.service';
 export * from './lib/apis/permissions.service';
diff --git a/ui/projects/streampipes/shared-ui/src/lib/components/basic-field-description/basic-field-description.component.html b/ui/projects/streampipes/shared-ui/src/lib/components/basic-field-description/basic-field-description.component.html
index 83115501f2..a33844fb7d 100644
--- a/ui/projects/streampipes/shared-ui/src/lib/components/basic-field-description/basic-field-description.component.html
+++ b/ui/projects/streampipes/shared-ui/src/lib/components/basic-field-description/basic-field-description.component.html
@@ -19,7 +19,8 @@
 <div
     fxFlex="100"
     fxLayout="row"
-    fxLayoutAlign="start center"
+    fxLayoutGap="10px"
+    fxLayoutAlign="start start"
     class="field-description-outer"
 >
     <div [fxFlex]="descriptionPanelWidth" fxLayout="column">
diff --git a/ui/src/app/assets/assets.module.ts b/ui/src/app/assets/assets.module.ts
index c8adc3fa06..fc81b953f7 100644
--- a/ui/src/app/assets/assets.module.ts
+++ b/ui/src/app/assets/assets.module.ts
@@ -57,6 +57,8 @@ import { MatButtonToggleModule } from '@angular/material/button-toggle';
 import { AssetDetailsLabelsComponent } from './components/asset-details/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component';
 import { MatChipGrid, MatChipsModule } from '@angular/material/chips';
 import { MatAutocompleteModule } from '@angular/material/autocomplete';
+import { AssetDetailsSiteComponent } from './components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-details-site.component';
+import { AssetLocationComponent } from './components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-location/asset-location.component';
 
 @NgModule({
     imports: [
@@ -112,7 +114,9 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
         AssetDetailsBasicsComponent,
         AssetDetailsLabelsComponent,
         AssetDetailsLinksComponent,
+        AssetDetailsSiteComponent,
         AssetLinkSectionComponent,
+        AssetLocationComponent,
         AssetUploadDialogComponent,
         EditAssetLinkDialogComponent,
         SpAssetDetailsComponent,
diff --git a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-basics.component.html b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-basics.component.html
index 71f9b00b9c..0cc81d4e70 100644
--- a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-basics.component.html
+++ b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-basics.component.html
@@ -100,12 +100,27 @@
                 label="Labels"
                 description="Assign additional labels to better discover your assets"
             >
-                <sp-asset-details-labels-component
+                <sp-asset-details-labels
                     [asset]="asset"
                     [editMode]="editMode"
                     class="w-100"
                 >
-                </sp-asset-details-labels-component>
+                </sp-asset-details-labels>
+            </sp-basic-field-description>
+            <sp-basic-field-description
+                *ngIf="rootNode"
+                fxFlex="100"
+                descriptionPanelWidth="30"
+                label="Sites"
+                description="Assign a location (site and area) to this asset"
+            >
+                <sp-asset-details-site
+                    class="w-100"
+                    [asset]="asset"
+                    [sites]="sites"
+                    [editMode]="editMode"
+                >
+                </sp-asset-details-site>
             </sp-basic-field-description>
         </div>
     </div>
diff --git a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-basics.component.ts b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-basics.component.ts
index f84ef1fd3b..7315d5e4c4 100644
--- a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-basics.component.ts
+++ b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-basics.component.ts
@@ -16,37 +16,55 @@
  *
  */
 
-import { Component, Input, OnInit } from '@angular/core';
 import {
+    Component,
+    Input,
+    OnChanges,
+    OnInit,
+    SimpleChanges,
+} from '@angular/core';
+import {
+    AssetSiteDesc,
     Isa95TypeDesc,
     Isa95TypeService,
     SpAsset,
 } from '@streampipes/platform-services';
 
 @Component({
-    selector: 'sp-asset-details-basics-component',
+    selector: 'sp-asset-details-basics',
     templateUrl: './asset-details-basics.component.html',
     styleUrls: ['./asset-details-basics.component.scss'],
 })
-export class AssetDetailsBasicsComponent implements OnInit {
+export class AssetDetailsBasicsComponent implements OnInit, OnChanges {
     @Input()
     asset: SpAsset;
 
     @Input()
     editMode: boolean;
 
+    @Input()
+    rootNode: boolean;
+
+    @Input()
+    sites: AssetSiteDesc[];
+
     isa95Types: Isa95TypeDesc[] = [];
 
     constructor(private isa95TypeService: Isa95TypeService) {}
 
     ngOnInit() {
-        this.asset.assetType ??= {
-            assetIcon: undefined,
-            assetIconColor: undefined,
-            assetTypeCategory: undefined,
-            assetTypeLabel: undefined,
-            isa95AssetType: 'OTHER',
-        };
         this.isa95Types = this.isa95TypeService.getTypeDescriptions();
     }
+
+    ngOnChanges(changes: SimpleChanges) {
+        if (changes['asset']) {
+            this.asset.assetType ??= {
+                assetIcon: undefined,
+                assetIconColor: undefined,
+                assetTypeCategory: undefined,
+                assetTypeLabel: undefined,
+                isa95AssetType: 'OTHER',
+            };
+        }
+    }
 }
diff --git a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.ts b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.ts
index 445072cf5d..b26fbcde6a 100644
--- a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.ts
+++ b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.ts
@@ -39,7 +39,7 @@ import { map, startWith } from 'rxjs/operators';
 import { SpColorizationService } from '@streampipes/shared-ui';
 
 @Component({
-    selector: 'sp-asset-details-labels-component',
+    selector: 'sp-asset-details-labels',
     templateUrl: './asset-details-labels.component.html',
 })
 export class AssetDetailsLabelsComponent implements OnInit, OnChanges {
diff --git a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-details-site.component.html b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-details-site.component.html
new file mode 100644
index 0000000000..5b0c819560
--- /dev/null
+++ b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-details-site.component.html
@@ -0,0 +1,46 @@
+<!--
+  ~ 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-form-field color="accent" class="w-100">
+        <mat-label>Site</mat-label>
+        <mat-select
+            [disabled]="!editMode"
+            [(ngModel)]="asset.assetSite.siteId"
+            (selectionChange)="handleLocationChange($event)"
+        >
+            <mat-option *ngFor="let site of sites" [value]="site._id">
+                {{ site.label }}
+            </mat-option>
+        </mat-select>
+    </mat-form-field>
+    <mat-form-field color="accent" class="w-100" *ngIf="currentSite">
+        <mat-label>Area</mat-label>
+        <mat-select [disabled]="!editMode" [(ngModel)]="asset.assetSite.area">
+            <mat-option *ngFor="let area of currentSite.areas" [value]="area">
+                {{ area }}
+            </mat-option>
+        </mat-select>
+    </mat-form-field>
+    <sp-asset-location
+        [asset]="asset"
+        [editMode]="editMode"
+        *ngIf="asset.assetSite?.siteId"
+    >
+    </sp-asset-location>
+</div>
diff --git a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-details-site.component.ts b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-details-site.component.ts
new file mode 100644
index 0000000000..50988ff75a
--- /dev/null
+++ b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-details-site.component.ts
@@ -0,0 +1,63 @@
+/*
+ * 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, OnChanges, SimpleChanges } from '@angular/core';
+import { AssetSiteDesc, SpAsset } from '@streampipes/platform-services';
+import { MatSelectChange } from '@angular/material/select';
+
+@Component({
+    selector: 'sp-asset-details-site',
+    templateUrl: './asset-details-site.component.html',
+})
+export class AssetDetailsSiteComponent implements OnChanges {
+    @Input()
+    asset: SpAsset;
+
+    @Input()
+    editMode: boolean;
+
+    @Input()
+    sites: AssetSiteDesc[];
+
+    currentSite: AssetSiteDesc;
+
+    constructor() {}
+
+    ngOnChanges(changes: SimpleChanges) {
+        if (changes['asset']) {
+            this.asset.assetSite ??= {
+                area: undefined,
+                siteId: undefined,
+                hasExactLocation: false,
+            };
+            if (this.sites.length > 0) {
+                if (this.asset.assetSite.siteId !== undefined) {
+                    this.selectCurrentSite(this.asset.assetSite.siteId);
+                }
+            }
+        }
+    }
+
+    handleLocationChange(event: MatSelectChange) {
+        this.selectCurrentSite(event.value);
+    }
+
+    selectCurrentSite(siteId: string): void {
+        this.currentSite = this.sites.find(loc => loc._id === siteId);
+    }
+}
diff --git a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-location/asset-location.component.html b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-location/asset-location.component.html
new file mode 100644
index 0000000000..64443f8483
--- /dev/null
+++ b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-location/asset-location.component.html
@@ -0,0 +1,34 @@
+<!--
+  ~ 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" *ngIf="locationConfig && locationConfig.locationEnabled">
+    <mat-checkbox
+        [disabled]="!editMode"
+        [(ngModel)]="asset.assetSite.hasExactLocation"
+    >
+        Add exact location
+    </mat-checkbox>
+    <div *ngIf="asset.assetSite.hasExactLocation">
+        <sp-single-marker-map
+            [readonly]="!editMode"
+            [assetLocation]="asset.assetSite.location"
+            [locationConfig]="locationConfig"
+        >
+        </sp-single-marker-map>
+    </div>
+</div>
diff --git a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-location/asset-location.component.ts b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-location/asset-location.component.ts
new file mode 100644
index 0000000000..f0a7f7efe3
--- /dev/null
+++ b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-location/asset-location.component.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 {
+    LocationConfig,
+    LocationConfigService,
+    SpAsset,
+} from '@streampipes/platform-services';
+
+@Component({
+    selector: 'sp-asset-location',
+    templateUrl: './asset-location.component.html',
+})
+export class AssetLocationComponent implements OnInit {
+    @Input()
+    asset: SpAsset;
+
+    @Input()
+    editMode: boolean;
+
+    locationConfig: LocationConfig;
+
+    constructor(private locationConfigService: LocationConfigService) {}
+
+    ngOnInit() {
+        this.asset.assetSite.location ??= {
+            coordinates: {
+                latitude: 0,
+                longitude: 0,
+            },
+            zoom: 1,
+        };
+        this.locationConfigService
+            .getLocationConfig()
+            .subscribe(res => (this.locationConfig = res));
+    }
+}
diff --git a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-details-links.component.html b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-details-links.component.html
index 10795c7ac6..bb5d58a2bc 100644
--- a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-details-links.component.html
+++ b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-details-links.component.html
@@ -50,7 +50,7 @@
             class="mt-10"
         >
             <div *ngFor="let assetLinkType of assetLinkTypes">
-                <sp-asset-link-section-component
+                <sp-asset-link-section
                     [assetLinkType]="assetLinkType"
                     [asset]="asset"
                     [editMode]="editMode"
@@ -58,7 +58,7 @@
                         openEditAssetLinkDialog($event, false)
                     "
                 >
-                </sp-asset-link-section-component>
+                </sp-asset-link-section>
             </div>
         </div>
     </div>
diff --git a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-details-links.component.ts b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-details-links.component.ts
index c56cf74786..b9baa36975 100644
--- a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-details-links.component.ts
+++ b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-details-links.component.ts
@@ -29,7 +29,7 @@ import { EditAssetLinkDialogComponent } from '../../../../dialog/edit-asset-link
 import { AssetConstants } from '../../../../constants/asset.constants';
 
 @Component({
-    selector: 'sp-asset-details-links-component',
+    selector: 'sp-asset-details-links',
     templateUrl: './asset-details-links.component.html',
 })
 export class AssetDetailsLinksComponent implements OnInit {
diff --git a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-link-section/asset-link-item/asset-link-item.component.ts b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-link-section/asset-link-item/asset-link-item.component.ts
index 34825aba31..46eda1e238 100644
--- a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-link-section/asset-link-item/asset-link-item.component.ts
+++ b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-link-section/asset-link-item/asset-link-item.component.ts
@@ -21,7 +21,7 @@ import { AssetLink, AssetLinkType } from '@streampipes/platform-services';
 import { Router } from '@angular/router';
 
 @Component({
-    selector: 'sp-asset-link-item-component',
+    selector: 'sp-asset-link-item',
     templateUrl: './asset-link-item.component.html',
     styleUrls: ['./asset-link-item.component.scss'],
 })
diff --git a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-link-section/asset-link-section.component.html b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-link-section/asset-link-section.component.html
index d5e20ef909..8d936f3f62 100644
--- a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-link-section/asset-link-section.component.html
+++ b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-link-section/asset-link-section.component.html
@@ -27,7 +27,7 @@
                 | assetTypeFilter: assetLinkType.linkType
         "
     >
-        <sp-asset-link-item-component
+        <sp-asset-link-item
             [assetLink]="link"
             [assetLinkType]="assetLinkType"
             [editMode]="editMode"
@@ -35,6 +35,6 @@
             (deleteAssetLinkEmitter)="deleteAssetLink($event)"
             class="asset-link-item"
         >
-        </sp-asset-link-item-component>
+        </sp-asset-link-item>
     </div>
 </div>
diff --git a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-link-section/asset-link-section.component.ts b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-link-section/asset-link-section.component.ts
index ebfe1055ff..f92f3a6287 100644
--- a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-link-section/asset-link-section.component.ts
+++ b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-links/asset-link-section/asset-link-section.component.ts
@@ -24,7 +24,7 @@ import {
 } from '@streampipes/platform-services';
 
 @Component({
-    selector: 'sp-asset-link-section-component',
+    selector: 'sp-asset-link-section',
     templateUrl: './asset-link-section.component.html',
     styleUrls: ['./asset-link-section.component.scss'],
 })
diff --git a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.html b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.html
index 9b9157f43c..0e95be38a9 100644
--- a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.html
+++ b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.html
@@ -19,19 +19,21 @@
 <div fxFlex="100" fxLayout="column" *ngIf="asset">
     <mat-tab-group color="accent" [mat-stretch-tabs]="false">
         <mat-tab label="Basics" data-cy="asset-tab-basic">
-            <sp-asset-details-basics-component
+            <sp-asset-details-basics
                 [asset]="asset"
+                [sites]="sites"
+                [rootNode]="rootNode"
                 [editMode]="editMode"
             >
-            </sp-asset-details-basics-component>
+            </sp-asset-details-basics>
         </mat-tab>
         <mat-tab label="Asset Links" data-cy="asset-tab-asset-links">
-            <sp-asset-details-links-component
+            <sp-asset-details-links
                 [asset]="asset"
                 [editMode]="editMode"
                 (updateAssetEmitter)="updateAssetEmitter.emit($event)"
             >
-            </sp-asset-details-links-component>
+            </sp-asset-details-links>
         </mat-tab>
     </mat-tab-group>
 </div>
diff --git a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.ts b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.ts
index e61b6d379b..c4a7a55fc2 100644
--- a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.ts
+++ b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.ts
@@ -17,10 +17,10 @@
  */
 
 import { Component, EventEmitter, Input, Output } from '@angular/core';
-import { SpAsset } from '@streampipes/platform-services';
+import { AssetSiteDesc, SpAsset } from '@streampipes/platform-services';
 
 @Component({
-    selector: 'sp-asset-details-panel-component',
+    selector: 'sp-asset-details-panel',
     templateUrl: './asset-details-panel.component.html',
     styleUrls: ['./asset-details-panel.component.scss'],
 })
@@ -31,6 +31,12 @@ export class SpAssetDetailsPanelComponent {
     @Input()
     editMode: boolean;
 
+    @Input()
+    rootNode: boolean;
+
+    @Input()
+    sites: AssetSiteDesc[];
+
     @Output()
     updateAssetEmitter: EventEmitter<SpAsset> = new EventEmitter<SpAsset>();
 }
diff --git a/ui/src/app/assets/components/asset-details/asset-details.component.html b/ui/src/app/assets/components/asset-details/asset-details.component.html
index e99f362281..2e0928fd96 100644
--- a/ui/src/app/assets/components/asset-details/asset-details.component.html
+++ b/ui/src/app/assets/components/asset-details/asset-details.component.html
@@ -51,25 +51,27 @@
                 class="asset-tree-panel"
             >
                 <div fxLayout="column" fxFlex="100">
-                    <sp-asset-selection-panel-component
+                    <sp-asset-selection-panel
                         [editMode]="editMode"
                         [assetModel]="asset"
                         [selectedAsset]="selectedAsset"
-                        (selectedAssetEmitter)="selectedAsset = $event"
+                        (selectedAssetEmitter)="applySelectedAsset($event)"
                     >
-                    </sp-asset-selection-panel-component>
+                    </sp-asset-selection-panel>
                 </div>
             </mat-drawer>
             <mat-drawer-content class="h-100 asset-details-panel">
-                <sp-asset-details-panel-component
+                <sp-asset-details-panel
                     *ngIf="selectedAsset"
                     [asset]="selectedAsset"
+                    [sites]="sites"
+                    [rootNode]="rootNode"
                     [editMode]="editMode"
                     (updateAssetEmitter)="updateAsset()"
                     fxFlex="100"
                     fxLayout="row"
                 >
-                </sp-asset-details-panel-component>
+                </sp-asset-details-panel>
             </mat-drawer-content>
         </mat-drawer-container>
     </div>
diff --git a/ui/src/app/assets/components/asset-details/asset-details.component.ts b/ui/src/app/assets/components/asset-details/asset-details.component.ts
index 7956dae908..cbb53dc398 100644
--- a/ui/src/app/assets/components/asset-details/asset-details.component.ts
+++ b/ui/src/app/assets/components/asset-details/asset-details.component.ts
@@ -21,21 +21,25 @@ import { SpBreadcrumbService } from '@streampipes/shared-ui';
 import { ActivatedRoute } from '@angular/router';
 import { AssetConstants } from '../../constants/asset.constants';
 import {
+    AssetSiteDesc,
     GenericStorageService,
     SpAsset,
     SpAssetModel,
 } from '@streampipes/platform-services';
 import { SpAssetRoutes } from '../../assets.routes';
+import { zip } from 'rxjs';
 
 @Component({
-    selector: 'sp-asset-details-component',
+    selector: 'sp-asset-details',
     templateUrl: './asset-details.component.html',
     styleUrls: ['./asset-details.component.scss'],
 })
 export class SpAssetDetailsComponent implements OnInit {
     asset: SpAssetModel;
+    sites: AssetSiteDesc[] = [];
 
     selectedAsset: SpAsset;
+    rootNode = true;
 
     editMode: boolean;
 
@@ -50,22 +54,33 @@ export class SpAssetDetailsComponent implements OnInit {
     ngOnInit(): void {
         this.assetModelId = this.route.snapshot.params.assetId;
         this.editMode = this.route.snapshot.queryParams.editMode;
-        this.loadAsset();
+        this.loadResources();
     }
 
-    loadAsset(): void {
-        this.genericStorageService
-            .getDocument(AssetConstants.ASSET_APP_DOC_NAME, this.assetModelId)
-            .subscribe(asset => {
-                this.asset = asset;
-                if (!this.selectedAsset) {
-                    this.selectedAsset = this.asset;
-                }
-                this.breadcrumbService.updateBreadcrumb([
-                    SpAssetRoutes.BASE,
-                    { label: this.asset.assetName },
-                ]);
-            });
+    loadResources(): void {
+        const assetReq = this.genericStorageService.getDocument(
+            AssetConstants.ASSET_APP_DOC_NAME,
+            this.assetModelId,
+        );
+        const locationsReq = this.genericStorageService.getAllDocuments(
+            AssetConstants.ASSET_SITES_APP_DOC_NAME,
+        );
+        zip([assetReq, locationsReq]).subscribe(res => {
+            this.asset = res[0];
+            this.sites = res[1];
+            if (!this.selectedAsset) {
+                this.selectedAsset = this.asset;
+            }
+            this.breadcrumbService.updateBreadcrumb([
+                SpAssetRoutes.BASE,
+                { label: this.asset.assetName },
+            ]);
+        });
+    }
+
+    applySelectedAsset(event: { asset: SpAsset; rootNode: boolean }): void {
+        this.selectedAsset = event.asset;
+        this.rootNode = event.rootNode;
     }
 
     updateAsset() {
@@ -76,7 +91,7 @@ export class SpAssetDetailsComponent implements OnInit {
         this.genericStorageService
             .updateDocument(AssetConstants.ASSET_APP_DOC_NAME, this.asset)
             .subscribe(res => {
-                this.loadAsset();
+                this.loadResources();
                 this.editMode = false;
             });
     }
diff --git a/ui/src/app/assets/components/asset-details/asset-selection-panel/asset-selection-panel.component.html b/ui/src/app/assets/components/asset-details/asset-selection-panel/asset-selection-panel.component.html
index 851c39ef78..bf93eed73a 100644
--- a/ui/src/app/assets/components/asset-details/asset-selection-panel/asset-selection-panel.component.html
+++ b/ui/src/app/assets/components/asset-details/asset-selection-panel/asset-selection-panel.component.html
@@ -45,7 +45,7 @@
                         "
                         fxLayout="row"
                         fxFlex="100"
-                        (click)="selectNode(node)"
+                        (click)="selectNode(node, false)"
                     >
                         <span fxLayoutAlign="end center">{{
                             node.assetName
@@ -94,7 +94,7 @@
                             "
                             fxLayout="row"
                             fxFlex="100"
-                            (click)="selectNode(node)"
+                            (click)="selectNode(node, true)"
                         >
                             <span fxLayoutAlign="start center">{{
                                 node.assetName
diff --git a/ui/src/app/assets/components/asset-details/asset-selection-panel/asset-selection-panel.component.ts b/ui/src/app/assets/components/asset-details/asset-selection-panel/asset-selection-panel.component.ts
index b69e8566aa..80674a89ed 100644
--- a/ui/src/app/assets/components/asset-details/asset-selection-panel/asset-selection-panel.component.ts
+++ b/ui/src/app/assets/components/asset-details/asset-selection-panel/asset-selection-panel.component.ts
@@ -17,7 +17,6 @@
  */
 
 import {
-    AfterViewInit,
     Component,
     EventEmitter,
     Input,
@@ -30,7 +29,7 @@ import { NestedTreeControl } from '@angular/cdk/tree';
 import { MatTreeNestedDataSource } from '@angular/material/tree';
 
 @Component({
-    selector: 'sp-asset-selection-panel-component',
+    selector: 'sp-asset-selection-panel',
     templateUrl: './asset-selection-panel.component.html',
     styleUrls: ['./asset-selection-panel.component.scss'],
 })
@@ -45,7 +44,8 @@ export class SpAssetSelectionPanelComponent implements OnInit {
     editMode: boolean;
 
     @Output()
-    selectedAssetEmitter: EventEmitter<SpAsset> = new EventEmitter<SpAsset>();
+    selectedAssetEmitter: EventEmitter<{ asset: SpAsset; rootNode: boolean }> =
+        new EventEmitter<{ asset: SpAsset; rootNode: boolean }>();
 
     treeControl = new NestedTreeControl<SpAsset>(node => node.assets);
     dataSource = new MatTreeNestedDataSource<SpAsset>();
@@ -63,8 +63,8 @@ export class SpAssetSelectionPanelComponent implements OnInit {
         this.treeControl.expandAll();
     }
 
-    selectNode(asset: SpAsset) {
-        this.selectedAssetEmitter.emit(asset);
+    selectNode(asset: SpAsset, rootNode: boolean) {
+        this.selectedAssetEmitter.emit({ asset, rootNode });
     }
 
     addAsset(node: SpAsset) {
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 ba13abcf04..956c37fb0a 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
@@ -38,7 +38,7 @@ import { saveAs } from 'file-saver';
 import { IdGeneratorService } from '../../../core-services/id-generator/id-generator.service';
 
 @Component({
-    selector: 'sp-asset-overview-component',
+    selector: 'sp-asset-overview',
     templateUrl: './asset-overview.component.html',
     styleUrls: ['./asset-overview.component.scss'],
 })
diff --git a/ui/src/app/assets/constants/asset.constants.ts b/ui/src/app/assets/constants/asset.constants.ts
index 9d99800ca1..879e1a1243 100644
--- a/ui/src/app/assets/constants/asset.constants.ts
+++ b/ui/src/app/assets/constants/asset.constants.ts
@@ -18,5 +18,6 @@
 
 export class AssetConstants {
     public static ASSET_APP_DOC_NAME = 'asset-management';
+    public static ASSET_SITES_APP_DOC_NAME = 'asset-sites';
     public static ASSET_LINK_TYPES_DOC_NAME = 'asset-link-type';
 }
diff --git a/ui/src/app/configuration/configuration-tabs.ts b/ui/src/app/configuration/configuration-tabs.ts
index f897f212a5..e3359dd72f 100644
--- a/ui/src/app/configuration/configuration-tabs.ts
+++ b/ui/src/app/configuration/configuration-tabs.ts
@@ -66,6 +66,11 @@ export class SpConfigurationTabs {
                 itemTitle: 'Security',
                 itemLink: ['configuration', 'security'],
             },
+            {
+                itemId: 'sites',
+                itemTitle: 'Sites',
+                itemLink: ['configuration', 'sites'],
+            },
         ];
     }
 }
diff --git a/ui/src/app/configuration/configuration.module.ts b/ui/src/app/configuration/configuration.module.ts
index 305a667d6d..3d781332a1 100644
--- a/ui/src/app/configuration/configuration.module.ts
+++ b/ui/src/app/configuration/configuration.module.ts
@@ -79,6 +79,13 @@ import { EndpointItemComponent } from './extensions-installation/endpoint-item/e
 import { SpExtensionsInstallationComponent } from './extensions-installation/extensions-installation.component';
 import { MatMenuModule } from '@angular/material/menu';
 import { SpConfigurationLinkSettingsComponent } from './general-configuration/link-settings/link-settings.component';
+import { SitesConfigurationComponent } from './sites-configuration/sites-configuration.component';
+import { LocationFeaturesConfigurationComponent } from './sites-configuration/location-features-configuration/location-features-configuration.component';
+import { SiteAreaConfigurationComponent } from './sites-configuration/site-area-configuration/site-area-configuration.component';
+import { MatSort } from '@angular/material/sort';
+import { ManageSiteDialogComponent } from './dialog/manage-site/manage-site-dialog.component';
+import { EditAssetLocationComponent } from './dialog/manage-site/edit-location/edit-location.component';
+import { EditAssetLocationAreaComponent } from './dialog/manage-site/edit-location/edit-location-area/edit-location-area.component';
 
 @NgModule({
     imports: [
@@ -148,12 +155,17 @@ import { SpConfigurationLinkSettingsComponent } from './general-configuration/li
                         path: 'security',
                         component: SecurityConfigurationComponent,
                     },
+                    {
+                        path: 'sites',
+                        component: SitesConfigurationComponent,
+                    },
                 ],
             },
         ]),
         SharedUiModule,
         ColorPickerModule,
         CodemirrorModule,
+        MatSort,
     ],
     declarations: [
         ServiceConfigsComponent,
@@ -162,16 +174,22 @@ import { SpConfigurationLinkSettingsComponent } from './general-configuration/li
         ServiceConfigsBooleanComponent,
         ServiceConfigsNumberComponent,
         DeleteDatalakeIndexComponent,
+        EditAssetLocationComponent,
+        EditAssetLocationAreaComponent,
         EditUserDialogComponent,
         EditGroupDialogComponent,
         EmailConfigurationComponent,
         GeneralConfigurationComponent,
         ExtensionsServiceManagementComponent,
+        LocationFeaturesConfigurationComponent,
+        ManageSiteDialogComponent,
+        SitesConfigurationComponent,
         SecurityAuthenticationConfigurationComponent,
         SecurityConfigurationComponent,
         SecurityUserConfigComponent,
         SecurityUserGroupConfigComponent,
         SecurityServiceConfigComponent,
+        SiteAreaConfigurationComponent,
         MessagingConfigurationComponent,
         DatalakeConfigurationComponent,
         SpConfigurationLinkSettingsComponent,
diff --git a/ui/src/app/configuration/configuration.routes.ts b/ui/src/app/configuration/configuration.routes.ts
index 14ac644c49..83500ea14d 100644
--- a/ui/src/app/configuration/configuration.routes.ts
+++ b/ui/src/app/configuration/configuration.routes.ts
@@ -19,6 +19,5 @@
 import { SpBreadcrumbItem } from '@streampipes/shared-ui';
 
 export class SpConfigurationRoutes {
-    static CONFIGURATION_BASE_LINK = 'configuration';
     static BASE: SpBreadcrumbItem = { label: 'Configuration' };
 }
diff --git a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.html b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.html
new file mode 100644
index 0000000000..ad0392c3e0
--- /dev/null
+++ b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.html
@@ -0,0 +1,68 @@
+<!--
+  ~ 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" class="w-100" fxLayoutGap="10px">
+    <div
+        *ngFor="let area of site.areas; let i = index"
+        fxLayout="row"
+        fxFlex="100"
+        class="area"
+    >
+        <div fxFlex fxLayoutAlign="start center">{{ area }}</div>
+        <div fxLayoutAlign="end center">
+            <button
+                [attr.data-cy]="
+                    'sites-dialog-remove-area-button_' + area.replace(' ', '_')
+                "
+                mat-icon-button
+                color="accent"
+                (click)="removeArea(area)"
+            >
+                <mat-icon>remove</mat-icon>
+            </button>
+        </div>
+    </div>
+    <div *ngIf="site.areas.length === 0" fxLayoutAlign="start center">
+        <div class="no-areas-defined">No areas defined yet.</div>
+    </div>
+    <div fxLayout="row" fxLayoutGap="10px" class="w-100 mt-10">
+        <div fxFlex fxLayoutAlign="start center">
+            <mat-form-field
+                color="accent"
+                class="w-100"
+                subscriptSizing="dynamic"
+            >
+                <input
+                    data-cy="sites-dialog-new-area-input"
+                    matInput
+                    [(ngModel)]="newArea"
+                />
+            </mat-form-field>
+        </div>
+        <div fxLayoutAlign="end center">
+            <button
+                data-cy="sites-dialog-add-area-button"
+                mat-icon-button
+                color="accent"
+                (click)="addNewArea()"
+            >
+                <mat-icon>add</mat-icon>
+            </button>
+        </div>
+    </div>
+</div>
diff --git a/ui/cypress/support/utils/configuration/ConfigutationUtils.ts b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.scss
similarity index 84%
rename from ui/cypress/support/utils/configuration/ConfigutationUtils.ts
rename to ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.scss
index 44731e6e4f..d1743a813a 100644
--- a/ui/cypress/support/utils/configuration/ConfigutationUtils.ts
+++ b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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.
@@ -16,8 +16,13 @@
  *
  */
 
-export class ConfigurationUtils {
-    public static goToConfigurationExport() {
-        cy.visit('#/configuration/export');
-    }
+.no-areas-defined {
+    margin-top: 5px;
+    margin-bottom: 5px;
+    font-size: smaller;
+}
+
+.area {
+    padding: 5px;
+    font-weight: bold;
 }
diff --git a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.ts b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.ts
new file mode 100644
index 0000000000..ddd7baab40
--- /dev/null
+++ b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.ts
@@ -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.
+ *
+ */
+
+import { Component, Input } from '@angular/core';
+import { AssetSiteDesc } from '@streampipes/platform-services';
+
+@Component({
+    selector: 'sp-edit-asset-location-area-component',
+    templateUrl: './edit-location-area.component.html',
+    styleUrls: ['./edit-location-area.component.scss'],
+})
+export class EditAssetLocationAreaComponent {
+    @Input()
+    site: AssetSiteDesc;
+
+    newArea: string = '';
+
+    addNewArea(): void {
+        this.site.areas.push(this.newArea);
+    }
+
+    removeArea(area: string): void {
+        this.site.areas.splice(this.site.areas.indexOf(area), 1);
+    }
+}
diff --git a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.html b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.html
new file mode 100644
index 0000000000..0c24fba58f
--- /dev/null
+++ b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.html
@@ -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.
+  ~
+  -->
+
+<div fxLayout="column">
+    <sp-basic-field-description
+        fxFlex="100"
+        descriptionPanelWidth="30"
+        label="Label"
+        description="A label which describes the location"
+    >
+        <mat-form-field color="accent" class="w-100" subscriptSizing="dynamic">
+            <input
+                data-cy="sites-dialog-site-input"
+                matInput
+                [(ngModel)]="site.label"
+            />
+        </mat-form-field>
+    </sp-basic-field-description>
+    <sp-basic-field-description
+        fxFlex="100"
+        descriptionPanelWidth="30"
+        class="mt-10"
+        label="Areas"
+        description="Available areas within the location"
+    >
+        <sp-edit-asset-location-area-component [site]="site" fxFlex="100">
+        </sp-edit-asset-location-area-component>
+    </sp-basic-field-description>
+    <sp-basic-field-description
+        *ngIf="locationConfig.locationEnabled"
+        fxFlex="100"
+        descriptionPanelWidth="30"
+        class="mt-10"
+        label="Location"
+        description="Exact location of the site"
+    >
+        <sp-single-marker-map
+            fxFlex="100"
+            [locationConfig]="locationConfig"
+            [assetLocation]="site.location"
+        >
+        </sp-single-marker-map>
+    </sp-basic-field-description>
+</div>
diff --git a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.ts b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.ts
new file mode 100644
index 0000000000..9b1bc554c9
--- /dev/null
+++ b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.ts
@@ -0,0 +1,32 @@
+/*
+ * 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 } from '@angular/core';
+import { AssetSiteDesc, LocationConfig } from '@streampipes/platform-services';
+
+@Component({
+    selector: 'sp-edit-asset-location-component',
+    templateUrl: './edit-location.component.html',
+})
+export class EditAssetLocationComponent {
+    @Input()
+    site: AssetSiteDesc;
+
+    @Input()
+    locationConfig: LocationConfig;
+}
diff --git a/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.html b/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.html
new file mode 100644
index 0000000000..5cff55e084
--- /dev/null
+++ b/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.html
@@ -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.
+  ~
+  -->
+
+<div class="sp-dialog-container">
+    <div class="sp-dialog-content p-15">
+        <div *ngIf="clonedSite !== undefined">
+            <sp-edit-asset-location-component
+                [site]="clonedSite"
+                [locationConfig]="locationConfig"
+            >
+            </sp-edit-asset-location-component>
+        </div>
+    </div>
+    <mat-divider></mat-divider>
+    <div class="sp-dialog-actions">
+        <button
+            mat-button
+            mat-raised-button
+            data-cy="sites-dialog-save-button"
+            color="accent"
+            (click)="store()"
+            style="margin-right: 10px"
+        >
+            Save changes
+        </button>
+        <button
+            mat-button
+            mat-raised-button
+            class="mat-basic"
+            data-cy="sites-dialog-cancel-button"
+            (click)="close()"
+            style="margin-right: 10px"
+        >
+            Cancel
+        </button>
+    </div>
+</div>
diff --git a/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.scss b/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.scss
new file mode 100644
index 0000000000..18577c0d5e
--- /dev/null
+++ b/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.scss
@@ -0,0 +1,35 @@
+/*!
+ * 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.
+ *
+ */
+
+.location-selection-panel {
+    border-right: 1px solid var(--color-bg-2);
+}
+
+.location {
+    padding: 15px;
+    border-bottom: 1px solid var(--color-bg-2);
+}
+
+.location:hover {
+    background: var(--color-bg-2);
+    cursor: pointer;
+}
+
+.add-location-button {
+    border-bottom: 1px solid var(--color-bg-3);
+}
diff --git a/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.ts b/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.ts
new file mode 100644
index 0000000000..0c69f44770
--- /dev/null
+++ b/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.ts
@@ -0,0 +1,83 @@
+/*
+ * 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 { DialogRef } from '@streampipes/shared-ui';
+import {
+    AssetSiteDesc,
+    GenericStorageService,
+    LocationConfig,
+} from '@streampipes/platform-services';
+import { AssetConstants } from '../../../assets/constants/asset.constants';
+
+@Component({
+    selector: 'sp-manage-site-dialog-component',
+    templateUrl: './manage-site-dialog.component.html',
+    styleUrls: ['./manage-site-dialog.component.scss'],
+})
+export class ManageSiteDialogComponent implements OnInit {
+    @Input()
+    site: AssetSiteDesc;
+
+    @Input()
+    locationConfig: LocationConfig;
+
+    clonedSite: AssetSiteDesc;
+    createMode = false;
+
+    constructor(
+        private dialogRef: DialogRef<ManageSiteDialogComponent>,
+        private genericStorageService: GenericStorageService,
+    ) {}
+
+    ngOnInit(): void {
+        if (this.site !== undefined) {
+            this.clonedSite = JSON.parse(JSON.stringify(this.site));
+        } else {
+            this.initializeNewSite();
+        }
+    }
+
+    close(emitReload = false): void {
+        this.dialogRef.close(emitReload);
+    }
+
+    initializeNewSite(): void {
+        this.clonedSite = {
+            appDocType: AssetConstants.ASSET_SITES_APP_DOC_NAME,
+            _id: undefined,
+            label: 'New site',
+            location: { coordinates: { latitude: 0, longitude: 0 } },
+            areas: [],
+        };
+        this.createMode = true;
+    }
+
+    store(): void {
+        const observable = this.createMode
+            ? this.genericStorageService.createDocument(
+                  AssetConstants.ASSET_SITES_APP_DOC_NAME,
+                  this.clonedSite,
+              )
+            : this.genericStorageService.updateDocument(
+                  AssetConstants.ASSET_SITES_APP_DOC_NAME,
+                  this.clonedSite,
+              );
+        observable.subscribe(res => this.close(true));
+    }
+}
diff --git a/ui/src/app/configuration/sites-configuration/location-features-configuration/location-features-configuration.component.html b/ui/src/app/configuration/sites-configuration/location-features-configuration/location-features-configuration.component.html
new file mode 100644
index 0000000000..eb6e28df23
--- /dev/null
+++ b/ui/src/app/configuration/sites-configuration/location-features-configuration/location-features-configuration.component.html
@@ -0,0 +1,83 @@
+<!--
+  ~ 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">
+    <form
+        [formGroup]="locationForm"
+        fxFlex="100"
+        fxLayout="column"
+        *ngIf="locationConfig"
+    >
+        <sp-split-section
+            title="Geo features"
+            subtitle="Geo features are used to better organize assets."
+        >
+            <mat-checkbox
+                formControlName="locationFeaturesEnabled"
+                data-cy="sites-enable-location-features-checkbox"
+                >Enable geo features
+            </mat-checkbox>
+            <div *ngIf="showLocationDetails" class="mt-10">
+                <div class="subsection-title">
+                    Tile server URL (use placeholders for x, y and z
+                    coordinates)
+                </div>
+                <mat-form-field
+                    color="accent"
+                    class="w-100"
+                    subscriptSizing="dynamic"
+                >
+                    <input
+                        formControlName="tileServerUrl"
+                        fxFlex
+                        matInput
+                        data-cy="sites-location-config-tile-server"
+                    />
+                </mat-form-field>
+                <div class="subsection-title mt-10">
+                    Attribution text if required by the tile server
+                </div>
+                <mat-form-field
+                    color="accent"
+                    class="w-100"
+                    subscriptSizing="dynamic"
+                >
+                    <input
+                        formControlName="attributionText"
+                        fxFlex
+                        matInput
+                        data-cy="sites-location-config-attribution-text"
+                    />
+                </mat-form-field>
+            </div>
+        </sp-split-section>
+        <sp-split-section>
+            <div>
+                <button
+                    mat-raised-button
+                    color="accent"
+                    data-cy="sites-location-features-button"
+                    [disabled]="!locationForm.valid"
+                    (click)="save()"
+                >
+                    Save
+                </button>
+            </div>
+        </sp-split-section>
+    </form>
+</div>
diff --git a/ui/src/app/configuration/sites-configuration/location-features-configuration/location-features-configuration.component.ts b/ui/src/app/configuration/sites-configuration/location-features-configuration/location-features-configuration.component.ts
new file mode 100644
index 0000000000..9e878f9a30
--- /dev/null
+++ b/ui/src/app/configuration/sites-configuration/location-features-configuration/location-features-configuration.component.ts
@@ -0,0 +1,107 @@
+/*
+ * 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, OnDestroy, OnInit } from '@angular/core';
+import {
+    UntypedFormBuilder,
+    UntypedFormControl,
+    UntypedFormGroup,
+    Validators,
+} from '@angular/forms';
+import {
+    LocationConfig,
+    LocationConfigService,
+} from '@streampipes/platform-services';
+import { Subscription } from 'rxjs';
+import { MatSnackBar } from '@angular/material/snack-bar';
+
+@Component({
+    selector: 'sp-location-features-configuration',
+    templateUrl: './location-features-configuration.component.html',
+})
+export class LocationFeaturesConfigurationComponent
+    implements OnInit, OnDestroy
+{
+    @Input()
+    locationConfig: LocationConfig;
+
+    locationForm: UntypedFormGroup;
+    formSubscription: Subscription;
+    showLocationDetails = false;
+
+    constructor(
+        private fb: UntypedFormBuilder,
+        private snackBar: MatSnackBar,
+        private locationConfigService: LocationConfigService,
+    ) {}
+
+    ngOnInit(): void {
+        this.locationForm = this.fb.group({});
+        this.showLocationDetails = this.locationConfig.locationEnabled;
+        this.locationForm.addControl(
+            'locationFeaturesEnabled',
+            new UntypedFormControl(this.locationConfig.locationEnabled),
+        );
+        this.locationForm.addControl(
+            'tileServerUrl',
+            new UntypedFormControl(
+                this.locationConfig.tileServerUrl,
+                this.showLocationDetails ? Validators.required : [],
+            ),
+        );
+        this.locationForm.addControl(
+            'attributionText',
+            new UntypedFormControl(this.locationConfig.attributionText || ''),
+        );
+        this.formSubscription = this.locationForm
+            .get('locationFeaturesEnabled')
+            .valueChanges.subscribe(checked => {
+                this.showLocationDetails = checked;
+                if (checked) {
+                    this.locationForm.controls.tileServerUrl.setValidators(
+                        Validators.required,
+                    );
+                } else {
+                    this.locationForm.controls.tileServerUrl.setValidators([]);
+                }
+            });
+    }
+
+    save(): void {
+        this.locationConfig.locationEnabled = this.locationForm.get(
+            'locationFeaturesEnabled',
+        ).value;
+        if (this.locationConfig.locationEnabled) {
+            this.locationConfig.tileServerUrl =
+                this.locationForm.get('tileServerUrl').value;
+            this.locationConfig.attributionText =
+                this.locationForm.get('attributionText').value;
+        }
+        this.locationConfigService
+            .updateLocationConfig(this.locationConfig)
+            .subscribe(() => {
+                this.snackBar.open('Location configuration updated', 'Ok', {
+                    duration: 3000,
+                });
+            });
+    }
+
+    ngOnDestroy() {
+        this.formSubscription?.unsubscribe();
+    }
+}
diff --git a/ui/src/app/configuration/sites-configuration/site-area-configuration/site-area-configuration.component.html b/ui/src/app/configuration/sites-configuration/site-area-configuration/site-area-configuration.component.html
new file mode 100644
index 0000000000..c7705c3c93
--- /dev/null
+++ b/ui/src/app/configuration/sites-configuration/site-area-configuration/site-area-configuration.component.html
@@ -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.
+  ~
+  -->
+
+<sp-split-section
+    title="Sites & Areas"
+    subtitle="Manage your organization's sites and production areas"
+>
+    <div fxLayout="row">
+        <button
+            mat-raised-button
+            color="accent"
+            data-cy="sites-manage-sites-button"
+            (click)="openManageSitesDialog(undefined)"
+        >
+            <mat-icon>add</mat-icon>
+            <span>New site</span>
+        </button>
+    </div>
+    <sp-table
+        class="mt-10"
+        [dataSource]="dataSource"
+        [columns]="displayedColumns"
+        matSort
+        data-cy="all-sites-table"
+    >
+        <ng-container matColumnDef="name">
+            <th mat-header-cell *matHeaderCellDef><b>Site</b></th>
+            <td mat-cell *matCellDef="let site" data-cy="site-table-row-label">
+                {{ site.label }}
+            </td>
+        </ng-container>
+        <ng-container matColumnDef="areas">
+            <th mat-header-cell *matHeaderCellDef><b>Areas</b></th>
+            <td mat-cell *matCellDef="let site" data-cy="site-table-row-areas">
+                {{ site.areas.toString() }}
+            </td>
+        </ng-container>
+        <ng-container matColumnDef="actions">
+            <th mat-header-cell *matHeaderCellDef></th>
+            <td mat-cell *matCellDef="let site">
+                <div fxLayout="row" fxLayoutAlign="end center">
+                    <button
+                        mat-icon-button
+                        color="accent"
+                        data-cy="sites-edit-button"
+                        (click)="openManageSitesDialog(site)"
+                    >
+                        <mat-icon>edit</mat-icon>
+                    </button>
+                    <button
+                        [attr.data-cy]="
+                            'sites-delete-button-' +
+                            site.label.replaceAll(' ', '_')
+                        "
+                        (click)="deleteSite(site)"
+                        mat-icon-button
+                        color="accent"
+                    >
+                        <mat-icon>delete</mat-icon>
+                    </button>
+                </div>
+            </td>
+        </ng-container>
+    </sp-table>
+</sp-split-section>
diff --git a/ui/src/app/configuration/sites-configuration/site-area-configuration/site-area-configuration.component.ts b/ui/src/app/configuration/sites-configuration/site-area-configuration/site-area-configuration.component.ts
new file mode 100644
index 0000000000..d0de6e25e9
--- /dev/null
+++ b/ui/src/app/configuration/sites-configuration/site-area-configuration/site-area-configuration.component.ts
@@ -0,0 +1,88 @@
+/*
+ * 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 {
+    AssetSiteDesc,
+    GenericStorageService,
+    LocationConfig,
+} from '@streampipes/platform-services';
+import { AssetConstants } from '../../../assets/constants/asset.constants';
+import { MatTableDataSource } from '@angular/material/table';
+import { ManageSiteDialogComponent } from '../../dialog/manage-site/manage-site-dialog.component';
+import { DialogService, PanelType } from '@streampipes/shared-ui';
+
+@Component({
+    selector: 'sp-site-area-configuration',
+    templateUrl: './site-area-configuration.component.html',
+})
+export class SiteAreaConfigurationComponent implements OnInit {
+    @Input()
+    locationConfig: LocationConfig;
+
+    allSites: AssetSiteDesc[] = [];
+    dataSource: MatTableDataSource<AssetSiteDesc> =
+        new MatTableDataSource<AssetSiteDesc>();
+    displayedColumns = ['name', 'areas', 'actions'];
+
+    constructor(
+        private genericStorageService: GenericStorageService,
+        private dialogService: DialogService,
+    ) {}
+
+    ngOnInit() {
+        this.loadSites();
+    }
+
+    loadSites(): void {
+        this.genericStorageService
+            .getAllDocuments(AssetConstants.ASSET_SITES_APP_DOC_NAME)
+            .subscribe(res => {
+                this.allSites = res;
+                this.dataSource.data = this.allSites;
+            });
+    }
+
+    deleteSite(site: AssetSiteDesc): void {
+        this.genericStorageService
+            .deleteDocument(
+                AssetConstants.ASSET_SITES_APP_DOC_NAME,
+                site._id,
+                site._rev,
+            )
+            .subscribe(() => this.loadSites());
+    }
+
+    openManageSitesDialog(site: AssetSiteDesc): void {
+        const dialogRef = this.dialogService.open(ManageSiteDialogComponent, {
+            panelType: PanelType.SLIDE_IN_PANEL,
+            title: 'Manage site',
+            width: '50vw',
+            data: {
+                site,
+                locationConfig: this.locationConfig,
+            },
+        });
+
+        dialogRef.afterClosed().subscribe(reload => {
+            if (reload) {
+                this.loadSites();
+            }
+        });
+    }
+}
diff --git a/ui/src/app/configuration/sites-configuration/sites-configuration.component.html b/ui/src/app/configuration/sites-configuration/sites-configuration.component.html
new file mode 100644
index 0000000000..939208cfb5
--- /dev/null
+++ b/ui/src/app/configuration/sites-configuration/sites-configuration.component.html
@@ -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.
+  ~
+  -->
+
+<sp-basic-nav-tabs [spNavigationItems]="tabs" [activeLink]="'sites'">
+    <div fxLayout="column" *ngIf="locationConfig">
+        <sp-location-features-configuration [locationConfig]="locationConfig">
+        </sp-location-features-configuration>
+
+        <sp-site-area-configuration [locationConfig]="locationConfig">
+        </sp-site-area-configuration>
+    </div>
+</sp-basic-nav-tabs>
diff --git a/ui/src/app/configuration/sites-configuration/sites-configuration.component.ts b/ui/src/app/configuration/sites-configuration/sites-configuration.component.ts
new file mode 100644
index 0000000000..2f26fe2b32
--- /dev/null
+++ b/ui/src/app/configuration/sites-configuration/sites-configuration.component.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { SpConfigurationTabs } from '../configuration-tabs';
+import {
+    LocationConfig,
+    LocationConfigService,
+} from '@streampipes/platform-services';
+
+@Component({
+    selector: 'sp-sites-configuration',
+    templateUrl: './sites-configuration.component.html',
+})
+export class SitesConfigurationComponent implements OnInit {
+    tabs = SpConfigurationTabs.getTabs();
+
+    locationConfig: LocationConfig;
+
+    constructor(private locationConfigService: LocationConfigService) {}
+
+    ngOnInit() {
+        this.locationConfigService.getLocationConfig().subscribe(res => {
+            this.locationConfig = res;
+        });
+    }
+}
diff --git a/ui/src/app/connect/services/transformation-rule.service.spec.ts b/ui/src/app/connect/services/transformation-rule.service.spec.ts
deleted file mode 100644
index a0030bf34f..0000000000
--- a/ui/src/app/connect/services/transformation-rule.service.spec.ts
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import { TransformationRuleService } from './transformation-rule.service';
-import {
-    CreateNestedRuleDescription,
-    EventPropertyNested,
-    EventPropertyPrimitive,
-    EventPropertyUnion,
-    EventSchema,
-    MoveRuleDescription,
-    RenameRuleDescription,
-} from '@streampipes/platform-services';
-
-describe('TransformationRuleService', () => {
-    const service = new TransformationRuleService();
-
-    it('Get complete key from schema nested', () => {
-        const eventProperties: EventPropertyUnion[] = [];
-        const eventPropertyPrimitive: EventPropertyPrimitive =
-            new EventPropertyPrimitive();
-        eventPropertyPrimitive.elementId = 'id_1';
-        eventPropertyPrimitive.runtimeName = 'a';
-        const eventPropertyNested: EventPropertyNested =
-            new EventPropertyNested();
-        eventPropertyNested.elementId = 'id_2';
-        eventPropertyNested.runtimeName = 'b';
-        eventPropertyNested.eventProperties = [];
-        eventPropertyNested.eventProperties.push(eventPropertyPrimitive);
-        eventProperties.push(eventPropertyNested);
-
-        const result: string = service.getCompleteRuntimeNameKey(
-            eventProperties,
-            'id_1',
-        );
-
-        expect(result).toBe('b.a');
-    });
-
-    it('Get complete key from schema primitve', () => {
-        const eventProperties: EventPropertyUnion[] = [];
-        const eventPropertyPrimitive: EventPropertyPrimitive =
-            new EventPropertyPrimitive();
-        eventPropertyPrimitive.elementId = 'id_1';
-        eventPropertyPrimitive.runtimeName = 'a';
-
-        eventProperties.push(eventPropertyPrimitive);
-
-        const result: string = service.getCompleteRuntimeNameKey(
-            eventProperties,
-            'id_1',
-        );
-
-        expect(result).toBe('a');
-    });
-
-    it('check get all ids with one id', () => {
-        const eventProperties: EventPropertyUnion[] = [];
-        const eventPropertyPrimitive: EventPropertyPrimitive =
-            new EventPropertyPrimitive();
-        eventPropertyPrimitive.elementId = 'id_1';
-        eventProperties.push(eventPropertyPrimitive);
-
-        const result: string[] = service.getAllIds(eventProperties);
-        expect(result.length).toBe(1);
-        expect(result[0]).toBe('id_1');
-    });
-
-    it('check get all ids with multiple ids', () => {
-        const eventProperties: EventPropertyUnion[] = [];
-        const eventPropertyPrimitive: EventPropertyPrimitive =
-            new EventPropertyPrimitive();
-        eventPropertyPrimitive.elementId = 'id_1';
-        const eventPropertyPrimitive1: EventPropertyPrimitive =
-            new EventPropertyPrimitive();
-        eventPropertyPrimitive1.elementId = 'id_2';
-        const eventPropertyNested: EventPropertyNested =
-            new EventPropertyNested();
-        eventPropertyNested.elementId = 'id_3';
-        eventPropertyNested.eventProperties = [];
-        eventPropertyNested.eventProperties.push(eventPropertyPrimitive1);
-
-        eventProperties.push(eventPropertyPrimitive);
-        eventProperties.push(eventPropertyNested);
-
-        const result: string[] = service.getAllIds(eventProperties);
-        expect(result.length).toBe(3);
-        expect(result[0]).toBe('id_1');
-        expect(result[2]).toBe('id_2');
-        expect(result[1]).toBe('id_3');
-    });
-
-    it('Create Nested Rules simple', () => {
-        const oldEventSchema: EventSchema = new EventSchema();
-        oldEventSchema.eventProperties = [];
-
-        const newEventSchema: EventSchema = new EventSchema();
-        const propertyNested: EventPropertyNested = new EventPropertyNested();
-        propertyNested.eventProperties = [];
-        propertyNested.elementId = 'id';
-        propertyNested.runtimeName = 'a';
-        newEventSchema.eventProperties = [];
-        newEventSchema.eventProperties.push(propertyNested);
-
-        const result: CreateNestedRuleDescription[] =
-            service.getCreateNestedRules(
-                newEventSchema.eventProperties,
-                oldEventSchema,
-                newEventSchema,
-            );
-
-        expect(result.length).toBe(1);
-        expect(result[0].runtimeKey).toBe('a');
-    });
-
-    it('Create Nested Rules nested', () => {
-        const oldEventSchema: EventSchema = new EventSchema();
-        oldEventSchema.eventProperties = [];
-
-        const newEventSchema: EventSchema = new EventSchema();
-        newEventSchema.eventProperties = [];
-        const nestedNested: EventPropertyNested = new EventPropertyNested();
-        nestedNested.runtimeName = 'a';
-        nestedNested.elementId = 'deepnested';
-        nestedNested.eventProperties = [];
-
-        const nestedProperty: EventPropertyNested = new EventPropertyNested();
-        nestedProperty.runtimeName = 'b';
-        nestedNested.elementId = 'nested';
-        nestedProperty.eventProperties = [];
-        nestedNested.eventProperties.push(nestedProperty);
-        newEventSchema.eventProperties.push(nestedNested);
-
-        const result: CreateNestedRuleDescription[] =
-            service.getCreateNestedRules(
-                newEventSchema.eventProperties,
-                oldEventSchema,
-                newEventSchema,
-            );
-
-        expect(result.length).toBe(2);
-        expect(result[0].runtimeKey).toBe('a');
-        expect(result[1].runtimeKey).toBe('a.b');
-    });
-
-    it('Create Move Rules simple', () => {
-        const oldEventSchema: EventSchema = new EventSchema();
-        const oldPropertyToMove: EventPropertyPrimitive =
-            new EventPropertyPrimitive();
-        oldPropertyToMove.runtimeName = 'a';
-        oldPropertyToMove.elementId = 'a1';
-        const oldNestedProperty: EventPropertyNested =
-            new EventPropertyNested();
-        oldNestedProperty.runtimeName = 'b';
-        oldNestedProperty.elementId = 'b1';
-        oldNestedProperty.eventProperties = [];
-        oldEventSchema.eventProperties = [];
-        oldEventSchema.eventProperties.push(oldPropertyToMove);
-        oldEventSchema.eventProperties.push(oldNestedProperty);
-
-        const newEventSchema: EventSchema = new EventSchema();
-        newEventSchema.eventProperties = [];
-        const newPropertyToMove: EventPropertyPrimitive =
-            new EventPropertyPrimitive();
-        newPropertyToMove.runtimeName = 'a';
-        newPropertyToMove.elementId = 'a1';
-        const newNestedProperty: EventPropertyNested =
-            new EventPropertyNested();
-        newNestedProperty.runtimeName = 'b';
-        newNestedProperty.elementId = 'b1';
-        newNestedProperty.eventProperties = [];
-        newNestedProperty.eventProperties.push(newPropertyToMove);
-        newEventSchema.eventProperties.push(newNestedProperty);
-
-        const result: MoveRuleDescription[] = service.getMoveRules(
-            newEventSchema.eventProperties,
-            oldEventSchema,
-            newEventSchema,
-        );
-
-        expect(result.length).toBe(1);
-        expect(result[0].oldRuntimeKey).toBe('a');
-    });
-
-    it('Rename simple', () => {
-        const oldEventSchema: EventSchema = new EventSchema();
-        const oldEventPropertyPrimitive: EventPropertyPrimitive =
-            new EventPropertyPrimitive();
-        oldEventPropertyPrimitive.elementId = 'id_1';
-        oldEventPropertyPrimitive.runtimeName = 'a';
-        oldEventSchema.eventProperties = [];
-        oldEventSchema.eventProperties.push(oldEventPropertyPrimitive);
-
-        const newEventSchema: EventSchema = new EventSchema();
-        const newEventPropertyPrimitive: EventPropertyPrimitive =
-            new EventPropertyPrimitive();
-        newEventPropertyPrimitive.elementId = 'id_1';
-        newEventPropertyPrimitive.runtimeName = 'b';
-        newEventSchema.eventProperties = [];
-        newEventSchema.eventProperties.push(newEventPropertyPrimitive);
-
-        const result: RenameRuleDescription[] = service.getRenameRules(
-            newEventSchema.eventProperties,
-            oldEventSchema,
-            newEventSchema,
-        );
-
-        expect(result.length).toBe(1);
-        expect(result[0].oldRuntimeKey).toBe('a');
-        expect(result[0].newRuntimeKey).toBe('b');
-    });
-
-    it('Rename nested', () => {
-        const oldEventSchema: EventSchema = new EventSchema();
-        const oldNestedEventProperty: EventPropertyNested =
-            new EventPropertyNested();
-        oldNestedEventProperty.elementId = 'id_2';
-        oldNestedEventProperty.runtimeName = 'b';
-        oldNestedEventProperty.eventProperties = [];
-        const oldEventPropertyPrimitive: EventPropertyPrimitive =
-            new EventPropertyPrimitive();
-        oldEventPropertyPrimitive.elementId = 'id_1';
-        oldEventPropertyPrimitive.runtimeName = 'a';
-        oldNestedEventProperty.eventProperties.push(oldEventPropertyPrimitive);
-        oldEventSchema.eventProperties = [];
-        oldEventSchema.eventProperties.push(oldNestedEventProperty);
-
-        const newEventSchema: EventSchema = new EventSchema();
-        const newNestedEventProperty: EventPropertyNested =
-            new EventPropertyNested();
-        newNestedEventProperty.elementId = 'id_2';
-        newNestedEventProperty.runtimeName = 'b';
-        const newEventPropertyPrimitive: EventPropertyPrimitive =
-            new EventPropertyPrimitive();
-        newEventPropertyPrimitive.elementId = 'id_1';
-        newEventPropertyPrimitive.runtimeName = 'b';
-        newNestedEventProperty.eventProperties = [];
-        newNestedEventProperty.eventProperties.push(newEventPropertyPrimitive);
-        newEventSchema.eventProperties = [];
-        newEventSchema.eventProperties.push(newNestedEventProperty);
-
-        const result: RenameRuleDescription[] = service.getRenameRules(
-            newEventSchema.eventProperties,
-            oldEventSchema,
-            newEventSchema,
-        );
-
-        expect(result.length).toBe(1);
-        expect(result[0].oldRuntimeKey).toBe('b.a');
-        expect(result[0].newRuntimeKey).toBe('b.b');
-    });
-});
diff --git a/ui/src/app/connect/services/transformation-rule.service.ts b/ui/src/app/connect/services/transformation-rule.service.ts
index 443abebab9..2d04eb8876 100644
--- a/ui/src/app/connect/services/transformation-rule.service.ts
+++ b/ui/src/app/connect/services/transformation-rule.service.ts
@@ -46,6 +46,8 @@ export class TransformationRuleService {
         private staticValueTransformService: StaticValueTransformService,
     ) {}
 
+    private delimiter = '<-=>';
+
     public getTransformationRuleDescriptions(
         originalSchema: EventSchema,
         targetSchema: EventSchema,
@@ -183,22 +185,22 @@ export class TransformationRuleService {
                 if (keyOld && keyNew) {
                     const keyOldPrefix = keyOld.substr(
                         0,
-                        keyOld.lastIndexOf('.'),
+                        keyOld.lastIndexOf(this.delimiter),
                     );
                     const keyNewPrefix = keyNew.substr(
                         0,
-                        keyNew.lastIndexOf('.'),
+                        keyNew.lastIndexOf(this.delimiter),
                     );
 
                     if (keyOldPrefix !== keyNewPrefix) {
                         let keyOfOldValue = '';
                         if (keyOldPrefix === '') {
                             keyOfOldValue = keyNew.substr(
-                                keyNew.lastIndexOf('.') + 1,
+                                keyNew.lastIndexOf(this.delimiter) + 1,
                             );
                         } else {
                             keyOfOldValue = `${keyOldPrefix}.${keyNew.substr(
-                                keyNew.lastIndexOf('.') + 1,
+                                keyNew.lastIndexOf(this.delimiter) + 1,
                             )}`;
                         }
 
@@ -393,7 +395,10 @@ export class TransformationRuleService {
                         id,
                     );
                     if (methodResult != null) {
-                        result = eventProperty.runtimeName + '.' + methodResult;
+                        result =
+                            eventProperty.runtimeName +
+                            this.delimiter +
+                            methodResult;
                     }
                 }
             }
@@ -408,7 +413,7 @@ export class TransformationRuleService {
 
     public getRuntimeNameKey(completeKey: string): string {
         if (completeKey) {
-            const keyElements = completeKey.split('.');
+            const keyElements = completeKey.split(this.delimiter);
 
             if (keyElements.length === 0) {
                 return completeKey;
diff --git a/ui/src/app/core-ui/core-ui.module.ts b/ui/src/app/core-ui/core-ui.module.ts
index 971206876f..bf9a91ff30 100644
--- a/ui/src/app/core-ui/core-ui.module.ts
+++ b/ui/src/app/core-ui/core-ui.module.ts
@@ -113,6 +113,8 @@ import { StaticTreeInputButtonMenuComponent } from './static-properties/static-r
 import { StaticTreeInputSelectedNodesComponent } from './static-properties/static-runtime-resolvable-tree-input/static-tree-input-selected-nodes/static-tree-input-selected-nodes.component';
 import { StaticTreeInputBrowseNodesComponent } from './static-properties/static-runtime-resolvable-tree-input/static-tree-input-browse-nodes/static-tree-input-browse-nodes.component';
 import { StaticTreeInputNodeDetailsComponent } from './static-properties/static-runtime-resolvable-tree-input/static-tree-input-node-details/static-tree-input-node-details.component';
+import { 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';
 
 @NgModule({
@@ -163,6 +165,7 @@ import { StaticTreeInputTextEditorComponent } from './static-properties/static-r
         QuillModule.forRoot(),
         MatTreeModule,
         MarkdownModule.forRoot(),
+        LeafletModule,
     ],
     declarations: [
         DataDownloadDialogComponent,
@@ -197,6 +200,7 @@ import { StaticTreeInputTextEditorComponent } from './static-properties/static-r
         StaticTreeInputNodeDetailsComponent,
         StaticTreeInputTextEditorComponent,
         StaticSlideToggleComponent,
+        SingleMarkerMapComponent,
         ErrorHintComponent,
         AddToCollectionComponent,
         PipelineStartedStatusComponent,
@@ -250,6 +254,7 @@ import { StaticTreeInputTextEditorComponent } from './static-properties/static-r
         StatusIndicatorComponent,
         MultiStepStatusIndicatorComponent,
         PipelineOperationStatusComponent,
+        SingleMarkerMapComponent,
     ],
 })
 export class CoreUiModule {}
diff --git a/ui/src/app/core-ui/single-marker-map/single-marker-map.component.html b/ui/src/app/core-ui/single-marker-map/single-marker-map.component.html
new file mode 100644
index 0000000000..921d108a40
--- /dev/null
+++ b/ui/src/app/core-ui/single-marker-map/single-marker-map.component.html
@@ -0,0 +1,29 @@
+<!--
+  ~ 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
+    class="w-100"
+    [ngStyle]="{ height: mapHeight + 'px' }"
+    leaflet
+    (leafletClick)="onMarkerAdded($event)"
+    [leafletOptions]="mapOptions"
+    (leafletZoomChange)="onZoomChange($event)"
+    (leafletMapReady)="onMapReady($event)"
+>
+    <div *ngFor="let layer of layers" [leafletLayer]="layer"></div>
+</div>
diff --git a/ui/src/app/core-ui/single-marker-map/single-marker-map.component.ts b/ui/src/app/core-ui/single-marker-map/single-marker-map.component.ts
new file mode 100644
index 0000000000..ffeb12657b
--- /dev/null
+++ b/ui/src/app/core-ui/single-marker-map/single-marker-map.component.ts
@@ -0,0 +1,129 @@
+/*
+ * 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 {
+    icon,
+    Layer,
+    LeafletMouseEvent,
+    Map,
+    MapOptions,
+    marker,
+    Marker,
+    tileLayer,
+} from 'leaflet';
+import {
+    AssetLocation,
+    LatLng,
+    LocationConfig,
+} from '@streampipes/platform-services';
+
+@Component({
+    selector: 'sp-single-marker-map',
+    templateUrl: './single-marker-map.component.html',
+})
+export class SingleMarkerMapComponent implements OnInit {
+    @Input()
+    locationConfig: LocationConfig;
+
+    @Input()
+    assetLocation: AssetLocation;
+
+    @Input()
+    mapHeight: number = 400;
+
+    @Input()
+    readonly = false;
+
+    map: Map;
+    mapOptions: MapOptions;
+    layers: Layer[];
+    marker: Marker;
+
+    ngOnInit() {
+        this.assetLocation ??= {
+            coordinates: {
+                latitude: 0,
+                longitude: 0,
+            },
+            zoom: 1,
+        };
+        this.mapOptions = {
+            layers: [
+                tileLayer(this.locationConfig.tileServerUrl, {
+                    maxZoom: 18,
+                    attribution: this.locationConfig.attributionText,
+                }),
+            ],
+            zoom: this.assetLocation.zoom || 1,
+            center: {
+                lat: this.assetLocation.coordinates.latitude,
+                lng: this.assetLocation.coordinates.longitude,
+            },
+        };
+    }
+
+    makeMarker(location: LatLng): Marker {
+        return marker(
+            { lat: location.latitude, lng: location.longitude },
+            {
+                icon: icon({
+                    iconSize: [25, 41],
+                    iconAnchor: [13, 41],
+                    iconUrl: 'assets/img/marker-icon.png',
+                    shadowUrl: 'assets/img/marker-shadow.png',
+                }),
+            },
+        );
+    }
+
+    onMapReady(map: Map) {
+        this.map = map;
+        this.map.attributionControl.setPrefix('');
+        this.map.invalidateSize();
+        this.addMarker(this.assetLocation.coordinates);
+    }
+
+    onZoomChange(zoom: number): void {
+        this.assetLocation.zoom = zoom;
+    }
+
+    onMarkerAdded(e: LeafletMouseEvent) {
+        if (!this.readonly) {
+            this.addMarker({
+                latitude: e.latlng.lat,
+                longitude: e.latlng.lng,
+            });
+        }
+    }
+
+    addMarker(location: LatLng): void {
+        if (location) {
+            if (!this.marker) {
+                this.marker = this.makeMarker(location);
+                this.marker.addTo(this.map);
+            } else {
+                this.marker.setLatLng({
+                    lat: location.latitude,
+                    lng: location.longitude,
+                });
+            }
+            this.assetLocation.coordinates = location;
+        }
+    }
+}


Test patch:
diff --git a/streampipes-connect-shared/src/test/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToOriginalSchemaConverterTest.java b/streampipes-connect-shared/src/test/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToOriginalSchemaConverterTest.java
index 1b8d964dd6..966ea56c84 100644
--- a/streampipes-connect-shared/src/test/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToOriginalSchemaConverterTest.java
+++ b/streampipes-connect-shared/src/test/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToOriginalSchemaConverterTest.java
@@ -40,6 +40,8 @@ import static org.apache.streampipes.connect.shared.preprocessing.convert.Helper
 
 public class ToOriginalSchemaConverterTest {
 
+  private static final String NESTED_DELIMITER = "<-=>";
+
   @Test
   public void testSimpleUnitConversion() {
     List<EventProperty> properties = makeSimpleProperties(true);
@@ -60,10 +62,11 @@ public class ToOriginalSchemaConverterTest {
 
     var rules = new ArrayList<TransformationRuleDescription>();
 
-    rules.add(makeUnitTransformationRule("nested.stringProp"));
+    rules.add(makeUnitTransformationRule("nested" + NESTED_DELIMITER + "stringProp"));
 
     var resultProperties = executeAndReturnResult(properties, rules);
-    var nestedResultProperty = ((EventPropertyNested) resultProperties.get(1)).getEventProperties().get(0);
+    var nestedResultProperty = ((EventPropertyNested) resultProperties.get(1)).getEventProperties()
+                                                                              .get(0);
 
     Assertions.assertEquals(2, resultProperties.size());
     Assertions.assertEquals("originalUnit", getUnit(nestedResultProperty));
@@ -73,7 +76,8 @@ public class ToOriginalSchemaConverterTest {
   public void testSimpleMoveConversion() {
     List<EventProperty> properties = makeNestedProperties();
     var nestedProperty = ((EventPropertyNested) properties.get(1));
-    nestedProperty.getEventProperties().add(EpProperties.stringEp(Labels.empty(), "epToBeMoved", ""));
+    nestedProperty.getEventProperties()
+                  .add(EpProperties.stringEp(Labels.empty(), "epToBeMoved", ""));
 
     var rules = new ArrayList<TransformationRuleDescription>();
 
@@ -81,10 +85,16 @@ public class ToOriginalSchemaConverterTest {
 
     var result = executeAndReturnResult(properties, rules);
     Assertions.assertEquals(3, result.size());
-    Assertions.assertEquals("timestamp",
-                            result.get(0).getRuntimeName());
-    Assertions.assertEquals(2,
-                            ((EventPropertyNested) result.get(1)).getEventProperties().size());
+    Assertions.assertEquals(
+        "timestamp",
+        result.get(0)
+              .getRuntimeName()
+    );
+    Assertions.assertEquals(
+        2,
+        ((EventPropertyNested) result.get(1)).getEventProperties()
+                                             .size()
+    );
   }
 
   @Test
@@ -95,12 +105,17 @@ public class ToOriginalSchemaConverterTest {
     rules.add(makeDeleteTransformationRule("epToBeRestored"));
     var result = executeAndReturnResult(properties, rules);
     Assertions.assertEquals(4, result.size());
-    Assertions.assertEquals("epToBeRestored",
-                            result.get(3).getRuntimeName());
+    Assertions.assertEquals(
+        "epToBeRestored",
+        result.get(3)
+              .getRuntimeName()
+    );
   }
 
-  private List<EventProperty> executeAndReturnResult(List<EventProperty> properties,
-                                                     List<TransformationRuleDescription> rules) {
+  private List<EventProperty> executeAndReturnResult(
+      List<EventProperty> properties,
+      List<TransformationRuleDescription> rules
+  ) {
     var result = new SchemaConverter().toOriginalSchema(new EventSchema(properties), rules);
     return result.getEventProperties();
   }
diff --git a/streampipes-connect-shared/src/test/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToTransformedSchemaConverterTest.java b/streampipes-connect-shared/src/test/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToTransformedSchemaConverterTest.java
index d31615e3d7..44443d151a 100644
--- a/streampipes-connect-shared/src/test/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToTransformedSchemaConverterTest.java
+++ b/streampipes-connect-shared/src/test/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToTransformedSchemaConverterTest.java
@@ -105,7 +105,7 @@ public class ToTransformedSchemaConverterTest {
 
     var rules = new ArrayList<TransformationRuleDescription>();
 
-    rules.add(makeUnitTransformationRule("nested.stringProp"));
+    rules.add(makeUnitTransformationRule("nested<-=>stringProp"));
 
     var resultProperties = executeAndReturnResult(properties, rules);
     var nestedResultProperty = ((EventPropertyNested) resultProperties.get(1)).getEventProperties().get(0);
diff --git a/ui/cypress/tests/assetManagement/createAsset.spec.ts b/ui/cypress/tests/assetManagement/createAsset.spec.ts
index e52d7b500c..7f5171631a 100644
--- a/ui/cypress/tests/assetManagement/createAsset.spec.ts
+++ b/ui/cypress/tests/assetManagement/createAsset.spec.ts
@@ -20,7 +20,7 @@ import { AdapterBuilder } from '../../support/builder/AdapterBuilder';
 import { ConnectUtils } from '../../support/utils/connect/ConnectUtils';
 import { AssetUtils } from '../../support/utils/asset/AssetUtils';
 import { DashboardUtils } from '../../support/utils/DashboardUtils';
-import { ConfigurationUtils } from '../../support/utils/configuration/ConfigutationUtils';
+import { ConfigurationUtils } from '../../support/utils/configuration/ConfigurationUtils';
 
 describe('Creates a new adapter, add to assets and export assets', () => {
     beforeEach('Setup Test', () => {
diff --git a/ui/cypress/tests/configuration/sites/sites-geo-features.spec.ts b/ui/cypress/tests/configuration/sites/sites-geo-features.spec.ts
new file mode 100644
index 0000000000..4f8649f8e0
--- /dev/null
+++ b/ui/cypress/tests/configuration/sites/sites-geo-features.spec.ts
@@ -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.
+ *
+ */
+
+import { SiteUtils } from '../../../support/utils/configuration/SiteUtils';
+import { ConfigurationUtils } from '../../../support/utils/configuration/ConfigurationUtils';
+
+describe('Test geo features settings', () => {
+    beforeEach('Setup Test', () => {
+        cy.initStreamPipesTest();
+    });
+
+    it('Perform Test', () => {
+        // enable geo features
+        ConfigurationUtils.goToSitesConfiguration();
+        SiteUtils.enableGeoFeatures('url');
+
+        ConfigurationUtils.goToGeneralConfiguration();
+        ConfigurationUtils.goToSitesConfiguration();
+
+        cy.dataCy(SiteUtils.CHECKBOX_ENABLE_LOCATION_FEATURES)
+            .find('input')
+            .should('be.checked');
+
+        cy.dataCy(SiteUtils.INPUT_TILE_SERVER_URL).should('have.value', 'url');
+    });
+});
diff --git a/ui/cypress/tests/configuration/sites/sites.spec.ts b/ui/cypress/tests/configuration/sites/sites.spec.ts
new file mode 100644
index 0000000000..5b0fb8b861
--- /dev/null
+++ b/ui/cypress/tests/configuration/sites/sites.spec.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { SiteUtils } from '../../../support/utils/configuration/SiteUtils';
+import { ConfigurationUtils } from '../../../support/utils/configuration/ConfigurationUtils';
+
+describe('Test configuration of sites', () => {
+    beforeEach('Setup Test', () => {
+        cy.initStreamPipesTest();
+    });
+
+    it('Perform Test', () => {
+        const site = 'My Site';
+        const newSite = 'My modified Site';
+        const areas = ['Area A', 'Area B'];
+        ConfigurationUtils.goToSitesConfiguration();
+        SiteUtils.createNewSite(site, areas);
+
+        cy.dataCy(SiteUtils.LABEL_TABLE_NAME).should('have.length', 1);
+
+        cy.dataCy(SiteUtils.LABEL_TABLE_NAME)
+            .first()
+            .should('contain.text', site);
+        cy.dataCy(SiteUtils.LABEL_TABLE_AREA)
+            .first()
+            .should('contains.text', areas.toString());
+
+        SiteUtils.openEditSiteDialog();
+
+        cy.dataCy(SiteUtils.INPUT_SITE_DIALOG_SITE_INPUT, { timeout: 2000 })
+            .clear()
+            .type(newSite);
+
+        cy.dataCy(SiteUtils.BUTTON_SITE_DIALOG_REMOVE_AREA + '_Area_A').click();
+        cy.dataCy(SiteUtils.BUTTON_SITE_DIALOG_SAVE).click();
+
+        cy.dataCy(SiteUtils.LABEL_TABLE_NAME).should('have.length', 1);
+
+        cy.dataCy(SiteUtils.LABEL_TABLE_NAME)
+            .first()
+            .should('contain.text', newSite);
+        cy.dataCy(SiteUtils.LABEL_TABLE_AREA)
+            .first()
+            .should('have.text', ' Area B ');
+
+        cy.dataCy(SiteUtils.BUTTON_DELETE_SITE + '-My_modified_Site').click();
+        cy.dataCy(SiteUtils.LABEL_TABLE_NAME).should('have.length', 0);
+    });
+});
diff --git a/ui/cypress/tests/connect/editAdapterValuesAndFields.spec.ts b/ui/cypress/tests/connect/editAdapterValuesAndFields.spec.ts
index 52269352a0..86800e448b 100644
--- a/ui/cypress/tests/connect/editAdapterValuesAndFields.spec.ts
+++ b/ui/cypress/tests/connect/editAdapterValuesAndFields.spec.ts
@@ -19,6 +19,7 @@
 import { ConnectUtils } from '../../support/utils/connect/ConnectUtils';
 import { ConnectBtns } from '../../support/utils/connect/ConnectBtns';
 import { AdapterBuilder } from '../../support/builder/AdapterBuilder';
+import { ConnectEventSchemaUtils } from '../../support/utils/connect/ConnectEventSchemaUtils';
 
 describe('Test Edit Adapter', () => {
     beforeEach('Setup Test', () => {
@@ -42,9 +43,7 @@ describe('Test Edit Adapter', () => {
         cy.dataCy('connect-add-field-name-button').click();
         cy.dataCy('edit-density').click();
         // Change runtime name
-        cy.dataCy('connect-edit-field-runtime-name')
-            .clear()
-            .type('test-density');
+        ConnectBtns.runtimeNameInput().clear().type('test-density');
         // Change field semantic type
         cy.get('[id="domainproperty"]')
             .clear()
@@ -75,10 +74,8 @@ describe('Test Edit Adapter', () => {
         ConnectBtns.editAdapter().click();
         cy.contains('Next').click();
         cy.dataCy('edit-density').click();
-        cy.dataCy('connect-edit-field-runtime-name').should(
-            'have.value',
-            'test-density',
-        );
+        ConnectEventSchemaUtils.validateRuntimeName('test-density');
+
         cy.get('[id="domainproperty"]').should(
             'have.value',
             'http://schema.org/Numbers',
@@ -91,7 +88,7 @@ describe('Test Edit Adapter', () => {
         );
 
         // Delete inserted values in edit field
-        cy.dataCy('connect-edit-field-runtime-name').clear();
+        ConnectBtns.runtimeNameInput().clear();
         cy.get('[id="domainproperty"]').clear();
         ConnectBtns.changeRuntimeType()
             .click()
diff --git a/ui/cypress/tests/connect/rules/schemaRules.smoke.spec.ts b/ui/cypress/tests/connect/rules/schemaRules.smoke.spec.ts
index 673cf31ebf..63968cc478 100644
--- a/ui/cypress/tests/connect/rules/schemaRules.smoke.spec.ts
+++ b/ui/cypress/tests/connect/rules/schemaRules.smoke.spec.ts
@@ -23,10 +23,10 @@ import { ConnectEventSchemaUtils } from '../../../support/utils/connect/ConnectE
 describe('Connect schema rule transformations', () => {
     beforeEach('Setup Test', () => {
         cy.initStreamPipesTest();
-        FileManagementUtils.addFile('connect/schemaRules/input.csv');
     });
 
-    it('Perform Test', () => {
+    it('Test several schema rules', () => {
+        FileManagementUtils.addFile('connect/schemaRules/input.csv');
         const adapterConfiguration =
             ConnectUtils.setUpPreprocessingRuleTest(true);
 
@@ -36,7 +36,10 @@ describe('Connect schema rule transformations', () => {
         // Delete one property
         ConnectEventSchemaUtils.deleteProperty('density');
 
-        // Set data type to float
+        // Rename property with special char
+        ConnectEventSchemaUtils.renameProperty('contains.dot', 'dot');
+
+        // Set data type to integer
         ConnectEventSchemaUtils.changePropertyDataType(
             'temperature',
             'Integer',