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 76cce2a1e7 feat: Improve export dialog (#4502)
76cce2a1e7 is described below

commit 76cce2a1e7e5c73fa123276cae9e343705e0e096
Author: Dominik Riemer <[email protected]>
AuthorDate: Sat Jun 20 14:08:48 2026 +0200

    feat: Improve export dialog (#4502)
---
 .../apache/streampipes/export/ExportManager.java   |  19 ++
 .../export/generator/ExportPackageGenerator.java   |  50 ++++-
 .../model/export/ExportConfiguration.java          |  14 ++
 .../storage/api/system/IGenericStorage.java        |   2 +
 .../couchdb/impl/system/GenericStorageImpl.java    |  27 +++
 ui/deployment/i18n/de.json                         |   8 +-
 ui/deployment/i18n/en.json                         |   8 +-
 ui/deployment/i18n/pl.json                         |   8 +-
 .../src/lib/model/gen/streampipes-model.ts         |   4 +
 .../export/data-export-import.component.html       |  18 ++
 .../export/data-export-import.component.ts         |  52 +++--
 .../data-export-dialog.component.html              | 237 ++++++++++++++++-----
 .../export-dialog/data-export-dialog.component.ts  | 233 ++++++++++++++++++--
 .../data-import-dialog.component.html              |  31 ++-
 14 files changed, 601 insertions(+), 110 deletions(-)

diff --git 
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/ExportManager.java
 
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/ExportManager.java
index 37ccdeaef8..d6ffe88194 100644
--- 
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/ExportManager.java
+++ 
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/ExportManager.java
@@ -20,7 +20,10 @@ package org.apache.streampipes.export;
 
 import org.apache.streampipes.export.generator.ExportPackageGenerator;
 import 
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
+import org.apache.streampipes.model.assets.SpAssetModel;
 import org.apache.streampipes.model.export.ExportConfiguration;
+import org.apache.streampipes.model.export.ExportItem;
+import org.apache.streampipes.storage.management.StorageDispatcher;
 
 import java.io.IOException;
 import java.util.List;
@@ -35,8 +38,10 @@ public class ExportManager {
         .stream()
         .map(assetId -> new AssetLinkResolver(assetId, 
extensionServiceRequestManager).resolveResources())
         .collect(Collectors.toList());
+    var genericStorageAppDocTypes = getGenericStorageAppDocTypes();
 
     exportConfig.setAssetExportConfiguration(assetExportConfigurations);
+    exportConfig.setGenericStorageAppDocTypes(genericStorageAppDocTypes);
 
     return exportConfig;
   }
@@ -47,4 +52,18 @@ public class ExportManager {
     return new ExportPackageGenerator(exportConfiguration, 
extensionServiceRequestManager).generateExportPackage();
   }
 
+  private static List<ExportItem> getGenericStorageAppDocTypes() {
+    try {
+      return StorageDispatcher.INSTANCE.getNoSqlStore()
+          .getGenericStorage()
+          .getAllAppDocTypes()
+          .stream()
+          .filter(appDocType -> !SpAssetModel.APP_DOC_TYPE.equals(appDocType))
+          .map(appDocType -> new ExportItem(appDocType, appDocType, false))
+          .collect(Collectors.toList());
+    } catch (IOException e) {
+        return List.of();
+    }
+  }
+
 }
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 9637e204d1..3d1bc2b684 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
@@ -46,8 +46,10 @@ import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.nio.file.Files;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
@@ -72,6 +74,7 @@ public class ExportPackageGenerator {
   public byte[] generateExportPackage() throws IOException {
     ZipFileBuilder builder = ZipFileBuilder.create();
     var manifest = new StreamPipesApplicationPackage();
+    Set<String> exportedGenericStorageDocumentIds = new HashSet<>();
 
     addAssets(builder, exportConfiguration
         .getAssetExportConfiguration()
@@ -119,7 +122,11 @@ public class ExportPackageGenerator {
       });
 
       config.getGenericStorageDocuments().forEach(item -> {
-        addDoc(builder, item, new GenericStorageDocumentResolver(), 
manifest::addGenericStorageDocument);
+        addGenericStorageDocument(builder,
+            item,
+            new GenericStorageDocumentResolver(),
+            manifest::addGenericStorageDocument,
+            exportedGenericStorageDocumentIds);
       });
 
       config.getLabels().forEach(item -> {
@@ -144,6 +151,17 @@ public class ExportPackageGenerator {
       });
     });
 
+    if (exportConfiguration.getGenericStorageAppDocTypes() != null) {
+      exportConfiguration.getGenericStorageAppDocTypes().forEach(item -> {
+        if (item.isSelected()) {
+          addGenericStorageDocumentsByType(builder,
+              item.getResourceId(),
+              manifest::addGenericStorageDocument,
+              exportedGenericStorageDocumentIds);
+        }
+      });
+    }
+
     builder.addManifest(defaultMapper.writeValueAsString(manifest));
 
 
@@ -177,6 +195,36 @@ public class ExportPackageGenerator {
     }
   }
 
+  private void addGenericStorageDocumentsByType(ZipFileBuilder builder,
+                                                String appDocType,
+                                                Consumer<String> function,
+                                                Set<String> 
exportedGenericStorageDocumentIds) {
+    var resolver = new GenericStorageDocumentResolver();
+
+    try {
+      StorageDispatcher.INSTANCE.getNoSqlStore()
+          .getGenericStorage()
+          .findAll(appDocType)
+          .forEach(document -> addGenericStorageDocument(builder,
+              resolver.convert(document),
+              resolver,
+              function,
+              exportedGenericStorageDocumentIds));
+    } catch (IOException e) {
+      LOG.warn("Could not load generic storage documents for appDocType {}", 
appDocType, e);
+    }
+  }
+
+  private void addGenericStorageDocument(ZipFileBuilder builder,
+                                         ExportItem exportItem,
+                                         AbstractResolver<?> resolver,
+                                         Consumer<String> function,
+                                         Set<String> 
exportedGenericStorageDocumentIds) {
+    if (exportedGenericStorageDocumentIds.add(exportItem.getResourceId())) {
+      addDoc(builder, exportItem, resolver, function);
+    }
+  }
+
   private String sanitize(String resourceId) {
     return resourceId.replaceAll(":", "").replaceAll("\\.", "");
   }
diff --git 
a/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportConfiguration.java
 
b/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportConfiguration.java
index 2a0052ba1e..f6656a4172 100644
--- 
a/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportConfiguration.java
+++ 
b/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportConfiguration.java
@@ -28,9 +28,11 @@ import java.util.List;
 public class ExportConfiguration {
 
   private List<AssetExportConfiguration> assetExportConfiguration;
+  private List<ExportItem> genericStorageAppDocTypes;
 
   public ExportConfiguration() {
     this.assetExportConfiguration = new ArrayList<>();
+    this.genericStorageAppDocTypes = new ArrayList<>();
   }
 
   public List<AssetExportConfiguration> getAssetExportConfiguration() {
@@ -40,4 +42,16 @@ public class ExportConfiguration {
   public void setAssetExportConfiguration(List<AssetExportConfiguration> 
assetExportConfiguration) {
     this.assetExportConfiguration = assetExportConfiguration;
   }
+
+  public List<ExportItem> getGenericStorageAppDocTypes() {
+    return genericStorageAppDocTypes;
+  }
+
+  public void setGenericStorageAppDocTypes(List<ExportItem> 
genericStorageAppDocTypes) {
+    this.genericStorageAppDocTypes = genericStorageAppDocTypes;
+  }
+
+  public void addGenericStorageAppDocType(ExportItem exportItem) {
+    this.genericStorageAppDocTypes.add(exportItem);
+  }
 }
diff --git 
a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/system/IGenericStorage.java
 
b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/system/IGenericStorage.java
index a84a3fa786..5fcc957c71 100644
--- 
a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/system/IGenericStorage.java
+++ 
b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/system/IGenericStorage.java
@@ -26,6 +26,8 @@ import java.util.Map;
 
 public interface IGenericStorage {
 
+  List<String> getAllAppDocTypes() throws IOException;
+
   List<Map<String, Object>> findAll(String type) throws IOException;
 
   List<Map<String, Object>> find(String appDocType, Map<String, Object> query) 
throws IOException;
diff --git 
a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/system/GenericStorageImpl.java
 
b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/system/GenericStorageImpl.java
index bbe8afddcc..f3ffc17093 100644
--- 
a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/system/GenericStorageImpl.java
+++ 
b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/system/GenericStorageImpl.java
@@ -36,6 +36,8 @@ import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
 
 public class GenericStorageImpl implements IGenericStorage {
 
@@ -55,6 +57,31 @@ public class GenericStorageImpl implements IGenericStorage {
         .setSerializationInclusion(JsonInclude.Include.NON_NULL);
   }
 
+  @Override
+  public List<String> getAllAppDocTypes() throws IOException {
+    String query = getDatabaseRoute() + "/_design/appDocType/_view/appDocType";
+    Map<String, Object> queryResult = this.queryDocuments(query);
+
+    List<Map<String, Object>> rows = (List<Map<String, Object>>) 
queryResult.get("rows");
+
+    return rows.stream()
+        .filter(row -> !String.valueOf(row.get(ID)).startsWith("_design"))
+        .map(row -> row.get("key"))
+        .map(key -> {
+          if (key instanceof List<?> keyList && !keyList.isEmpty()) {
+            return keyList.get(0);
+          }
+
+          return key;
+        })
+        .filter(Objects::nonNull)
+        .map(Object::toString)
+        .filter(appDocType -> !appDocType.isBlank())
+        .distinct()
+        .sorted()
+        .collect(Collectors.toList());
+  }
+
   @Override
   public List<Map<String, Object>> findAll(String type) throws IOException {
     String query = getDatabaseRoute() + 
"/_design/appDocType/_view/appDocType?" + Utils
diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index 46bfc43003..a5d72cf6a9 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -80,6 +80,7 @@
   "Add user": "Benutzer hinzufügen",
   "Add, Edit, and Delete export providers used for backing up data lakes.": 
"Hinzufügen, Bearbeiten und Löschen von Exportanbietern, die für die Sicherung 
von Data Lakes verwendet werden.",
   "Additional documents from generic storage": "Zusätzliche Dokumente aus dem 
Generic Storage",
+  "Additional exports": "Zusätzliche Exporte",
   "Advanced Filter": "Erweiterter Filter",
   "Advanced filter": "Erweiterter Filter",
   "Advanced filter active": "Erweiterter Filter aktiv",
@@ -442,9 +443,9 @@
   "Export Provider": "Exportanbieter",
   "Export Providers": "Exportanbieter",
   "Export Settings": "Export-Einstellungen",
+  "Export all documents": "Alle Dokumente exportieren",
   "Export assets and all linked resources": "Assets und alle damit verbundenen 
Ressourcen exportieren",
   "Export resources": "Ressourcen exportieren",
-  "Exported items {{assetName}}": "Exportierte Items {{assetName}}",
   "Exporting resources...": "Ressourcen exportieren...",
   "Fail": "Fehlgeschlagen",
   "False": "Falsch",
@@ -871,6 +872,9 @@
   "Rename the changed fields": "Geänderte Felder umbenennen",
   "Rename the fields with changed data types by using the \"Field Renamer\" 
data processor": "Benennen Sie die Felder mit geänderten Datentypen mit dem 
Datenprozessor \"Field Renamer\" um",
   "Repeat password": "Passwort wiederholen",
+  "Replace existing": "Vorhandene ersetzen",
+  "Replace existing documents with the same ID": "Vorhandene Dokumente mit 
derselben ID ersetzen",
+  "Replacing existing documents can cause side-effects for resources that are 
related to the replaced document. Use this feature with caution.": "Das 
Ersetzen vorhandener Dokumente kann Nebenwirkungen für Ressourcen verursachen, 
die mit dem ersetzten Dokument verknüpft sind. Verwenden Sie diese Funktion mit 
Vorsicht.",
   "Require users to accept terms after login": "Benutzer müssen 
Nutzungsbedingungen nach Anmeldung akzeptieren",
   "Reset": "Zurücksetzen",
   "Reset adapter state": "Adapterstatus zurücksetzen",
@@ -953,6 +957,7 @@
   "Select visible": "Sichtbare auswählen",
   "Selected Nodes": "Ausgewählte Knoten",
   "Selected Privileges": "Ausgewählte Rechte",
+  "Selecting a document type will export all documents of this type": "Durch 
die Auswahl eines Dokumenttyps werden alle Dokumente dieses Typs exportiert",
   "Selects which values are shown in each label.": "Wählt aus, welche Werte in 
jeder Beschriftung angezeigt werden.",
   "Self-registration requires valid mail server and configured basic host/port 
settings.": "Die Selbstregistrierung erfordert einen gültigen Mailserver und 
konfigurierte Grundeinstellungen für Host und Port.",
   "Semantic type": "Semantischer Typ",
@@ -1190,7 +1195,6 @@
   "Update pipeline": "Pipeline aktualisieren",
   "Update profile": "Profil aktualisieren",
   "Update result preview": "Ergebnisvorschau aktualisieren",
-  "Update/overwrite existing documents with the same ID (operation may break 
things)": "Vorhandene Dokumente mit derselben ID aktualisieren/überschreiben 
(dieser Vorgang kann zu Problemen führen)",
   "Updating adapter {{adapterName}}": "Aktualisieren des Adapters 
{{adapterName}}",
   "Upload": "Hochladen",
   "Upload CSV file": "CSV-Datei hochladen",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index 66c48b1178..f4b2ed0f2d 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -80,6 +80,7 @@
   "Add user": null,
   "Add, Edit, and Delete export providers used for backing up data lakes.": 
null,
   "Additional documents from generic storage": null,
+  "Additional exports": null,
   "Advanced Filter": null,
   "Advanced filter": null,
   "Advanced filter active": null,
@@ -442,9 +443,9 @@
   "Export Provider": null,
   "Export Providers": null,
   "Export Settings": null,
+  "Export all documents": null,
   "Export assets and all linked resources": null,
   "Export resources": null,
-  "Exported items {{assetName}}": "Exported items {{assetName}}",
   "Exporting resources...": null,
   "Fail": null,
   "False": null,
@@ -871,6 +872,9 @@
   "Rename the changed fields": null,
   "Rename the fields with changed data types by using the \"Field Renamer\" 
data processor": null,
   "Repeat password": null,
+  "Replace existing": null,
+  "Replace existing documents with the same ID": null,
+  "Replacing existing documents can cause side-effects for resources that are 
related to the replaced document. Use this feature with caution.": null,
   "Require users to accept terms after login": null,
   "Reset": null,
   "Reset adapter state": null,
@@ -953,6 +957,7 @@
   "Select visible": null,
   "Selected Nodes": null,
   "Selected Privileges": null,
+  "Selecting a document type will export all documents of this type": null,
   "Selects which values are shown in each label.": null,
   "Self-registration requires valid mail server and configured basic host/port 
settings.": null,
   "Semantic type": null,
@@ -1190,7 +1195,6 @@
   "Update pipeline": null,
   "Update profile": null,
   "Update result preview": null,
-  "Update/overwrite existing documents with the same ID (operation may break 
things)": null,
   "Updating adapter {{adapterName}}": "Updating adapter {{adapterName}}",
   "Upload": null,
   "Upload CSV file": null,
diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json
index b998c2d4c2..4ff88dd0b2 100644
--- a/ui/deployment/i18n/pl.json
+++ b/ui/deployment/i18n/pl.json
@@ -80,6 +80,7 @@
   "Add user": "Dodaj użytkownika",
   "Add, Edit, and Delete export providers used for backing up data lakes.": 
"Dodawaj, edytuj i usuwaj exporterów używanych do tworzenia kopii zapasowych 
Data Lake.",
   "Additional documents from generic storage": "Dodatkowe dokumenty z ogólnego 
magazynu",
+  "Additional exports": "Dodatkowe eksporty",
   "Advanced Filter": "Filtr zaawansowany",
   "Advanced filter": "Filtr zaawansowany",
   "Advanced filter active": "Filtr zaawansowany aktywny",
@@ -442,9 +443,9 @@
   "Export Provider": "Eksporter",
   "Export Providers": "Dostawcy eksportu",
   "Export Settings": "Ustawienia eksportu",
+  "Export all documents": "Eksportuj wszystkie dokumenty",
   "Export assets and all linked resources": "Eksportuj zasoby i wszystkie 
powiązane zasoby",
   "Export resources": "Eksportuj zasoby",
-  "Exported items {{assetName}}": "Wyeksportowane elementy {{assetName}}",
   "Exporting resources...": "Eksportowanie zasobów...",
   "Fail": "Niepowodzenie",
   "False": "Fałsz",
@@ -871,6 +872,9 @@
   "Rename the changed fields": "Zmień nazwy zmienionych pól",
   "Rename the fields with changed data types by using the \"Field Renamer\" 
data processor": "Zmień nazwy pól ze zmienionymi typami danych za pomocą 
procesora danych \"Field Renamer\"",
   "Repeat password": "Powtórz hasło",
+  "Replace existing": "Zastąp istniejące",
+  "Replace existing documents with the same ID": "Zastąp istniejące dokumenty 
o tym samym ID",
+  "Replacing existing documents can cause side-effects for resources that are 
related to the replaced document. Use this feature with caution.": 
"Zastępowanie istniejących dokumentów może powodować skutki uboczne dla zasobów 
powiązanych z zastępowanym dokumentem. Korzystaj z tej funkcji ostrożnie.",
   "Require users to accept terms after login": "Wymagaj zaakceptowania 
warunków po zalogowaniu",
   "Reset": "Resetuj",
   "Reset adapter state": "Zresetuj stan adaptera",
@@ -953,6 +957,7 @@
   "Select visible": "Wybierz widoczne",
   "Selected Nodes": "Wybrane węzły",
   "Selected Privileges": "Wybrane uprawnienia",
+  "Selecting a document type will export all documents of this type": 
"Wybranie typu dokumentu spowoduje wyeksportowanie wszystkich dokumentów tego 
typu",
   "Selects which values are shown in each label.": "Wybiera, które wartości są 
wyświetlane na każdej etykiecie.",
   "Self-registration requires valid mail server and configured basic host/port 
settings.": "Samorejestracja wymaga poprawnego serwera pocztowego oraz 
skonfigurowanych podstawowych ustawień hosta/portu.",
   "Semantic type": "Typ semantyczny",
@@ -1190,7 +1195,6 @@
   "Update pipeline": "Zaktualizuj strumień",
   "Update profile": "Zaktualizuj profil",
   "Update result preview": "Zaktualizuj podgląd wyniku",
-  "Update/overwrite existing documents with the same ID (operation may break 
things)": "Aktualizuj/nadpisz istniejące dokumenty o tym samym ID (operacja 
może coś zepsuć:) )",
   "Updating adapter {{adapterName}}": "Aktualizowanie adaptera 
{{adapterName}}",
   "Upload": "Prześlij",
   "Upload CSV file": "Prześlij plik CSV",
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 ba62219b0e..d4c873037d 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
@@ -1993,6 +1993,7 @@ export class ExportConfig {
 
 export class ExportConfiguration {
     assetExportConfiguration: AssetExportConfiguration[];
+    genericStorageAppDocTypes: ExportItem[];
 
     static fromData(
         data: ExportConfiguration,
@@ -2005,6 +2006,9 @@ export class ExportConfiguration {
         instance.assetExportConfiguration = __getCopyArrayFn(
             AssetExportConfiguration.fromData,
         )(data.assetExportConfiguration);
+        instance.genericStorageAppDocTypes = __getCopyArrayFn(
+            ExportItem.fromData,
+        )(data.genericStorageAppDocTypes);
         return instance;
     }
 }
diff --git a/ui/src/app/configuration/export/data-export-import.component.html 
b/ui/src/app/configuration/export/data-export-import.component.html
index 2175486f27..638edcca44 100644
--- a/ui/src/app/configuration/export/data-export-import.component.html
+++ b/ui/src/app/configuration/export/data-export-import.component.html
@@ -25,9 +25,27 @@
                     'Export assets and all linked resources' | translate
                 "
             >
+                <div section-actions fxLayout="row" fxLayoutGap="10px">
+                    <button
+                        mat-button
+                        mat-flat-button
+                        (click)="selectAllAssets(true)"
+                    >
+                        {{ 'Select all' | translate }}
+                    </button>
+                    <button
+                        mat-button
+                        mat-flat-button
+                        class="mat-basic"
+                        (click)="selectAllAssets(false)"
+                    >
+                        {{ 'Select none' | translate }}
+                    </button>
+                </div>
                 @for (asset of assets; track asset) {
                     <div fxLayout="column">
                         <mat-checkbox
+                            [checked]="isSelected(asset.elementId)"
                             (change)="
                                 handleSelectionChange($event, asset.elementId)
                             "
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 add7982b58..1e11fef721 100644
--- a/ui/src/app/configuration/export/data-export-import.component.ts
+++ b/ui/src/app/configuration/export/data-export-import.component.ts
@@ -46,14 +46,15 @@ import {
     FlexDirective,
     LayoutAlignDirective,
     LayoutDirective,
+    LayoutGapDirective,
 } 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[];
+    referencedLabels: Record<string, ExportItem[]>;
+    referencedSites: Record<string, ExportItem[]>;
 }
 
 @Component({
@@ -65,6 +66,7 @@ interface AssetReferenceExportItems {
         LayoutDirective,
         FlexDirective,
         LayoutAlignDirective,
+        LayoutGapDirective,
         SplitSectionComponent,
         MatCheckbox,
         MatButton,
@@ -113,6 +115,16 @@ export class SpDataExportImportComponent implements OnInit 
{
         }
     }
 
+    selectAllAssets(select: boolean): void {
+        this.selectedAssets = select
+            ? this.assets.map(asset => asset.elementId)
+            : [];
+    }
+
+    isSelected(assetId: string): boolean {
+        return this.selectedAssets.includes(assetId);
+    }
+
     openExportDialog(): void {
         this.getReferencedAssetDocuments().subscribe(
             referencedAssetDocuments => {
@@ -178,29 +190,39 @@ export class SpDataExportImportComponent implements 
OnInit {
         labels: SpLabel[],
         sites: AssetSiteDesc[],
     ): AssetReferenceExportItems {
-        const labelIds = new Set<string>();
-        const siteIds = new Set<string>();
-
-        assets.forEach(asset =>
-            this.collectAssetReferences(asset, labelIds, siteIds),
+        const labelsById = new Map(
+            labels.filter(label => label._id).map(label => [label._id!, 
label]),
         );
+        const sitesById = new Map(
+            sites.filter(site => site._id).map(site => [site._id, site]),
+        );
+        const referencedLabels: Record<string, ExportItem[]> = {};
+        const referencedSites: Record<string, ExportItem[]> = {};
+
+        assets.forEach(asset => {
+            const labelIds = new Set<string>();
+            const siteIds = new Set<string>();
+            this.collectAssetReferences(asset, labelIds, siteIds);
 
-        return {
-            referencedLabels: labels
-                .filter(label => label._id && labelIds.has(label._id))
+            referencedLabels[asset.elementId] = [...labelIds]
+                .map(labelId => labelsById.get(labelId))
+                .filter((label): label is SpLabel => label !== undefined)
                 .map(label => ({
                     resourceId: label._id!,
                     label: label.label,
                     selected: true,
-                })),
-            referencedSites: sites
-                .filter(site => site._id && siteIds.has(site._id))
+                }));
+            referencedSites[asset.elementId] = [...siteIds]
+                .map(siteId => sitesById.get(siteId))
+                .filter((site): site is AssetSiteDesc => site !== undefined)
                 .map(site => ({
                     resourceId: site._id,
                     label: site.label,
                     selected: true,
-                })),
-        };
+                }));
+        });
+
+        return { referencedLabels, referencedSites };
     }
 
     private collectAssetReferences(
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 1863ff8511..78ad88e566 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
@@ -20,67 +20,186 @@
     <div class="sp-dialog-content">
         @if (preview && !exportInProgress) {
             <div fxFlex="100" fxLayout="column" class="p-15">
-                @for (
-                    config of preview.assetExportConfiguration;
-                    track config;
-                    let first = $first
-                ) {
-                    <div>
-                        <h4>
-                            {{ 'Exported items {{assetName}}' | translate:{
-                            assetName : config.assetName} }}
-                        </h4>
-                        <sp-data-export-item
-                            [exportItems]="config.adapters"
-                            [sectionTitle]="'Adapters' | translate"
-                        ></sp-data-export-item>
-                        <sp-data-export-item
-                            [exportItems]="config.dashboards"
-                            [sectionTitle]="'Dashboards' | translate"
-                        ></sp-data-export-item>
-                        <sp-data-export-item
-                            [exportItems]="config.dataViews"
-                            [sectionTitle]="'Charts' | translate"
-                        ></sp-data-export-item>
-                        <sp-data-export-item
-                            [exportItems]="config.dataSources"
-                            [sectionTitle]="'Data Streams' | translate"
-                        ></sp-data-export-item>
-                        <sp-data-export-item
-                            [exportItems]="config.dataLakeMeasures"
-                            [sectionTitle]="'Data Lake Storage' | translate"
-                        ></sp-data-export-item>
-                        <sp-data-export-item
-                            [exportItems]="config.files"
-                            [sectionTitle]="'Files' | translate"
-                        ></sp-data-export-item>
-                        <sp-data-export-item
-                            [exportItems]="config.pipelines"
-                            [sectionTitle]="'Pipelines' | translate"
-                        ></sp-data-export-item>
-
-                        <sp-generic-storage-items
-                            [exportItems]="config.genericStorageDocuments"
-                        >
-                        </sp-generic-storage-items>
-                    </div>
+                @for (section of exportSections; track section.key) {
+                    <sp-split-section
+                        [level]="3"
+                        [title]="section.title | translate"
+                    >
+                        @if (getAssetsForSection(section.key).length > 0) {
+                            @for (
+                                assetConfig of 
getAssetsForSection(section.key);
+                                track assetConfig.assetId
+                            ) {
+                                <div
+                                    fxLayout="row"
+                                    fxLayoutAlign="space-between center"
+                                >
+                                    <h5>
+                                        {{ assetConfig.assetName }}
+                                    </h5>
+                                    <mat-slide-toggle
+                                        color="accent"
+                                        [checked]="
+                                            isExportAllSelected(
+                                                section.key,
+                                                assetConfig
+                                            )
+                                        "
+                                        (change)="
+                                            changeExportAllSelection(
+                                                section.key,
+                                                assetConfig,
+                                                $event
+                                            )
+                                        "
+                                    >
+                                        {{ 'Export all documents' | translate 
}}
+                                    </mat-slide-toggle>
+                                </div>
+                                @if (
+                                    !isExportAllSelected(
+                                        section.key,
+                                        assetConfig
+                                    )
+                                ) {
+                                    <div
+                                        fxLayout="row"
+                                        fxLayoutGap="10px"
+                                        class="mb-5"
+                                    >
+                                        <button
+                                            mat-button
+                                            mat-flat-button
+                                            color="accent"
+                                            class="small-button"
+                                            (click)="
+                                                selectAllItemsForAsset(
+                                                    section.key,
+                                                    assetConfig,
+                                                    true
+                                                )
+                                            "
+                                        >
+                                            {{ 'Select all' | translate }}
+                                        </button>
+                                        <button
+                                            mat-button
+                                            mat-flat-button
+                                            class="small-button mat-basic"
+                                            (click)="
+                                                selectAllItemsForAsset(
+                                                    section.key,
+                                                    assetConfig,
+                                                    false
+                                                )
+                                            "
+                                        >
+                                            {{ 'Select none' | translate }}
+                                        </button>
+                                    </div>
+                                    <mat-form-field
+                                        appearance="outline"
+                                        class="form-field mt-md"
+                                    >
+                                        <mat-select
+                                            multiple
+                                            [ngModel]="
+                                                getSelectedItemResourceIds(
+                                                    section.key,
+                                                    assetConfig
+                                                )
+                                            "
+                                            (ngModelChange)="
+                                                updateSelectedItems(
+                                                    section.key,
+                                                    assetConfig,
+                                                    $event
+                                                )
+                                            "
+                                        >
+                                            @for (
+                                                exportItem of 
assetConfig.items;
+                                                track exportItem.resourceId
+                                            ) {
+                                                <mat-option
+                                                    [value]="
+                                                        exportItem.resourceId
+                                                    "
+                                                >
+                                                    {{ exportItem.label }}
+                                                </mat-option>
+                                            }
+                                        </mat-select>
+                                    </mat-form-field>
+                                }
+                            }
+                        } @else {
+                            <sp-alert-banner
+                                type="info"
+                                [title]="'No linked resources' | translate"
+                                [description]="
+                                    'Only linked resources are part of the 
application package.'
+                                        | translate
+                                "
+                            ></sp-alert-banner>
+                        }
+                    </sp-split-section>
                 }
-                @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>
+                <sp-split-section
+                    [level]="3"
+                    [title]="'Additional exports' | translate"
+                    [subtitle]="
+                        'Selecting a document type will export all documents 
of this type'
+                            | translate
+                    "
+                >
+                    @if (getGenericStorageAppDocTypes().length > 0) {
+                        <div section-actions fxLayout="row" fxLayoutGap="10px">
+                            <button
+                                mat-button
+                                mat-flat-button
+                                color="accent"
+                                class="small-button"
+                                (click)="
+                                    selectAllItems(
+                                        getGenericStorageAppDocTypes(),
+                                        true
+                                    )
+                                "
+                            >
+                                {{ 'Select all' | translate }}
+                            </button>
+                            <button
+                                mat-button
+                                mat-flat-button
+                                class="small-button mat-basic"
+                                (click)="
+                                    selectAllItems(
+                                        getGenericStorageAppDocTypes(),
+                                        false
+                                    )
+                                "
+                            >
+                                {{ 'Select none' | translate }}
+                            </button>
+                        </div>
+                        <div fxLayout="column" class="mt-md">
+                            @for (
+                                exportItem of getGenericStorageAppDocTypes();
+                                track exportItem.resourceId
+                            ) {
+                                <mat-checkbox
+                                    [checked]="exportItem.selected"
+                                    (change)="changeItem($event, exportItem)"
+                                >
+                                    {{ exportItem.label }}
+                                </mat-checkbox>
+                            }
+                        </div>
+                    } @else {
+                        <span>{{ 'No linked resources' | translate }}</span>
                     }
-                }
+                </sp-split-section>
             </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 31b6dd1c28..0b64ce549b 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
@@ -16,10 +16,15 @@
  *
  */
 
-import { Component, Input, OnInit, inject } from '@angular/core';
-import { DialogRef } from '@streampipes/shared-ui';
+import { Component, inject, Input, OnInit } from '@angular/core';
+import {
+    DialogRef,
+    SpAlertBannerComponent,
+    SplitSectionComponent,
+} from '@streampipes/shared-ui';
 import { DataExportService } from '../data-export.service';
 import {
+    AssetExportConfiguration,
     ExportConfiguration,
     ExportItem,
 } from '@streampipes/platform-services';
@@ -27,27 +32,58 @@ import {
     FlexDirective,
     LayoutAlignDirective,
     LayoutDirective,
+    LayoutGapDirective,
 } from '@ngbracket/ngx-layout/flex';
-import { SpDataExportItemComponent } from 
'./data-export-item/data-export-item.component';
-import { GenericStorageItemsComponent } from 
'./generic-storage-items/generic-storage-items.component';
 import { MatProgressSpinner } from '@angular/material/progress-spinner';
 import { MatDivider } from '@angular/material/divider';
 import { MatButton } from '@angular/material/button';
+import { MatCheckbox, MatCheckboxChange } from '@angular/material/checkbox';
+import {
+    MatSlideToggle,
+    MatSlideToggleChange,
+} from '@angular/material/slide-toggle';
+import { FormsModule } from '@angular/forms';
+import { MatFormField } from '@angular/material/form-field';
+import { MatOption, MatSelect } from '@angular/material/select';
 import { TranslatePipe } from '@ngx-translate/core';
 
+interface SectionAssetConfiguration {
+    assetId: string;
+    assetName: string;
+    items: ExportItem[];
+}
+
+type ExportSectionKey =
+    | 'adapters'
+    | 'dashboards'
+    | 'dataViews'
+    | 'dataSources'
+    | 'dataLakeMeasures'
+    | 'files'
+    | 'pipelines'
+    | 'labels'
+    | 'sites';
+
 @Component({
     selector: 'sp-data-export-dialog',
     templateUrl: './data-export-dialog.component.html',
     imports: [
         FlexDirective,
         LayoutDirective,
-        SpDataExportItemComponent,
-        GenericStorageItemsComponent,
         LayoutAlignDirective,
+        LayoutGapDirective,
         MatProgressSpinner,
         MatDivider,
         MatButton,
+        MatCheckbox,
+        MatSlideToggle,
+        FormsModule,
+        MatFormField,
+        MatSelect,
+        MatOption,
+        SplitSectionComponent,
         TranslatePipe,
+        SpAlertBannerComponent,
     ],
 })
 export class SpDataExportDialogComponent implements OnInit {
@@ -59,29 +95,141 @@ export class SpDataExportDialogComponent implements OnInit 
{
     selectedAssets: string[];
 
     @Input()
-    referencedLabels: ExportItem[] = [];
+    referencedLabels: Record<string, ExportItem[]> = {};
 
     @Input()
-    referencedSites: ExportItem[] = [];
+    referencedSites: Record<string, ExportItem[]> = {};
+
+    exportSections = [
+        { key: 'adapters', title: 'Adapters' },
+        { key: 'dashboards', title: 'Dashboards' },
+        { key: 'dataViews', title: 'Charts' },
+        { key: 'dataSources', title: 'Data Streams' },
+        { key: 'dataLakeMeasures', title: 'Data Lake Storage' },
+        { key: 'files', title: 'Files' },
+        { key: 'pipelines', title: 'Pipelines' },
+        { key: 'labels', title: 'Labels' },
+        { key: 'sites', title: 'Sites' },
+    ] as const;
 
     preview: ExportConfiguration;
     exportInProgress = false;
+    exportAllSelections: Record<string, boolean> = {};
+    sectionAssets: Record<string, SectionAssetConfiguration[]> = {};
+    selectedItemResourceIds: Record<string, string[]> = {};
 
     ngOnInit(): void {
         this.dataExportService
             .getExportPreview(this.selectedAssets)
             .subscribe(preview => {
                 this.preview = preview;
+                this.addReferencedAssetDocuments(this.preview);
+                this.sortPreviewItems();
+                this.initializeSectionAssets();
+                this.initializeExportAllSelections();
             });
     }
 
+    getExportItems(
+        config: AssetExportConfiguration,
+        key: (typeof this.exportSections)[number]['key'],
+    ): ExportItem[] {
+        return config[key];
+    }
+
+    getAssetsForSection(key: ExportSectionKey): SectionAssetConfiguration[] {
+        return this.sectionAssets[key] ?? [];
+    }
+
+    selectAllItems(
+        items: ExportItem[],
+        select: boolean,
+        selectionKey?: string,
+    ): void {
+        items.forEach(item => (item.selected = select));
+
+        if (selectionKey) {
+            this.selectedItemResourceIds[selectionKey] = select
+                ? items.map(item => item.resourceId)
+                : [];
+        }
+    }
+
+    selectAllItemsForAsset(
+        key: ExportSectionKey,
+        assetConfig: SectionAssetConfiguration,
+        select: boolean,
+    ): void {
+        this.selectAllItems(
+            assetConfig.items,
+            select,
+            this.getExportAllSelectionKey(key, assetConfig),
+        );
+    }
+
+    isExportAllSelected(
+        key: ExportSectionKey,
+        assetConfig: SectionAssetConfiguration,
+    ): boolean {
+        return this.exportAllSelections[
+            this.getExportAllSelectionKey(key, assetConfig)
+        ];
+    }
+
+    changeExportAllSelection(
+        key: ExportSectionKey,
+        assetConfig: SectionAssetConfiguration,
+        event: MatSlideToggleChange,
+    ): void {
+        const selectionKey = this.getExportAllSelectionKey(key, assetConfig);
+        this.exportAllSelections[selectionKey] = event.checked;
+
+        if (event.checked) {
+            this.selectAllItems(assetConfig.items, true, selectionKey);
+        }
+    }
+
+    getGenericStorageAppDocTypes(): ExportItem[] {
+        return this.preview?.genericStorageAppDocTypes ?? [];
+    }
+
+    getSelectedItemResourceIds(
+        key: ExportSectionKey,
+        assetConfig: SectionAssetConfiguration,
+    ): string[] {
+        return (
+            this.selectedItemResourceIds[
+                this.getExportAllSelectionKey(key, assetConfig)
+            ] ?? []
+        );
+    }
+
+    updateSelectedItems(
+        key: ExportSectionKey,
+        assetConfig: SectionAssetConfiguration,
+        selectedResourceIds: string[],
+    ): void {
+        const selectionKey = this.getExportAllSelectionKey(key, assetConfig);
+        this.selectedItemResourceIds[selectionKey] = selectedResourceIds ?? [];
+
+        const items = assetConfig.items;
+        items.forEach(exportItem => {
+            exportItem.selected = (selectedResourceIds ?? []).includes(
+                exportItem.resourceId,
+            );
+        });
+    }
+
+    changeItem(event: MatCheckboxChange, exportItem: ExportItem): void {
+        exportItem.selected = event.checked;
+    }
+
     close(): void {
         this.dialogRef.close();
     }
 
     generateDownloadPackage(): void {
         this.exportInProgress = true;
-        this.addReferencedAssetDocuments(this.preview);
         this.dataExportService.triggerExport(this.preview).subscribe(result => 
{
             this.downloadFile(result);
         });
@@ -103,19 +251,66 @@ export class SpDataExportDialogComponent implements 
OnInit {
         this.dialogRef.close();
     }
 
-    private addReferencedAssetDocuments(preview: ExportConfiguration): void {
-        const firstAssetExportConfig = preview.assetExportConfiguration?.[0];
+    private initializeExportAllSelections(): void {
+        this.exportSections.forEach(section => {
+            this.getAssetsForSection(section.key).forEach(assetConfig => {
+                const selectionKey = this.getExportAllSelectionKey(
+                    section.key,
+                    assetConfig,
+                );
+                this.exportAllSelections[selectionKey] = true;
+                this.selectAllItems(assetConfig.items, true, selectionKey);
+            });
+        });
+    }
+
+    private initializeSectionAssets(): void {
+        this.sectionAssets = {};
+
+        this.exportSections.forEach(section => {
+            this.sectionAssets[section.key] =
+                this.preview.assetExportConfiguration
+                    .map(config => ({
+                        assetId: config.assetId,
+                        assetName: config.assetName,
+                        items: this.getExportItems(config, section.key),
+                    }))
+                    .filter(config => config.items.length > 0);
+        });
+    }
+
+    private sortPreviewItems(): void {
+        this.preview.assetExportConfiguration.forEach(config => {
+            this.exportSections.forEach(section => {
+                this.sortExportItems(config[section.key]);
+            });
+        });
+
+        this.sortExportItems(this.preview.genericStorageAppDocTypes);
+    }
 
-        if (firstAssetExportConfig) {
-            firstAssetExportConfig.labels = this.mergeExportItems(
-                firstAssetExportConfig.labels,
-                this.referencedLabels,
+    private sortExportItems(items: ExportItem[]): void {
+        items.sort((left, right) => left.label.localeCompare(right.label));
+    }
+
+    private getExportAllSelectionKey(
+        key: ExportSectionKey,
+        assetConfig: SectionAssetConfiguration,
+    ): string {
+        return `${key}::${assetConfig.assetId}`;
+    }
+
+    private addReferencedAssetDocuments(preview: ExportConfiguration): void {
+        preview.assetExportConfiguration?.forEach(assetExportConfig => {
+            assetExportConfig.labels = this.mergeExportItems(
+                assetExportConfig.labels,
+                this.referencedLabels[assetExportConfig.assetId] ?? [],
             );
-            firstAssetExportConfig.sites = this.mergeExportItems(
-                firstAssetExportConfig.sites,
-                this.referencedSites,
+            assetExportConfig.sites = this.mergeExportItems(
+                assetExportConfig.sites,
+                this.referencedSites[assetExportConfig.assetId] ?? [],
             );
-        }
+        });
     }
 
     private mergeExportItems(
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 9d64939a1f..aaef675895 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
@@ -169,17 +169,28 @@
                                             | translate
                                     }}
                                 </mat-checkbox>
-                                <div class="warning-box">
-                                    <mat-checkbox
-                                        [(ngModel)]="
-                                            
importConfiguration.overwriteExistingDocuments
-                                        "
-                                        >{{
-                                            'Update/overwrite existing 
documents with the same ID (operation may break things)'
+                                <mat-checkbox
+                                    [(ngModel)]="
+                                        
importConfiguration.overwriteExistingDocuments
+                                    "
+                                    >{{
+                                        'Replace existing documents with the 
same ID'
+                                            | translate
+                                    }}
+                                </mat-checkbox>
+                                @if (
+                                    
importConfiguration.overwriteExistingDocuments
+                                ) {
+                                    <sp-alert-banner
+                                        type="warning"
+                                        [title]="'Replace existing' | 
translate"
+                                        [description]="
+                                            'Replacing existing documents can 
cause side-effects for resources that are related to the replaced document. Use 
this feature with caution.'
                                                 | translate
-                                        }}
-                                    </mat-checkbox>
-                                </div>
+                                        "
+                                    >
+                                    </sp-alert-banner>
+                                }
                             </div>
                         }
                     </sp-split-section>

Reply via email to