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

dominikriemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new 0141c1bfc4 feat(#3968): Assets Do Not Consider Dependencies on Labels 
and Sites (#4559)
0141c1bfc4 is described below

commit 0141c1bfc4a5288442af0f320b1f616b25ef726f
Author: Jacqueline Höllig <[email protected]>
AuthorDate: Fri Jun 19 16:02:35 2026 +0200

    feat(#3968): Assets Do Not Consider Dependencies on Labels and Sites (#4559)
    
    Co-authored-by: Dominik Riemer <[email protected]>
---
 .../export/dataimport/ImportGenerator.java         | 25 +++++-
 .../export/dataimport/PerformImportGenerator.java  | 14 ++++
 .../export/dataimport/PreviewImportGenerator.java  | 22 ++++-
 .../export/generator/ExportPackageGenerator.java   |  8 ++
 .../model/export/AssetExportConfiguration.java     | 28 +++++++
 ui/deployment/i18n/de.json                         | 13 ++-
 ui/deployment/i18n/en.json                         |  7 ++
 ui/deployment/i18n/pl.json                         | 11 ++-
 .../src/lib/model/gen/streampipes-model.ts         |  4 +
 .../export/data-export-import.component.ts         | 96 +++++++++++++++++++++-
 .../data-export-dialog.component.html              | 20 ++++-
 .../export-dialog/data-export-dialog.component.ts  | 41 ++++++++-
 .../data-import-dialog.component.html              |  8 ++
 .../import-dialog/data-import-dialog.component.ts  |  4 +-
 .../label-configuration.component.html             |  1 -
 .../label-configuration.component.ts               | 55 ++++++++++++-
 .../site-area-configuration.component.html         |  1 -
 .../site-area-configuration.component.ts           | 72 +++++++++++++++-
 18 files changed, 411 insertions(+), 19 deletions(-)

diff --git 
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/ImportGenerator.java
 
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/ImportGenerator.java
index 71025092e5..d2e582aaef 100644
--- 
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/ImportGenerator.java
+++ 
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/ImportGenerator.java
@@ -24,6 +24,7 @@ import 
org.apache.streampipes.model.export.StreamPipesApplicationPackage;
 import org.apache.streampipes.serializers.json.JacksonSerializer;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
@@ -39,6 +40,9 @@ import java.util.Map;
 public abstract class ImportGenerator<T> {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(ImportGenerator.class);
+  private static final String LABELS_APP_DOC_TYPE = "sp-labels";
+  private static final String SITES_APP_DOC_TYPE = "asset-sites";
+  private static final String APP_DOC_TYPE_FIELD = "appDocType";
 
   protected ObjectMapper defaultMapper;
 
@@ -120,7 +124,7 @@ public abstract class ImportGenerator<T> {
 
     for (String documentId : manifest.getGenericStorageDocuments()) {
       try {
-        handleGenericStorageDocument(asString(previewFiles.get(documentId)), 
documentId);
+        
handleGenericStorageDocumentByType(asString(previewFiles.get(documentId)), 
documentId);
       } catch (DocumentConflictException e) {
         LOG.warn("Skipping import of generic storage doc {} (already present 
with the same id)", documentId);
       }
@@ -158,6 +162,12 @@ public abstract class ImportGenerator<T> {
   protected abstract void handleFile(String document, String fileMetadataId, 
Map<String, byte[]> zipContent)
       throws IOException;
 
+  protected abstract void handleLabel(String document, String labelId)
+      throws JsonProcessingException;
+
+  protected abstract void handleSite(String document, String siteId)
+      throws JsonProcessingException;
+
   protected abstract void handleGenericStorageDocument(String document, String 
dataLakeMeasureId)
       throws JsonProcessingException;
 
@@ -165,4 +175,17 @@ public abstract class ImportGenerator<T> {
 
   protected abstract void afterResourcesCreated();
 
+  private void handleGenericStorageDocumentByType(String document, String 
documentId) throws JsonProcessingException {
+    Map<String, Object> genericStorageDocument = 
this.defaultMapper.readValue(document, new TypeReference<>() {
+    });
+    var appDocType = genericStorageDocument.get(APP_DOC_TYPE_FIELD);
+
+    if (LABELS_APP_DOC_TYPE.equals(appDocType)) {
+      handleLabel(document, documentId);
+    } else if (SITES_APP_DOC_TYPE.equals(appDocType)) {
+      handleSite(document, documentId);
+    } else {
+      handleGenericStorageDocument(document, documentId);
+    }
+  }
 }
diff --git 
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PerformImportGenerator.java
 
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PerformImportGenerator.java
index 0e4532deec..54dcd448cc 100644
--- 
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PerformImportGenerator.java
+++ 
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PerformImportGenerator.java
@@ -145,6 +145,20 @@ public class PerformImportGenerator extends 
ImportGenerator<Void> {
     new FileHandler().storeFile(fileMetadata.getFilename(), new 
ByteArrayInputStream(file));
   }
 
+  @Override
+  protected void handleLabel(String document, String labelId) throws 
JsonProcessingException {
+    if (shouldStore(labelId, config.getLabels())) {
+      writeDocument(document, new GenericStorageDocumentResolver());
+    }
+  }
+
+  @Override
+  protected void handleSite(String document, String siteId) throws 
JsonProcessingException {
+    if (shouldStore(siteId, config.getSites())) {
+      writeDocument(document, new GenericStorageDocumentResolver());
+    }
+  }
+
   @Override
   protected void handleGenericStorageDocument(String document, String 
documentId) throws JsonProcessingException {
     if (shouldStore(documentId, config.getGenericStorageDocuments())) {
diff --git 
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PreviewImportGenerator.java
 
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PreviewImportGenerator.java
index de8a0c976c..fe818cb59c 100644
--- 
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PreviewImportGenerator.java
+++ 
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PreviewImportGenerator.java
@@ -41,6 +41,8 @@ import java.util.function.Consumer;
 public class PreviewImportGenerator extends 
ImportGenerator<AssetExportConfiguration> {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(PreviewImportGenerator.class);
+  private static final String LABEL_FIELD = "label";
+
   private final AssetExportConfiguration importConfig;
   private final ExtensionServiceRequestManager extensionServiceRequestManager;
 
@@ -121,7 +123,19 @@ public class PreviewImportGenerator extends 
ImportGenerator<AssetExportConfigura
   }
 
   @Override
-  protected void handleGenericStorageDocument(String document, String 
genericDocId) throws JsonProcessingException {
+  protected void handleLabel(String document, String labelId) throws 
JsonProcessingException {
+    addExportItem(labelId, getGenericStorageDocumentLabel(document, labelId),
+        importConfig::addLabel);
+  }
+
+  @Override
+  protected void handleSite(String document, String siteId) throws 
JsonProcessingException {
+    addExportItem(siteId, getGenericStorageDocumentLabel(document, siteId),
+        importConfig::addSite);
+  }
+
+  @Override
+  protected void handleGenericStorageDocument(String document, String 
genericDocId) {
     addExportItem(genericDocId, genericDocId, 
importConfig::addGenericStorageDocument);
   }
 
@@ -133,4 +147,10 @@ public class PreviewImportGenerator extends 
ImportGenerator<AssetExportConfigura
   @Override
   protected void afterResourcesCreated() {
   }
+
+  private String getGenericStorageDocumentLabel(String document, String 
genericDocId) throws JsonProcessingException {
+    Map<String, Object> genericStorageDocument = 
this.defaultMapper.readValue(document, new TypeReference<>() {
+    });
+    return String.valueOf(genericStorageDocument.getOrDefault(LABEL_FIELD, 
genericDocId));
+  }
 }
diff --git 
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/generator/ExportPackageGenerator.java
 
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/generator/ExportPackageGenerator.java
index 1bc2829977..9637e204d1 100644
--- 
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/generator/ExportPackageGenerator.java
+++ 
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/generator/ExportPackageGenerator.java
@@ -122,6 +122,14 @@ public class ExportPackageGenerator {
         addDoc(builder, item, new GenericStorageDocumentResolver(), 
manifest::addGenericStorageDocument);
       });
 
+      config.getLabels().forEach(item -> {
+        addDoc(builder, item, new GenericStorageDocumentResolver(), 
manifest::addGenericStorageDocument);
+      });
+
+      config.getSites().forEach(item -> {
+        addDoc(builder, item, new GenericStorageDocumentResolver(), 
manifest::addGenericStorageDocument);
+      });
+
       config.getFiles().forEach(item -> {
         if (item.isSelected()) {
           var fileResolver = new FileResolver();
diff --git 
a/streampipes-model/src/main/java/org/apache/streampipes/model/export/AssetExportConfiguration.java
 
b/streampipes-model/src/main/java/org/apache/streampipes/model/export/AssetExportConfiguration.java
index 84c6ad3f46..2ac67ab67f 100644
--- 
a/streampipes-model/src/main/java/org/apache/streampipes/model/export/AssetExportConfiguration.java
+++ 
b/streampipes-model/src/main/java/org/apache/streampipes/model/export/AssetExportConfiguration.java
@@ -34,6 +34,8 @@ public class AssetExportConfiguration {
   private Set<ExportItem> dataSources;
   private Set<ExportItem> pipelines;
   private Set<ExportItem> files;
+  private Set<ExportItem> labels;
+  private Set<ExportItem> sites;
   private Set<ExportItem> genericStorageDocuments;
 
   private boolean overrideBrokerSettings;
@@ -48,6 +50,8 @@ public class AssetExportConfiguration {
     this.pipelines = new HashSet<>();
     this.files = new HashSet<>();
     this.assets = new HashSet<>();
+    this.labels = new HashSet<>();
+    this.sites = new HashSet<>();
     this.genericStorageDocuments = new HashSet<>();
   }
 
@@ -143,6 +147,30 @@ public class AssetExportConfiguration {
     this.files.add(item);
   }
 
+  public Set<ExportItem> getLabels() {
+    return labels;
+  }
+
+  public void setLabels(Set<ExportItem> labels) {
+    this.labels = labels;
+  }
+
+  public void addLabel(ExportItem label) {
+    this.labels.add(label);
+  }
+
+  public Set<ExportItem> getSites() {
+    return sites;
+  }
+
+  public void setSites(Set<ExportItem> sites) {
+    this.sites = sites;
+  }
+
+  public void addSite(ExportItem site) {
+    this.sites.add(site);
+  }
+
   public String getAssetName() {
     return assetName;
   }
diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index 684f06474f..dfc76c4696 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -109,7 +109,9 @@
   "Are you sure you want to delete this account?": "Sind Sie sicher, dass Sie 
dieses Konto löschen möchten?",
   "Are you sure you want to delete this asset?": "Möchten Sie dieses Asset 
wirklich löschen?",
   "Are you sure you want to delete this group?": "Sind Sie sicher, dass Sie 
diese Gruppe löschen wollen?",
+  "Are you sure you want to delete this label?": "Möchten Sie dieses Label 
wirklich löschen?",
   "Are you sure you want to delete this role?": "Sind Sie sicher, dass Sie 
diese Rolle löschen wollen?",
+  "Are you sure you want to delete this site?": "Sind Sie sicher, dass Sie 
diese Standort löschen wollen?",
   "Area": "Bereich",
   "Areas": "Bereiche",
   "Asset": "Asset",
@@ -583,6 +585,7 @@
   "Label Line": "Beschriftungslinie",
   "Label Position": "Beschriftungsposition",
   "Label font size": "Schriftgröße der Bezeichnung",
+  "Label is still in use": "Das Label wird weiterhin verwendet",
   "Label shown below the current gauge value.": "Beschriftung, die unter dem 
aktuellen Tacho-Wert angezeigt wird.",
   "Labels": "Labels",
   "Labels & custom fields": "Label und benutzerdefinierte Felder",
@@ -837,6 +840,7 @@
   "Re-enter mail address": "E-Mail-Adresse erneut eingeben",
   "Recipient for test mail": "Empfänger für Test-Mail",
   "Reduce event rate": "Datenrate reduzieren",
+  "Referenced resources": "Verwendete Ressourcen",
   "Refresh": "Neu laden",
   "Refresh Fields": "Felder aktualisieren",
   "Refresh adapters": "Adapter neu laden",
@@ -881,7 +885,7 @@
   "Restore password": "Passwort wiederherstellen",
   "Restrict to service tags": "Auf Service-Tags beschränken",
   "Result": "Ergebnis",
-  "Result Labels": "Ergebnisbeschriftungen",
+  "Result Labels": "Ergebnisbezeichnungen",
   "Retention": "Aufbewahrung",
   "Retention Log": "Speicherprotokoll",
   "Right": "Rechts",
@@ -1004,6 +1008,7 @@
   "Simple filters below are kept for fallback, but the advanced filter is 
currently used for queries.": "Die einfachen Filter unten bleiben als 
Rückfalloption erhalten, aber derzeit wird der erweiterte Filter für Abfragen 
verwendet.",
   "Single": "Einzeln",
   "Site": "Standort",
+  "Site is still in use": "Der Standort wird genutzt",
   "Site name is required": "Standortname erforderlich",
   "Sites": "Standorte",
   "Sites & Areas": "Standorte & Bereiche",
@@ -1136,8 +1141,10 @@
   "Title": "Titel",
   "Title must not be empty": "Titel darf nicht leer sein!",
   "To": "Bis",
-  "To delete a label, please remove the label from all assets": "Um ein Label 
zu löschen, entfernen Sie das Label bitte von allen Assets",
-  "To delete a site, please remove the site from all assets": "Um einen 
Standort zu löschen, entfernen Sie den Standort bitte von allen Assets",
+  "To delete a label, please remove the label from all assets": "Um ein Label 
zu löschen, entfernen Sie es bitte aus allen Assets",
+  "To delete a label, please remove the label from all assets.": "Um ein Label 
zu löschen, entfernen Sie es bitte aus allen Assets.",
+  "To delete a site, please remove the site from all assets": "Um einen 
Standort zu löschen, entfernen Sie diese bitte aus allen Assets",
+  "To delete a site, please remove the site from all assets.": "Um einen 
Standort zu löschen, entfernen Sie den Standort bitte von allen Assets.",
   "To enable the map view, a map provider needs to be configured. Admins can 
configure map providers under Settings -> Sites.": "Um die Kartenansicht zu 
aktivieren, ist ein konfigurierter Kartenanbieter erforderlich. Administratoren 
können Kartenanbieter unter Einstellungen → Standorte verwalten.",
   "Tooltip": "Tooltip",
   "Tooltip Content": "Tooltip-Inhalt",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index e31a855d66..8cc64c0bb6 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -109,7 +109,9 @@
   "Are you sure you want to delete this account?": null,
   "Are you sure you want to delete this asset?": null,
   "Are you sure you want to delete this group?": null,
+  "Are you sure you want to delete this label?": null,
   "Are you sure you want to delete this role?": null,
+  "Are you sure you want to delete this site?": null,
   "Area": null,
   "Areas": null,
   "Asset": null,
@@ -583,6 +585,7 @@
   "Label Line": null,
   "Label Position": null,
   "Label font size": null,
+  "Label is still in use": null,
   "Label shown below the current gauge value.": null,
   "Labels": null,
   "Labels & custom fields": null,
@@ -837,6 +840,7 @@
   "Re-enter mail address": null,
   "Recipient for test mail": null,
   "Reduce event rate": null,
+  "Referenced resources": null,
   "Refresh": null,
   "Refresh Fields": null,
   "Refresh adapters": null,
@@ -1004,6 +1008,7 @@
   "Simple filters below are kept for fallback, but the advanced filter is 
currently used for queries.": null,
   "Single": null,
   "Site": null,
+  "Site is still in use": null,
   "Site name is required": null,
   "Sites": null,
   "Sites & Areas": null,
@@ -1137,7 +1142,9 @@
   "Title must not be empty": null,
   "To": null,
   "To delete a label, please remove the label from all assets": null,
+  "To delete a label, please remove the label from all assets.": null,
   "To delete a site, please remove the site from all assets": null,
+  "To delete a site, please remove the site from all assets.": null,
   "To enable the map view, a map provider needs to be configured. Admins can 
configure map providers under Settings -> Sites.": null,
   "Tooltip": null,
   "Tooltip Content": null,
diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json
index e86a27129f..eba340d80e 100644
--- a/ui/deployment/i18n/pl.json
+++ b/ui/deployment/i18n/pl.json
@@ -109,7 +109,9 @@
   "Are you sure you want to delete this account?": "Czy na pewno chcesz usunąć 
to konto?",
   "Are you sure you want to delete this asset?": "Czy na pewno chcesz usunąć 
ten zasób?",
   "Are you sure you want to delete this group?": "Czy na pewno chcesz usunąć 
tę grupę?",
+  "Are you sure you want to delete this label?": "Czy na pewno chcesz usunąć 
tę etykietę?",
   "Are you sure you want to delete this role?": "Czy na pewno chcesz usunąć tę 
rolę?",
+  "Are you sure you want to delete this site?": "Czy na pewno chcesz usunąć tę 
stronę?",
   "Area": "Obszar",
   "Areas": "Obszary",
   "Asset": "Zasób",
@@ -583,6 +585,7 @@
   "Label Line": "Linia etykiety",
   "Label Position": "Pozycja etykiety",
   "Label font size": "Rozmiar czcionki etykiety",
+  "Label is still in use": "Etykieta jest nadal w użyciu",
   "Label shown below the current gauge value.": "Etykieta wyświetlana poniżej 
bieżącej wartości wskaźnika.",
   "Labels": "Etykiety",
   "Labels & custom fields": "Etykiety i pola niestandardowe",
@@ -837,6 +840,7 @@
   "Re-enter mail address": "Wprowadź ponownie adres e-mail",
   "Recipient for test mail": "Odbiorca wiadomości testowej",
   "Reduce event rate": "Zredukuj częstość zdarzeń",
+  "Referenced resources": "Wykaz źródeł",
   "Refresh": "Odśwież",
   "Refresh Fields": "Odśwież pola",
   "Refresh adapters": "Odśwież adaptery",
@@ -1004,6 +1008,7 @@
   "Simple filters below are kept for fallback, but the advanced filter is 
currently used for queries.": "Poniższe proste filtry są zachowane jako opcja 
zapasowa, ale obecnie do zapytań używany jest filtr zaawansowany.",
   "Single": "Pojedynczy",
   "Site": "Lokalizacja",
+  "Site is still in use": "Strona jest nadal aktywna",
   "Site name is required": "Nazwa lokalizacji jest wymagana",
   "Sites": "Lokalizacje",
   "Sites & Areas": "Lokalizacje i obszary",
@@ -1136,8 +1141,10 @@
   "Title": "Tytuł",
   "Title must not be empty": "Tytuł nie może być pusty",
   "To": "Do",
-  "To delete a label, please remove the label from all assets": "Aby usunąć 
etykietę, usuń ją ze wszystkich zasobów",
-  "To delete a site, please remove the site from all assets": "Aby usunąć 
lokalizację, usuń ją ze wszystkich zasobów",
+  "To delete a label, please remove the label from all assets": "Aby usunąć 
etykietę, należy ją usunąć ze wszystkich zasobów",
+  "To delete a label, please remove the label from all assets.": "Aby usunąć 
etykietę, należy ją usunąć ze wszystkich zasobów.",
+  "To delete a site, please remove the site from all assets": "Aby usunąć 
witrynę, należy usunąć ją ze wszystkich zasobów",
+  "To delete a site, please remove the site from all assets.": "Aby usunąć 
witrynę, należy ją usunąć ze wszystkich zasobów.",
   "To enable the map view, a map provider needs to be configured. Admins can 
configure map providers under Settings -> Sites.": "Aby włączyć widok mapy, 
należy skonfigurować dostawcę map. Administratorzy mogą to zrobić w Ustawienia 
-> Lokalizacje.",
   "Tooltip": "Podpowiedź",
   "Tooltip Content": "Zawartość podpowiedzi",
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 b2ae2c4487..ba62219b0e 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
@@ -572,9 +572,11 @@ export class AssetExportConfiguration {
     dataViews: ExportItem[];
     files: ExportItem[];
     genericStorageDocuments: ExportItem[];
+    labels: ExportItem[];
     overrideBrokerSettings: boolean;
     overwriteExistingDocuments: boolean;
     pipelines: ExportItem[];
+    sites: ExportItem[];
 
     static fromData(
         data: AssetExportConfiguration,
@@ -606,11 +608,13 @@ export class AssetExportConfiguration {
         instance.genericStorageDocuments = __getCopyArrayFn(
             ExportItem.fromData,
         )(data.genericStorageDocuments);
+        instance.labels = __getCopyArrayFn(ExportItem.fromData)(data.labels);
         instance.overrideBrokerSettings = data.overrideBrokerSettings;
         instance.overwriteExistingDocuments = data.overwriteExistingDocuments;
         instance.pipelines = __getCopyArrayFn(ExportItem.fromData)(
             data.pipelines,
         );
+        instance.sites = __getCopyArrayFn(ExportItem.fromData)(data.sites);
         return instance;
     }
 }
diff --git a/ui/src/app/configuration/export/data-export-import.component.ts 
b/ui/src/app/configuration/export/data-export-import.component.ts
index b9b06a1873..add7982b58 100644
--- a/ui/src/app/configuration/export/data-export-import.component.ts
+++ b/ui/src/app/configuration/export/data-export-import.component.ts
@@ -28,8 +28,15 @@ import {
 import { SpConfigurationRoutes } from '../configuration.breadcrumb';
 import { SpConfigurationTabsService } from '../configuration-tabs.service';
 import {
+    AssetConstants,
     AssetManagementService,
+    AssetSiteDesc,
+    ExportItem,
+    GenericStorageService,
+    LabelsService,
     SpAsset,
+    SpAssetModel,
+    SpLabel,
 } from '@streampipes/platform-services';
 import { MatCheckbox, MatCheckboxChange } from '@angular/material/checkbox';
 import { SpDataExportDialogComponent } from 
'./export-dialog/data-export-dialog.component';
@@ -41,6 +48,13 @@ import {
     LayoutDirective,
 } from '@ngbracket/ngx-layout/flex';
 import { MatButton } from '@angular/material/button';
+import { forkJoin, Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+interface AssetReferenceExportItems {
+    referencedLabels: ExportItem[];
+    referencedSites: ExportItem[];
+}
 
 @Component({
     selector: 'sp-data-export-import',
@@ -60,13 +74,15 @@ import { MatButton } from '@angular/material/button';
 export class SpDataExportImportComponent implements OnInit {
     private breadcrumbService = inject(SpBreadcrumbService);
     private assetManagementService = inject(AssetManagementService);
+    private genericStorageService = inject(GenericStorageService);
+    private labelsService = inject(LabelsService);
     private dialogService = inject(DialogService);
     private tabService = inject(SpConfigurationTabsService);
     private translateService = inject(TranslateService);
 
     tabs: SpNavigationItem[] = [];
 
-    assets: SpAsset[];
+    assets: SpAssetModel[] = [];
     selectedAssets: string[] = [];
 
     ngOnInit(): void {
@@ -98,12 +114,24 @@ export class SpDataExportImportComponent implements OnInit 
{
     }
 
     openExportDialog(): void {
+        this.getReferencedAssetDocuments().subscribe(
+            referencedAssetDocuments => {
+                this.openExportPreviewDialog(referencedAssetDocuments);
+            },
+        );
+    }
+
+    openExportPreviewDialog(
+        referencedAssetDocuments: AssetReferenceExportItems,
+    ): void {
         const dialogRef = this.dialogService.open(SpDataExportDialogComponent, 
{
             panelType: PanelType.SLIDE_IN_PANEL,
             title: this.translateService.instant('Export resources'),
             width: '50vw',
             data: {
                 selectedAssets: this.selectedAssets,
+                referencedLabels: referencedAssetDocuments.referencedLabels,
+                referencedSites: referencedAssetDocuments.referencedSites,
             },
         });
 
@@ -124,4 +152,70 @@ export class SpDataExportImportComponent implements OnInit 
{
             }
         });
     }
+
+    private getReferencedAssetDocuments(): 
Observable<AssetReferenceExportItems> {
+        return forkJoin({
+            assets: this.assetManagementService.getAllAssets(),
+            labels: this.labelsService.getAllLabels(),
+            sites: this.genericStorageService.getAllDocuments(
+                AssetConstants.ASSET_SITES_APP_DOC_NAME,
+            ),
+        }).pipe(
+            map(({ assets, labels, sites }) =>
+                this.toReferencedAssetDocuments(
+                    assets.filter(asset =>
+                        this.selectedAssets.includes(asset.elementId),
+                    ),
+                    labels,
+                    sites as AssetSiteDesc[],
+                ),
+            ),
+        );
+    }
+
+    private toReferencedAssetDocuments(
+        assets: SpAssetModel[],
+        labels: SpLabel[],
+        sites: AssetSiteDesc[],
+    ): AssetReferenceExportItems {
+        const labelIds = new Set<string>();
+        const siteIds = new Set<string>();
+
+        assets.forEach(asset =>
+            this.collectAssetReferences(asset, labelIds, siteIds),
+        );
+
+        return {
+            referencedLabels: labels
+                .filter(label => label._id && labelIds.has(label._id))
+                .map(label => ({
+                    resourceId: label._id!,
+                    label: label.label,
+                    selected: true,
+                })),
+            referencedSites: sites
+                .filter(site => site._id && siteIds.has(site._id))
+                .map(site => ({
+                    resourceId: site._id,
+                    label: site.label,
+                    selected: true,
+                })),
+        };
+    }
+
+    private collectAssetReferences(
+        asset: SpAsset,
+        labelIds: Set<string>,
+        siteIds: Set<string>,
+    ): void {
+        asset.labelIds?.forEach(labelId => labelIds.add(labelId));
+
+        if (asset.assetSite?.siteId) {
+            siteIds.add(asset.assetSite.siteId);
+        }
+
+        asset.assets?.forEach(subAsset =>
+            this.collectAssetReferences(subAsset, labelIds, siteIds),
+        );
+    }
 }
diff --git 
a/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.html
 
b/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.html
index c85f514d02..1863ff8511 100644
--- 
a/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.html
+++ 
b/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.html
@@ -22,7 +22,8 @@
             <div fxFlex="100" fxLayout="column" class="p-15">
                 @for (
                     config of preview.assetExportConfiguration;
-                    track config
+                    track config;
+                    let first = $first
                 ) {
                     <div>
                         <h4>
@@ -57,12 +58,29 @@
                             [exportItems]="config.pipelines"
                             [sectionTitle]="'Pipelines' | translate"
                         ></sp-data-export-item>
+
                         <sp-generic-storage-items
                             [exportItems]="config.genericStorageDocuments"
                         >
                         </sp-generic-storage-items>
                     </div>
                 }
+                @if (referencedLabels.length || referencedSites.length) {
+                    <h4>{{ 'Referenced resources' | translate }}</h4>
+                    @if (referencedLabels.length > 0) {
+                        <sp-data-export-item
+                            [exportItems]="referencedLabels"
+                            [sectionTitle]="'Labels' | translate"
+                        ></sp-data-export-item>
+                    }
+
+                    @if (referencedSites.length > 0) {
+                        <sp-data-export-item
+                            [exportItems]="referencedSites"
+                            [sectionTitle]="'Sites' | translate"
+                        ></sp-data-export-item>
+                    }
+                }
             </div>
         }
         @if (exportInProgress) {
diff --git 
a/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.ts 
b/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.ts
index 55282c7145..31b6dd1c28 100644
--- 
a/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.ts
+++ 
b/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.ts
@@ -19,7 +19,10 @@
 import { Component, Input, OnInit, inject } from '@angular/core';
 import { DialogRef } from '@streampipes/shared-ui';
 import { DataExportService } from '../data-export.service';
-import { ExportConfiguration } from '@streampipes/platform-services';
+import {
+    ExportConfiguration,
+    ExportItem,
+} from '@streampipes/platform-services';
 import {
     FlexDirective,
     LayoutAlignDirective,
@@ -55,6 +58,12 @@ export class SpDataExportDialogComponent implements OnInit {
     @Input()
     selectedAssets: string[];
 
+    @Input()
+    referencedLabels: ExportItem[] = [];
+
+    @Input()
+    referencedSites: ExportItem[] = [];
+
     preview: ExportConfiguration;
     exportInProgress = false;
 
@@ -72,6 +81,7 @@ export class SpDataExportDialogComponent implements OnInit {
 
     generateDownloadPackage(): void {
         this.exportInProgress = true;
+        this.addReferencedAssetDocuments(this.preview);
         this.dataExportService.triggerExport(this.preview).subscribe(result => 
{
             this.downloadFile(result);
         });
@@ -92,4 +102,33 @@ export class SpDataExportDialogComponent implements OnInit {
         window.URL.revokeObjectURL(url);
         this.dialogRef.close();
     }
+
+    private addReferencedAssetDocuments(preview: ExportConfiguration): void {
+        const firstAssetExportConfig = preview.assetExportConfiguration?.[0];
+
+        if (firstAssetExportConfig) {
+            firstAssetExportConfig.labels = this.mergeExportItems(
+                firstAssetExportConfig.labels,
+                this.referencedLabels,
+            );
+            firstAssetExportConfig.sites = this.mergeExportItems(
+                firstAssetExportConfig.sites,
+                this.referencedSites,
+            );
+        }
+    }
+
+    private mergeExportItems(
+        existingItems: ExportItem[] = [],
+        newItems: ExportItem[],
+    ): ExportItem[] {
+        const existingItemIds = new Set(
+            existingItems.map(item => item.resourceId),
+        );
+        const missingItems = newItems.filter(
+            item => !existingItemIds.has(item.resourceId),
+        );
+
+        return [...existingItems, ...missingItems];
+    }
 }
diff --git 
a/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.html
 
b/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.html
index 7ae8cdce00..9d64939a1f 100644
--- 
a/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.html
+++ 
b/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.html
@@ -140,6 +140,14 @@
                     [exportItems]="importConfiguration.pipelines"
                     [sectionTitle]="'Pipelines' | translate"
                 ></sp-data-export-item>
+                <sp-data-export-item
+                    [exportItems]="importConfiguration.labels"
+                    [sectionTitle]="'Labels' | translate"
+                ></sp-data-export-item>
+                <sp-data-export-item
+                    [exportItems]="importConfiguration.sites"
+                    [sectionTitle]="'Sites' | translate"
+                ></sp-data-export-item>
                 <sp-generic-storage-items
                     [exportItems]="importConfiguration.genericStorageDocuments"
                     [importMode]="true"
diff --git 
a/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.ts 
b/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.ts
index 6a95b69b63..4a15ae4e1a 100644
--- 
a/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.ts
+++ 
b/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.ts
@@ -165,6 +165,8 @@ export class SpDataImportDialogComponent {
         this.toggleAllItems(this.importConfiguration.dashboards, select);
         this.toggleAllItems(this.importConfiguration.dataViews, select);
         this.toggleAllItems(this.importConfiguration.dataLakeMeasures, select);
+        this.toggleAllItems(this.importConfiguration.labels, select);
+        this.toggleAllItems(this.importConfiguration.sites, select);
         this.toggleAllItems(
             this.importConfiguration.genericStorageDocuments,
             select,
@@ -173,6 +175,6 @@ export class SpDataImportDialogComponent {
     }
 
     private toggleAllItems(exportItem: ExportItem[], select: boolean): void {
-        exportItem.forEach(e => (e.selected = select));
+        exportItem?.forEach(e => (e.selected = select));
     }
 }
diff --git 
a/ui/src/app/configuration/label-configuration/label-configuration.component.html
 
b/ui/src/app/configuration/label-configuration/label-configuration.component.html
index 5420df12f5..c0375c8988 100644
--- 
a/ui/src/app/configuration/label-configuration/label-configuration.component.html
+++ 
b/ui/src/app/configuration/label-configuration/label-configuration.component.html
@@ -125,7 +125,6 @@
                                     color="accent"
                                     (click)="deleteLabel(label)"
                                     data-cy="delete-label-button"
-                                    
[disabled]="labelsinUse.includes(label._id)"
                                 >
                                     <mat-icon>delete</mat-icon>
                                 </button>
diff --git 
a/ui/src/app/configuration/label-configuration/label-configuration.component.ts 
b/ui/src/app/configuration/label-configuration/label-configuration.component.ts
index 960edf19b3..c6e03b4b35 100644
--- 
a/ui/src/app/configuration/label-configuration/label-configuration.component.ts
+++ 
b/ui/src/app/configuration/label-configuration/label-configuration.component.ts
@@ -21,6 +21,7 @@ import { SpConfigurationTabsService } from 
'../configuration-tabs.service';
 import { LabelsService, SpLabel } from '@streampipes/platform-services';
 import { SpConfigurationRoutes } from '../configuration.breadcrumb';
 import {
+    ConfirmDialogComponent,
     SpBasicNavTabsComponent,
     SpBreadcrumbService,
     SpLabelComponent,
@@ -46,7 +47,8 @@ import { MatButton, MatIconButton } from 
'@angular/material/button';
 import { SpEditLabelComponent } from './edit-label/edit-label.component';
 import { MatTooltip } from '@angular/material/tooltip';
 import { MatIcon } from '@angular/material/icon';
-import { TranslatePipe } from '@ngx-translate/core';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { MatDialog } from '@angular/material/dialog';
 
 @Component({
     selector: 'sp-label-configuration',
@@ -79,6 +81,8 @@ export class SpLabelConfigurationComponent implements OnInit {
     private breadcrumbService = inject(SpBreadcrumbService);
     private labelsService = inject(LabelsService);
     private tabService = inject(SpConfigurationTabsService);
+    private dialog = inject(MatDialog);
+    private translateService = inject(TranslateService);
 
     tabs: SpNavigationItem[] = [];
 
@@ -132,8 +136,53 @@ export class SpLabelConfigurationComponent implements 
OnInit {
     }
 
     deleteLabel(label: SpLabel): void {
-        this.labelsService.deleteLabel(label._id, label._rev).subscribe(() => {
-            this.reloadLabels();
+        this.labelsService.getLabelsInUse().subscribe(labelsInUse => {
+            this.labelsinUse = labelsInUse;
+
+            if (labelsInUse.includes(label._id)) {
+                this.showLabelInUseWarning();
+            } else {
+                this.showDeleteLabelDialog(label);
+            }
+        });
+    }
+
+    showLabelInUseWarning(): void {
+        this.dialog.open(ConfirmDialogComponent, {
+            width: '500px',
+            data: {
+                title: this.translateService.instant('Label is still in use'),
+                subtitle: this.translateService.instant(
+                    'To delete a label, please remove the label from all 
assets.',
+                ),
+                confirmTitle: this.translateService.instant('Ok'),
+            },
+        });
+    }
+
+    showDeleteLabelDialog(label: SpLabel): void {
+        const dialogRef = this.dialog.open(ConfirmDialogComponent, {
+            width: '500px',
+            data: {
+                title: this.translateService.instant(
+                    'Are you sure you want to delete this label?',
+                ),
+                subtitle: this.translateService.instant(
+                    'This action cannot be reversed!',
+                ),
+                cancelTitle: this.translateService.instant('Cancel'),
+                confirmTitle: this.translateService.instant('Delete label'),
+            },
+        });
+
+        dialogRef.afterClosed().subscribe(result => {
+            if (result === 'confirm') {
+                this.labelsService
+                    .deleteLabel(label._id, label._rev)
+                    .subscribe(() => {
+                        this.reloadLabels();
+                    });
+            }
         });
     }
 
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
index 39eeff4bf0..a43162717a 100644
--- 
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
@@ -87,7 +87,6 @@
                             (click)="deleteSite(site)"
                             mat-icon-button
                             color="accent"
-                            [disabled]="allUsedSiteIds.includes(site._id)"
                         >
                             <mat-icon>delete</mat-icon>
                         </button>
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
index 0b9e86dbbc..4d6c12e234 100644
--- 
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
@@ -33,6 +33,7 @@ import {
 } from '@angular/material/table';
 import { ManageSiteDialogComponent } from 
'../../dialog/manage-site/manage-site-dialog.component';
 import {
+    ConfirmDialogComponent,
     DialogService,
     PanelType,
     SplitSectionComponent,
@@ -47,6 +48,14 @@ import {
     LayoutDirective,
 } from '@ngbracket/ngx-layout/flex';
 import { MatTooltip } from '@angular/material/tooltip';
+import { MatDialog } from '@angular/material/dialog';
+
+interface AssetWithSite {
+    assetSite?: {
+        siteId?: string;
+    };
+    assets?: AssetWithSite[];
+}
 
 @Component({
     selector: 'sp-site-area-configuration',
@@ -74,6 +83,7 @@ export class SiteAreaConfigurationComponent implements OnInit 
{
     private genericStorageService = inject(GenericStorageService);
     private dialogService = inject(DialogService);
     private translateService = inject(TranslateService);
+    private dialog = inject(MatDialog);
 
     @Input()
     locationConfig: LocationConfig;
@@ -82,7 +92,7 @@ export class SiteAreaConfigurationComponent implements OnInit 
{
     dataSource: MatTableDataSource<AssetSiteDesc> =
         new MatTableDataSource<AssetSiteDesc>();
 
-    allUsedSiteIds = [];
+    allUsedSiteIds: string[] = [];
 
     @ViewChild(MatSort)
     sort: MatSort;
@@ -121,15 +131,71 @@ export class SiteAreaConfigurationComponent implements 
OnInit {
             });
     }
 
-    extractSiteIds(assets) {
+    extractSiteIds(assets: AssetWithSite[]): string[] {
         const allSiteIds = new Set<string>();
 
-        assets.forEach(asset => allSiteIds.add(asset.assetSite.siteId));
+        const extractSiteFromAsset = (asset: AssetWithSite) => {
+            if (asset.assetSite?.siteId) {
+                allSiteIds.add(asset.assetSite.siteId);
+            }
+            asset.assets?.forEach(subAsset => extractSiteFromAsset(subAsset));
+        };
+
+        assets.forEach(asset => extractSiteFromAsset(asset));
 
         return Array.from(allSiteIds);
     }
 
     deleteSite(site: AssetSiteDesc): void {
+        this.genericStorageService
+            .getAllDocuments(AssetConstants.ASSET_APP_DOC_NAME)
+            .subscribe(res => {
+                this.allUsedSiteIds = this.extractSiteIds(res);
+
+                if (this.allUsedSiteIds.includes(site._id)) {
+                    this.showSiteInUseWarning();
+                } else {
+                    this.showDeleteSiteDialog(site);
+                }
+            });
+    }
+
+    showSiteInUseWarning(): void {
+        this.dialog.open(ConfirmDialogComponent, {
+            width: '500px',
+            data: {
+                title: this.translateService.instant('Site is still in use'),
+                subtitle: this.translateService.instant(
+                    'To delete a site, please remove the site from all 
assets.',
+                ),
+                confirmTitle: this.translateService.instant('Ok'),
+            },
+        });
+    }
+
+    showDeleteSiteDialog(site: AssetSiteDesc): void {
+        const dialogRef = this.dialog.open(ConfirmDialogComponent, {
+            width: '500px',
+            data: {
+                title: this.translateService.instant(
+                    'Are you sure you want to delete this site?',
+                ),
+                subtitle: this.translateService.instant(
+                    'This action cannot be reversed!',
+                ),
+                cancelTitle: this.translateService.instant('Cancel'),
+                confirmTitle: this.translateService.instant('Delete site'),
+            },
+        });
+
+        dialogRef.afterClosed().subscribe(result => {
+            if (result === 'confirm') {
+                this.deleteSiteDocument(site);
+            }
+        });
+    }
+
+    deleteSiteDocument(site: AssetSiteDesc): void {
         this.genericStorageService
             .deleteDocument(
                 AssetConstants.ASSET_SITES_APP_DOC_NAME,


Reply via email to