This is an automated email from the ASF dual-hosted git repository.

riemer pushed a commit to branch 3141-add-site-and-area-configuration-to-assets
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to 
refs/heads/3141-add-site-and-area-configuration-to-assets by this push:
     new 41bc47c533 feat(#3141): Add option to configure production sites
41bc47c533 is described below

commit 41bc47c53393e60371bd2636b50360b672e8c75c
Author: Dominik Riemer <[email protected]>
AuthorDate: Wed Aug 14 21:22:10 2024 +0200

    feat(#3141): Add option to configure production sites
---
 .../model/configuration/LocationConfig.java        |  11 ++-
 .../model/configuration/SpCoreConfiguration.java   |  10 ++
 .../apache/streampipes/rest/ResetManagement.java   |  42 +++++++--
 .../impl/admin/LocationConfigurationResource.java  |  53 +++++++++++
 ...ConfigutationUtils.ts => ConfigurationUtils.ts} |   8 ++
 .../support/utils/configuration/SiteUtils.ts       |  65 +++++++++++++
 .../tests/assetManagement/createAsset.spec.ts      |   2 +-
 .../sites/sites-geo-features.spec.ts}              |  35 ++++---
 ui/cypress/tests/configuration/sites/sites.spec.ts |  64 +++++++++++++
 .../src/lib/apis/datalake-rest.service.ts          |  32 +------
 .../src/lib/apis/location-config.service.ts}       |  48 +++++-----
 .../src/lib/model/assets/asset.model.ts            |  24 ++++-
 .../src/lib/model/gen/streampipes-model.ts         |  69 ++++----------
 .../platform-services/src/public-api.ts            |   1 +
 .../basic-field-description.component.html         |   3 +-
 ui/src/app/assets/assets.module.ts                 |   4 +
 .../asset-details-basics.component.html            |  15 +++
 .../asset-details-basics.component.ts              |  36 +++++--
 .../asset-details-site.component.html}             |  38 ++++----
 .../asset-details-site.component.scss              |   0
 .../asset-details-site.component.ts                |  62 ++++++++++++
 .../asset-location/asset-location.component.html   |   0
 .../asset-location/asset-location.component.scss   |   0
 .../asset-location/asset-location.component.ts}    |  11 ++-
 .../asset-details-panel.component.html             |   2 +
 .../asset-details-panel.component.ts               |   8 +-
 .../asset-details/asset-details.component.html     |   4 +-
 .../asset-details/asset-details.component.ts       |  45 ++++++---
 .../asset-selection-panel.component.html           |   4 +-
 .../asset-selection-panel.component.ts             |   8 +-
 ui/src/app/assets/constants/asset.constants.ts     |   1 +
 ui/src/app/configuration/configuration-tabs.ts     |   5 +
 ui/src/app/configuration/configuration.module.ts   |  18 ++++
 ui/src/app/configuration/configuration.routes.ts   |   1 -
 .../edit-location-area.component.html              |  68 +++++++++++++
 .../edit-location-area.component.scss}             |  15 ++-
 .../edit-location-area.component.ts}               |  26 ++---
 .../edit-location/edit-location.component.html     |  44 +++++++++
 .../edit-location/edit-location.component.scss     |   0
 .../edit-location/edit-location.component.ts}      |  14 ++-
 .../manage-site/manage-site-dialog.component.html  |  31 ++++++
 .../manage-site/manage-site-dialog.component.scss} |  21 ++++-
 .../manage-site/manage-site-dialog.component.ts    |  80 ++++++++++++++++
 .../location-features-configuration.component.html |  67 +++++++++++++
 .../location-features-configuration.component.ts   | 105 +++++++++++++++++++++
 .../site-area-configuration.component.html         |  80 ++++++++++++++++
 .../site-area-configuration.component.ts           |  83 ++++++++++++++++
 .../sites-configuration.component.html}            |  19 +---
 .../sites-configuration.component.scss             |   0
 .../sites-configuration.component.ts}              |  13 ++-
 50 files changed, 1161 insertions(+), 234 deletions(-)

diff --git a/ui/src/app/assets/constants/asset.constants.ts 
b/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/LocationConfig.java
similarity index 77%
copy from ui/src/app/assets/constants/asset.constants.ts
copy to 
streampipes-model/src/main/java/org/apache/streampipes/model/configuration/LocationConfig.java
index 9d99800ca1..fe7bc35c60 100644
--- a/ui/src/app/assets/constants/asset.constants.ts
+++ 
b/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/LocationConfig.java
@@ -16,7 +16,10 @@
  *
  */
 
-export class AssetConstants {
-    public static ASSET_APP_DOC_NAME = 'asset-management';
-    public static ASSET_LINK_TYPES_DOC_NAME = 'asset-link-type';
-}
+package org.apache.streampipes.model.configuration;
+
+import org.apache.streampipes.model.shared.annotation.TsModel;
+
+@TsModel
+public record LocationConfig(boolean locationEnabled,
+                             String tileServerUrl) {}
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..5c04e6b1c7 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/support/utils/configuration/ConfigutationUtils.ts 
b/ui/cypress/support/utils/configuration/ConfigurationUtils.ts
similarity index 82%
copy from ui/cypress/support/utils/configuration/ConfigutationUtils.ts
copy to ui/cypress/support/utils/configuration/ConfigurationUtils.ts
index 44731e6e4f..f8148033e5 100644
--- a/ui/cypress/support/utils/configuration/ConfigutationUtils.ts
+++ b/ui/cypress/support/utils/configuration/ConfigurationUtils.ts
@@ -20,4 +20,12 @@ 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/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/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.ts
 b/ui/cypress/tests/configuration/sites/sites-geo-features.spec.ts
similarity index 50%
copy from 
ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.ts
copy to ui/cypress/tests/configuration/sites/sites-geo-features.spec.ts
index e61b6d379b..4f8649f8e0 100644
--- 
a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.ts
+++ b/ui/cypress/tests/configuration/sites/sites-geo-features.spec.ts
@@ -16,21 +16,26 @@
  *
  */
 
-import { Component, EventEmitter, Input, Output } from '@angular/core';
-import { SpAsset } from '@streampipes/platform-services';
+import { SiteUtils } from '../../../support/utils/configuration/SiteUtils';
+import { ConfigurationUtils } from 
'../../../support/utils/configuration/ConfigurationUtils';
 
-@Component({
-    selector: 'sp-asset-details-panel-component',
-    templateUrl: './asset-details-panel.component.html',
-    styleUrls: ['./asset-details-panel.component.scss'],
-})
-export class SpAssetDetailsPanelComponent {
-    @Input()
-    asset: SpAsset;
+describe('Test geo features settings', () => {
+    beforeEach('Setup Test', () => {
+        cy.initStreamPipesTest();
+    });
 
-    @Input()
-    editMode: boolean;
+    it('Perform Test', () => {
+        // enable geo features
+        ConfigurationUtils.goToSitesConfiguration();
+        SiteUtils.enableGeoFeatures('url');
 
-    @Output()
-    updateAssetEmitter: EventEmitter<SpAsset> = new EventEmitter<SpAsset>();
-}
+        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/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/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-basics.component.ts
 
b/ui/projects/streampipes/platform-services/src/lib/apis/location-config.service.ts
similarity index 50%
copy from 
ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-basics.component.ts
copy to 
ui/projects/streampipes/platform-services/src/lib/apis/location-config.service.ts
index f84ef1fd3b..f69cf16f64 100644
--- 
a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-basics.component.ts
+++ 
b/ui/projects/streampipes/platform-services/src/lib/apis/location-config.service.ts
@@ -16,37 +16,35 @@
  *
  */
 
-import { Component, Input, OnInit } from '@angular/core';
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
 import {
-    Isa95TypeDesc,
-    Isa95TypeService,
-    SpAsset,
+    LocationConfig,
+    PlatformServicesCommons,
 } from '@streampipes/platform-services';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
 
-@Component({
-    selector: 'sp-asset-details-basics-component',
-    templateUrl: './asset-details-basics.component.html',
-    styleUrls: ['./asset-details-basics.component.scss'],
+@Injectable({
+    providedIn: 'root',
 })
-export class AssetDetailsBasicsComponent implements OnInit {
-    @Input()
-    asset: SpAsset;
+export class LocationConfigService {
+    constructor(
+        private http: HttpClient,
+        private platformServicesCommons: PlatformServicesCommons,
+    ) {}
 
-    @Input()
-    editMode: boolean;
-
-    isa95Types: Isa95TypeDesc[] = [];
+    getLocationConfig(): Observable<LocationConfig> {
+        return this.http
+            .get(this.locationConfigPath)
+            .pipe(map(response => response as LocationConfig));
+    }
 
-    constructor(private isa95TypeService: Isa95TypeService) {}
+    updateLocationConfig(config: LocationConfig): Observable<any> {
+        return this.http.put(this.locationConfigPath, config);
+    }
 
-    ngOnInit() {
-        this.asset.assetType ??= {
-            assetIcon: undefined,
-            assetIconColor: undefined,
-            assetTypeCategory: undefined,
-            assetTypeLabel: undefined,
-            isa95AssetType: 'OTHER',
-        };
-        this.isa95Types = this.isa95TypeService.getTypeDescriptions();
+    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..1f353ed9c2 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,35 @@ export interface Isa95TypeDesc {
     type: Isa95Type;
 }
 
+export interface AssetSiteDesc {
+    _id: string;
+    _rev?: string;
+    appDocType: string;
+    label: string;
+    location: LatLng;
+    areas: string[];
+}
+
+export interface LatLng {
+    latitude: number;
+    longitude: number;
+}
+
+export interface AssetLocation {
+    siteId: string;
+    area: string;
+    exactLocation?: LatLng;
+    zoom?: number;
+}
+
 export interface SpAsset {
     assetId: string;
     assetName: string;
     assetDescription: string;
-
     assetType: AssetType;
     labelIds?: string[];
     assetLinks: AssetLink[];
-
+    assetLocation?: AssetLocation;
     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..b48be6e884 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-14 
12:45:11.
 
 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,24 @@ export class ListOutputStrategy extends OutputStrategy {
     }
 }
 
+export class LocationConfig {
+    locationEnabled: boolean;
+    tileServerUrl: string;
+
+    static fromData(
+        data: LocationConfig,
+        target?: LocationConfig,
+    ): LocationConfig {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new LocationConfig();
+        instance.locationEnabled = data.locationEnabled;
+        instance.tileServerUrl = data.tileServerUrl;
+        return instance;
+    }
+}
+
 export class MappingProperty extends StaticProperty {
     '@class':
         | 'org.apache.streampipes.model.staticproperty.MappingProperty'
@@ -2430,25 +2447,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 +3052,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..9d427ea958 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
@@ -107,6 +107,21 @@
                 >
                 </sp-asset-details-labels-component>
             </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-component
+                    class="w-100"
+                    [asset]="asset"
+                    [locations]="locations"
+                    [editMode]="editMode"
+                >
+                </sp-asset-details-site-component>
+            </sp-basic-field-description>
         </div>
     </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..beb0405276 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,8 +16,15 @@
  *
  */
 
-import { Component, Input, OnInit } from '@angular/core';
 import {
+    Component,
+    Input,
+    OnChanges,
+    OnInit,
+    SimpleChanges,
+} from '@angular/core';
+import {
+    AssetSiteDesc,
     Isa95TypeDesc,
     Isa95TypeService,
     SpAsset,
@@ -28,25 +35,36 @@ import {
     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()
+    locations: 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-panel.component.html
 
b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-details-site.component.html
similarity index 51%
copy from 
ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.html
copy to 
ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-details-site.component.html
index 9b9157f43c..37fc98499a 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-basics/asset-details-site/asset-details-site.component.html
@@ -16,22 +16,24 @@
   ~
   -->
 
-<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
-                [asset]="asset"
-                [editMode]="editMode"
-            >
-            </sp-asset-details-basics-component>
-        </mat-tab>
-        <mat-tab label="Asset Links" data-cy="asset-tab-asset-links">
-            <sp-asset-details-links-component
-                [asset]="asset"
-                [editMode]="editMode"
-                (updateAssetEmitter)="updateAssetEmitter.emit($event)"
-            >
-            </sp-asset-details-links-component>
-        </mat-tab>
-    </mat-tab-group>
+<div fxLayout="column">
+    <mat-form-field color="accent" class="w-100">
+        <mat-label>Site</mat-label>
+        <mat-select
+            [(ngModel)]="asset.assetLocation.siteId"
+            (selectionChange)="handleLocationChange($event)"
+        >
+            <mat-option *ngFor="let site of allSites" [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 [(ngModel)]="asset.assetLocation.area">
+            <mat-option *ngFor="let area of currentSite.areas" [value]="area">
+                {{ area }}
+            </mat-option>
+        </mat-select>
+    </mat-form-field>
 </div>
diff --git 
a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-details-site.component.scss
 
b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-details-site.component.scss
new file mode 100644
index 0000000000..e69de29bb2
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..085bbb2d42
--- /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,62 @@
+/*
+ * 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-component',
+    templateUrl: './asset-details-site.component.html',
+})
+export class AssetDetailsSiteComponent implements OnChanges {
+    @Input()
+    asset: SpAsset;
+
+    @Input()
+    editMode: boolean;
+
+    @Input()
+    allSites: AssetSiteDesc[];
+
+    currentSite: AssetSiteDesc;
+
+    constructor() {}
+
+    ngOnChanges(changes: SimpleChanges) {
+        if (changes['asset']) {
+            this.asset.assetLocation ??= {
+                area: undefined,
+                siteId: undefined,
+            };
+            if (this.allSites.length > 0) {
+                if (this.asset.assetLocation.siteId !== undefined) {
+                    this.selectCurrentSite(this.asset.assetLocation.siteId);
+                }
+            }
+        }
+    }
+
+    handleLocationChange(event: MatSelectChange) {
+        this.selectCurrentSite(event.value);
+    }
+
+    selectCurrentSite(siteId: string): void {
+        this.currentSite = this.allSites.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..e69de29bb2
diff --git 
a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-location/asset-location.component.scss
 
b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-location/asset-location.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/ui/src/app/assets/constants/asset.constants.ts 
b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-location/asset-location.component.ts
similarity index 80%
copy from ui/src/app/assets/constants/asset.constants.ts
copy to 
ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-location/asset-location.component.ts
index 9d99800ca1..51e0050bae 100644
--- a/ui/src/app/assets/constants/asset.constants.ts
+++ 
b/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-basics/asset-details-site/asset-location/asset-location.component.ts
@@ -16,7 +16,10 @@
  *
  */
 
-export class AssetConstants {
-    public static ASSET_APP_DOC_NAME = 'asset-management';
-    public static ASSET_LINK_TYPES_DOC_NAME = 'asset-link-type';
-}
+import { Component } from '@angular/core';
+
+@Component({
+    selector: 'sp-asset-location-component',
+    templateUrl: './asset-location.component.html',
+})
+export class AssetLocationComponent {}
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..589babcbfb 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
@@ -21,6 +21,8 @@
         <mat-tab label="Basics" data-cy="asset-tab-basic">
             <sp-asset-details-basics-component
                 [asset]="asset"
+                [locations]="locations"
+                [rootNode]="rootNode"
                 [editMode]="editMode"
             >
             </sp-asset-details-basics-component>
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..e25a3c1afe 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,7 +17,7 @@
  */
 
 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',
@@ -31,6 +31,12 @@ export class SpAssetDetailsPanelComponent {
     @Input()
     editMode: boolean;
 
+    @Input()
+    rootNode: boolean;
+
+    @Input()
+    locations: 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..02d4ab506a 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
@@ -55,7 +55,7 @@
                         [editMode]="editMode"
                         [assetModel]="asset"
                         [selectedAsset]="selectedAsset"
-                        (selectedAssetEmitter)="selectedAsset = $event"
+                        (selectedAssetEmitter)="applySelectedAsset($event)"
                     >
                     </sp-asset-selection-panel-component>
                 </div>
@@ -64,6 +64,8 @@
                 <sp-asset-details-panel-component
                     *ngIf="selectedAsset"
                     [asset]="selectedAsset"
+                    [locations]="locations"
+                    [rootNode]="rootNode"
                     [editMode]="editMode"
                     (updateAssetEmitter)="updateAsset()"
                     fxFlex="100"
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..b0c8a1682d 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,11 +21,13 @@ 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',
@@ -34,8 +36,10 @@ import { SpAssetRoutes } from '../../assets.routes';
 })
 export class SpAssetDetailsComponent implements OnInit {
     asset: SpAssetModel;
+    locations: 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.locations = 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..14fe8d1511 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,
@@ -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/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 83%
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..ccbab50832 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;
+    border: 1px solid var(--color-bg-2);
 }
diff --git 
a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.ts
 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.ts
similarity index 59%
copy from 
ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.ts
copy to 
ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.ts
index e61b6d379b..ddd7baab40 100644
--- 
a/ui/src/app/assets/components/asset-details/asset-details-panel/asset-details-panel.component.ts
+++ 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.ts
@@ -16,21 +16,25 @@
  *
  */
 
-import { Component, EventEmitter, Input, Output } from '@angular/core';
-import { SpAsset } from '@streampipes/platform-services';
+import { Component, Input } from '@angular/core';
+import { AssetSiteDesc } from '@streampipes/platform-services';
 
 @Component({
-    selector: 'sp-asset-details-panel-component',
-    templateUrl: './asset-details-panel.component.html',
-    styleUrls: ['./asset-details-panel.component.scss'],
+    selector: 'sp-edit-asset-location-area-component',
+    templateUrl: './edit-location-area.component.html',
+    styleUrls: ['./edit-location-area.component.scss'],
 })
-export class SpAssetDetailsPanelComponent {
+export class EditAssetLocationAreaComponent {
     @Input()
-    asset: SpAsset;
+    site: AssetSiteDesc;
 
-    @Input()
-    editMode: boolean;
+    newArea: string = '';
+
+    addNewArea(): void {
+        this.site.areas.push(this.newArea);
+    }
 
-    @Output()
-    updateAssetEmitter: EventEmitter<SpAsset> = new EventEmitter<SpAsset>();
+    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..c4e7a5f9cb
--- /dev/null
+++ 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.html
@@ -0,0 +1,44 @@
+<!--
+  ~ 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>
+</div>
diff --git 
a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.scss
 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/ui/src/app/assets/constants/asset.constants.ts 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.ts
similarity index 69%
copy from ui/src/app/assets/constants/asset.constants.ts
copy to 
ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.ts
index 9d99800ca1..750eabd46b 100644
--- a/ui/src/app/assets/constants/asset.constants.ts
+++ 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.ts
@@ -16,7 +16,15 @@
  *
  */
 
-export class AssetConstants {
-    public static ASSET_APP_DOC_NAME = 'asset-management';
-    public static ASSET_LINK_TYPES_DOC_NAME = 'asset-link-type';
+import { Component, Input } from '@angular/core';
+import { AssetSiteDesc } from '@streampipes/platform-services';
+
+@Component({
+    selector: 'sp-edit-asset-location-component',
+    templateUrl: './edit-location.component.html',
+    styleUrls: ['./edit-location.component.scss'],
+})
+export class EditAssetLocationComponent {
+    @Input()
+    site: AssetSiteDesc;
 }
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..2fc0c2e110
--- /dev/null
+++ 
b/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.html
@@ -0,0 +1,31 @@
+<div class="sp-dialog-container">
+    <div class="sp-dialog-content p-15">
+        <div *ngIf="clonedSite !== undefined">
+            <sp-edit-asset-location-component [site]="clonedSite">
+            </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/assets/constants/asset.constants.ts 
b/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.scss
similarity index 72%
copy from ui/src/app/assets/constants/asset.constants.ts
copy to 
ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.scss
index 9d99800ca1..18577c0d5e 100644
--- a/ui/src/app/assets/constants/asset.constants.ts
+++ 
b/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.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,7 +16,20 @@
  *
  */
 
-export class AssetConstants {
-    public static ASSET_APP_DOC_NAME = 'asset-management';
-    public static ASSET_LINK_TYPES_DOC_NAME = 'asset-link-type';
+.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..c15d8e01cf
--- /dev/null
+++ 
b/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.ts
@@ -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.
+ *
+ */
+
+import { Component, Input, OnInit } from '@angular/core';
+import { DialogRef } from '@streampipes/shared-ui';
+import {
+    AssetSiteDesc,
+    GenericStorageService,
+} 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;
+
+    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: { latitude: 0, longitude: 0 },
+            areas: [],
+        };
+        this.createMode = true;
+    }
+
+    store(): void {
+        console.log(this.clonedSite);
+        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..1ea40f2869
--- /dev/null
+++ 
b/ui/src/app/configuration/sites-configuration/location-features-configuration/location-features-configuration.component.html
@@ -0,0 +1,67 @@
+<!--
+  ~ 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]="parentForm"
+        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="showTileUrlInput" class="mt-10">
+                <div class="subsection-title">
+                    Tile server URL (use placeholders for x, y
+                </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>
+        </sp-split-section>
+        <sp-split-section>
+            <div>
+                <button
+                    mat-raised-button
+                    color="accent"
+                    data-cy="sites-location-features-button"
+                    [disabled]="!parentForm.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..7acfaeb750
--- /dev/null
+++ 
b/ui/src/app/configuration/sites-configuration/location-features-configuration/location-features-configuration.component.ts
@@ -0,0 +1,105 @@
+/*
+ * 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, 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
+{
+    locationConfig: LocationConfig;
+
+    parentForm: UntypedFormGroup;
+    formSubscription: Subscription;
+    showTileUrlInput = false;
+
+    constructor(
+        private fb: UntypedFormBuilder,
+        private snackBar: MatSnackBar,
+        private locationConfigService: LocationConfigService,
+    ) {}
+
+    ngOnInit(): void {
+        this.parentForm = this.fb.group({});
+        this.locationConfigService.getLocationConfig().subscribe(res => {
+            this.locationConfig = res;
+            this.showTileUrlInput = res.locationEnabled;
+            this.parentForm.addControl(
+                'locationFeaturesEnabled',
+                new UntypedFormControl(this.locationConfig.locationEnabled),
+            );
+            this.parentForm.addControl(
+                'tileServerUrl',
+                new UntypedFormControl(
+                    this.locationConfig.tileServerUrl,
+                    this.showTileUrlInput ? Validators.required : [],
+                ),
+            );
+            this.formSubscription = this.parentForm
+                .get('locationFeaturesEnabled')
+                .valueChanges.subscribe(checked => {
+                    this.showTileUrlInput = checked;
+                    if (checked) {
+                        this.parentForm.controls.tileServerUrl.setValidators(
+                            Validators.required,
+                        );
+                    } else {
+                        this.parentForm.controls.tileServerUrl.setValidators(
+                            [],
+                        );
+                    }
+                });
+        });
+    }
+
+    save(): void {
+        this.locationConfig.locationEnabled = this.parentForm.get(
+            'locationFeaturesEnabled',
+        ).value;
+        if (this.locationConfig.locationEnabled) {
+            this.locationConfig.tileServerUrl =
+                this.parentForm.get('tileServerUrl').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..fdecc2259f
--- /dev/null
+++ 
b/ui/src/app/configuration/sites-configuration/site-area-configuration/site-area-configuration.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, OnInit } from '@angular/core';
+import {
+    AssetSiteDesc,
+    GenericStorageService,
+} 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 {
+    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,
+            },
+        });
+
+        dialogRef.afterClosed().subscribe(reload => {
+            if (reload) {
+                this.loadSites();
+            }
+        });
+    }
+}
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/basic-field-description/basic-field-description.component.html
 
b/ui/src/app/configuration/sites-configuration/sites-configuration.component.html
similarity index 66%
copy from 
ui/projects/streampipes/shared-ui/src/lib/components/basic-field-description/basic-field-description.component.html
copy to 
ui/src/app/configuration/sites-configuration/sites-configuration.component.html
index 83115501f2..05a204d305 100644
--- 
a/ui/projects/streampipes/shared-ui/src/lib/components/basic-field-description/basic-field-description.component.html
+++ 
b/ui/src/app/configuration/sites-configuration/sites-configuration.component.html
@@ -16,17 +16,8 @@
   ~
   -->
 
-<div
-    fxFlex="100"
-    fxLayout="row"
-    fxLayoutAlign="start center"
-    class="field-description-outer"
->
-    <div [fxFlex]="descriptionPanelWidth" fxLayout="column">
-        <div class="field-description-label">{{ label }}</div>
-        <div class="field-description">{{ description }}</div>
-    </div>
-    <div fxFlex fxLayoutAlign="start center">
-        <ng-content></ng-content>
-    </div>
-</div>
+<sp-basic-nav-tabs [spNavigationItems]="tabs" [activeLink]="'sites'">
+    <sp-location-features-configuration> </sp-location-features-configuration>
+
+    <sp-site-area-configuration> </sp-site-area-configuration>
+</sp-basic-nav-tabs>
diff --git 
a/ui/src/app/configuration/sites-configuration/sites-configuration.component.scss
 
b/ui/src/app/configuration/sites-configuration/sites-configuration.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/ui/src/app/assets/constants/asset.constants.ts 
b/ui/src/app/configuration/sites-configuration/sites-configuration.component.ts
similarity index 69%
copy from ui/src/app/assets/constants/asset.constants.ts
copy to 
ui/src/app/configuration/sites-configuration/sites-configuration.component.ts
index 9d99800ca1..7254d234e1 100644
--- a/ui/src/app/assets/constants/asset.constants.ts
+++ 
b/ui/src/app/configuration/sites-configuration/sites-configuration.component.ts
@@ -16,7 +16,14 @@
  *
  */
 
-export class AssetConstants {
-    public static ASSET_APP_DOC_NAME = 'asset-management';
-    public static ASSET_LINK_TYPES_DOC_NAME = 'asset-link-type';
+import { Component } from '@angular/core';
+import { SpConfigurationTabs } from '../configuration-tabs';
+
+@Component({
+    selector: 'sp-sites-configuration',
+    templateUrl: './sites-configuration.component.html',
+    styleUrls: ['./sites-configuration.component.scss'],
+})
+export class SitesConfigurationComponent {
+    tabs = SpConfigurationTabs.getTabs();
 }


Reply via email to