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>