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

zehnder 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 0d24ef4786 Add related assets to adapter during adapter generation 
process  (#3804)
0d24ef4786 is described below

commit 0d24ef47862da6b7796d457d4ee24e10738baaf2
Author: Jacqueline Höllig <[email protected]>
AuthorDate: Mon Oct 6 14:58:29 2025 +0200

    Add related assets to adapter during adapter generation process  (#3804)
---
 .../api/IDataExplorerSchemaManagement.java         |   3 +
 .../dataexplorer/DataExplorerSchemaManagement.java |  58 ++++----
 .../impl/datalake/DataLakeMeasureResource.java     |  43 +++---
 ui/deployment/i18n/en.json                         |   7 +-
 .../src/lib/apis/datalake-rest.service.ts          |   6 +
 .../src/lib/model/assets/asset.model.ts            |  16 +++
 .../adapter-asset-configuration.component.html     |  72 ++++++++++
 .../adapter-asset-configuration.component.scss     | 123 ++++++++++++++++
 .../adapter-asset-configuration.component.ts       | 133 ++++++++++++++++++
 .../adapter-configuration.component.html           |   1 +
 .../adapter-configuration.component.ts             |   6 +-
 .../start-adapter-configuration.component.html     |  58 +++++---
 .../start-adapter-configuration.component.ts       |  26 +++-
 ui/src/app/connect/connect.module.ts               |   7 +-
 .../adapter-started-dialog.component.html          |   1 +
 .../adapter-started-dialog.component.ts            | 154 +++++++++++++++++++-
 .../adapter-started-success.component.html         |  22 +++
 .../adapter-started-success.component.ts           |   3 +
 .../adapter-asset-configuration.service.ts         | 156 +++++++++++++++++++++
 19 files changed, 811 insertions(+), 84 deletions(-)

diff --git 
a/streampipes-data-explorer-api/src/main/java/org/apache/streampipes/dataexplorer/api/IDataExplorerSchemaManagement.java
 
b/streampipes-data-explorer-api/src/main/java/org/apache/streampipes/dataexplorer/api/IDataExplorerSchemaManagement.java
index a210b515b3..c171469c52 100644
--- 
a/streampipes-data-explorer-api/src/main/java/org/apache/streampipes/dataexplorer/api/IDataExplorerSchemaManagement.java
+++ 
b/streampipes-data-explorer-api/src/main/java/org/apache/streampipes/dataexplorer/api/IDataExplorerSchemaManagement.java
@@ -21,6 +21,7 @@ package org.apache.streampipes.dataexplorer.api;
 import org.apache.streampipes.model.datalake.DataLakeMeasure;
 
 import java.util.List;
+import java.util.Optional;
 
 public interface IDataExplorerSchemaManagement {
 
@@ -28,6 +29,8 @@ public interface IDataExplorerSchemaManagement {
 
   DataLakeMeasure getById(String elementId);
 
+  Optional<DataLakeMeasure> getExistingMeasureByName(String measureName);
+
   DataLakeMeasure createOrUpdateMeasurement(DataLakeMeasure measure);
 
   void deleteMeasurement(String elementId);
diff --git 
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagement.java
 
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagement.java
index ef3db4c7d3..718dcba4af 100644
--- 
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagement.java
+++ 
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagement.java
@@ -51,7 +51,8 @@ public class DataExplorerSchemaManagement implements 
IDataExplorerSchemaManageme
   }
 
   /**
-   * For new measurements an entry is generated in the database. For existing 
measurements the schema is updated
+   * For new measurements an entry is generated in the database. For existing
+   * measurements the schema is updated
    * according to the update strategy defined by the measurement.
    */
   @Override
@@ -75,28 +76,28 @@ public class DataExplorerSchemaManagement implements 
IDataExplorerSchemaManageme
    */
   private void handleExistingMeasurement(
       DataLakeMeasure measure,
-      DataLakeMeasure existingMeasure
-  ) {
+      DataLakeMeasure existingMeasure) {
     measure.setElementId(existingMeasure.getElementId());
     if 
(DataLakeMeasureSchemaUpdateStrategy.UPDATE_SCHEMA.equals(measure.getSchemaUpdateStrategy()))
 {
       // For the update schema strategy the old schema is overwritten with the 
new one
       updateMeasurement(measure);
     } else {
-      // For the extent existing schema strategy the old schema is merged with 
the new one
+      // For the extent existing schema strategy the old schema is merged with 
the new
+      // one
       unifyEventSchemaAndUpdateMeasure(measure, existingMeasure);
     }
   }
 
-
   /**
    * Returns the existing measure that has the provided measure name
    */
-  private Optional<DataLakeMeasure> getExistingMeasureByName(String 
measureName) {
+  @Override
+  public Optional<DataLakeMeasure> getExistingMeasureByName(String 
measureName) {
     return dataLakeStorage.findAll()
-                          .stream()
-                          .filter(m -> m.getMeasureName()
-                                        .equals(measureName))
-                          .findFirst();
+        .stream()
+        .filter(m -> m.getMeasureName()
+            .equals(measureName))
+        .findFirst();
   }
 
   private static void setDefaultUpdateStrategyIfNoneProvided(DataLakeMeasure 
measure) {
@@ -117,16 +118,15 @@ public class DataExplorerSchemaManagement implements 
IDataExplorerSchemaManageme
   @Override
   public boolean deleteMeasurementByName(String measureName) {
     var measureToDeleteOpt = dataLakeStorage.findAll()
-                                            .stream()
-                                            .filter(measurement -> 
measurement.getMeasureName()
-                                                                               
.equals(measureName))
-                                            .findFirst();
+        .stream()
+        .filter(measurement -> measurement.getMeasureName()
+            .equals(measureName))
+        .findFirst();
 
     return measureToDeleteOpt.map(measure -> {
       dataLakeStorage.deleteElementById(measure.getElementId());
       return true;
-    }
-    ).orElse(false);
+    }).orElse(false);
   }
 
   @Override
@@ -146,16 +146,15 @@ public class DataExplorerSchemaManagement implements 
IDataExplorerSchemaManageme
   }
 
   /**
-   * First the event schemas of the measurements are merged and then the 
measure is updated in the database
+   * First the event schemas of the measurements are merged and then the 
measure
+   * is updated in the database
    */
   private void unifyEventSchemaAndUpdateMeasure(
       DataLakeMeasure measure,
-      DataLakeMeasure existingMeasure
-  ) {
+      DataLakeMeasure existingMeasure) {
     var properties = getUnifiedEventProperties(
         existingMeasure,
-        measure
-    );
+        measure);
 
     measure
         .getEventSchema()
@@ -170,17 +169,15 @@ public class DataExplorerSchemaManagement implements 
IDataExplorerSchemaManageme
    */
   private List<EventProperty> getUnifiedEventProperties(
       DataLakeMeasure measure1,
-      DataLakeMeasure measure2
-  ) {
-// Combine the event properties from both measures into a single Stream
+      DataLakeMeasure measure2) {
+    // Combine the event properties from both measures into a single Stream
     var allMeasurementProperties = Stream.concat(
         measure1.getEventSchema()
-                .getEventProperties()
-                .stream(),
+            .getEventProperties()
+            .stream(),
         measure2.getEventSchema()
-                .getEventProperties()
-                .stream()
-    );
+            .getEventProperties()
+            .stream());
 
     // Filter event properties by removing duplicate runtime names
     // If there are duplicate keys, choose the first occurrence
@@ -188,8 +185,7 @@ public class DataExplorerSchemaManagement implements 
IDataExplorerSchemaManageme
         .collect(Collectors.toMap(
             EventProperty::getRuntimeName,
             Function.identity(),
-            (eventProperty, eventProperty2) -> eventProperty
-        ))
+            (eventProperty, eventProperty2) -> eventProperty))
         .values();
     return new ArrayList<>(unifiedEventProperties);
   }
diff --git 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeMeasureResource.java
 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeMeasureResource.java
index 97a3a4f0ea..32d6a26293 100644
--- 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeMeasureResource.java
+++ 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeMeasureResource.java
@@ -49,42 +49,33 @@ public class DataLakeMeasureResource extends 
AbstractAuthGuardedRestResource {
 
   public DataLakeMeasureResource() {
     this.dataLakeMeasureManagement = new 
DataExplorerDispatcher().getDataExplorerManager()
-                                                                 
.getSchemaManagement();
+        .getSchemaManagement();
   }
 
-  @PostMapping(
-      produces = MediaType.APPLICATION_JSON_VALUE,
-      consumes = MediaType.APPLICATION_JSON_VALUE
-  )
+  @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = 
MediaType.APPLICATION_JSON_VALUE)
   public ResponseEntity<DataLakeMeasure> addDataLake(@RequestBody 
DataLakeMeasure dataLakeMeasure) {
     DataLakeMeasure result = 
this.dataLakeMeasureManagement.createOrUpdateMeasurement(dataLakeMeasure);
     return ok(result);
   }
 
   /**
-   * Handles HTTP GET requests to retrieve the entry counts of specified 
measurements.
+   * Handles HTTP GET requests to retrieve the entry counts of specified
+   * measurements.
    *
    * @param measurementNames A list of measurement names to return the count.
-   * @return A ResponseEntity containing a map of measurement names and their 
corresponding entry counts.
+   * @return A ResponseEntity containing a map of measurement names and their
+   *         corresponding entry counts.
    */
-  @Operation(
-      summary = "Retrieve measurement counts",
-      description = "Retrieves the entry counts for the specified measurements 
from the data lake.")
-  @GetMapping(
-      path = "/count",
-      produces = MediaType.APPLICATION_JSON_VALUE)
+  @Operation(summary = "Retrieve measurement counts", description = "Retrieves 
the entry counts for the specified measurements from the data lake.")
+  @GetMapping(path = "/count", produces = MediaType.APPLICATION_JSON_VALUE)
   public ResponseEntity<Map<String, Integer>> getEntryCountsOfMeasurments(
-      @Parameter(description = "A list of measurement names to return the 
count.")
-      @RequestParam(value = "measurementNames")
-      List<String> measurementNames
-  ) {
+      @Parameter(description = "A list of measurement names to return the 
count.") @RequestParam(value = "measurementNames") List<String> 
measurementNames) {
     var allMeasurements = this.dataLakeMeasureManagement.getAllMeasurements();
     var result = new DataExplorerDispatcher()
         .getDataExplorerManager()
         .getMeasurementCounter(
             allMeasurements,
-            measurementNames
-        )
+            measurementNames)
         .countMeasurementSizes();
     return ok(result);
   }
@@ -99,11 +90,20 @@ public class DataLakeMeasureResource extends 
AbstractAuthGuardedRestResource {
     }
   }
 
+  @GetMapping(path = "byName/{measureName}", produces = 
MediaType.APPLICATION_JSON_VALUE)
+  public ResponseEntity<?> getDataLakeMeasureName(@PathVariable("measureName") 
String measureName) {
+    var measure = 
this.dataLakeMeasureManagement.getExistingMeasureByName(measureName);
+    if (Objects.nonNull(measure)) {
+      return ok(measure);
+    } else {
+      return notFound();
+    }
+  }
+
   @PutMapping(path = "{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
   public ResponseEntity<?> updateDataLakeMeasure(
       @PathVariable("id") String elementId,
-      @RequestBody DataLakeMeasure measure
-  ) {
+      @RequestBody DataLakeMeasure measure) {
     if (elementId.equals(measure.getElementId())) {
       try {
         this.dataLakeMeasureManagement.updateMeasurement(measure);
@@ -115,7 +115,6 @@ public class DataLakeMeasureResource extends 
AbstractAuthGuardedRestResource {
     return badRequest();
   }
 
-
   @DeleteMapping(path = "{id}", produces = MediaType.APPLICATION_JSON_VALUE)
   public ResponseEntity<?> deleteDataLakeMeasure(@PathVariable("id") String 
elementId) {
     try {
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index ec3ee1c10a..56d251824c 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -482,5 +482,10 @@
   "Error Details": null,
   "Resources": null,
   "All {{allResourcesAlias}}": "All {{allResourcesAlias}}",
-  "{{ widgetTitle }} Clone": "{{ widgetTitle }} Clone"
+  "{{ widgetTitle }} Clone": "{{ widgetTitle }} Clone",
+  "Your {{assetTypes}} were successfully added to {{assetIds}}.": "Your 
{{assetTypes}} were successfully added to {{assetIds}}.",
+  "Your {{assetTypes}} were successfully deleted from {{assetIds}}.": "Your 
{{assetTypes}} were successfully deleted from {{assetIds}}.",
+  "Starting adapter {{adapterName}}": "Starting adapter {{adapterName}}",
+  "Creating adapter {{adapterName}}": "Creating adapter {{adapterName}}",
+  "Updating adapter {{adapterName}}": "Updating adapter {{adapterName}}"
 }
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts
 
b/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts
index 5009e20a18..fc47f133ea 100644
--- 
a/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts
+++ 
b/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts
@@ -75,6 +75,12 @@ export class DatalakeRestService {
             .pipe(map(res => res as DataLakeMeasure));
     }
 
+    getMeasurementByName(name: String): Observable<DataLakeMeasure> {
+        return this.http
+            .get(`${this.dataLakeMeasureUrl}/byName/${name}`)
+            .pipe(map(res => res as DataLakeMeasure));
+    }
+
     performMultiQuery(
         queryParams: DatalakeQueryParameters[],
     ): Observable<SpQueryResult[]> {
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/model/assets/asset.model.ts 
b/ui/projects/streampipes/platform-services/src/lib/model/assets/asset.model.ts
index 084b874214..b64f794ec6 100644
--- 
a/ui/projects/streampipes/platform-services/src/lib/model/assets/asset.model.ts
+++ 
b/ui/projects/streampipes/platform-services/src/lib/model/assets/asset.model.ts
@@ -43,6 +43,14 @@ export interface AssetLink {
     navigationActive: boolean;
 }
 
+export interface LinkageData {
+    //Data Model to extract AssetLinks from the UI
+    name: string;
+    id: string;
+    type: string;
+    selected?: boolean | null;
+}
+
 export interface Isa95TypeDesc {
     label: string;
     type: Isa95Type;
@@ -94,6 +102,14 @@ export interface SpAssetModel extends SpAsset {
     removable: boolean;
 }
 
+export interface SpAssetTreeNode {
+    assetId: string;
+    assetName: string;
+    assets?: SpAssetTreeNode[];
+    spAssetModelId: string;
+    flattenPath: any[];
+}
+
 export type Isa95Type =
     | 'PROCESS_CELL'
     | 'PRODUCTION_UNIT'
diff --git 
a/ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.html
 
b/ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.html
new file mode 100644
index 0000000000..765dcb4975
--- /dev/null
+++ 
b/ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.html
@@ -0,0 +1,72 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~    http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  -->
+<div *ngIf="assetsData?.length">
+    <mat-tree
+        [dataSource]="dataSource"
+        [treeControl]="treeControl"
+        class="asset-tree"
+    >
+        <!-- Parent Node Definition -->
+        <mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild">
+            <div
+                class="mat-tree-node"
+                (click)="onAssetSelect(node)"
+                [class.selected-node]="isSelected(node)"
+            >
+                <button
+                    mat-icon-button
+                    matTreeNodeToggle
+                    [attr.aria-label]="'Toggle ' + node.assetName"
+                >
+                    <mat-icon>{{
+                        treeControl.isExpanded(node)
+                            ? 'expand_more'
+                            : 'chevron_right'
+                    }}</mat-icon>
+                </button>
+                <span>{{ node.assetName }}</span>
+            </div>
+            <div *ngIf="treeControl.isExpanded(node)" role="group">
+                <ng-container matTreeNodeOutlet></ng-container>
+            </div>
+        </mat-nested-tree-node>
+
+        <!-- Leaf Node Definition (no children) -->
+        <mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
+            <div
+                class="mat-tree-node"
+                (click)="onAssetSelect(node)"
+                [class.selected-node]="isSelected(node)"
+            >
+                <button
+                    mat-icon-button
+                    matTreeNodeToggle
+                    [attr.aria-label]="'Toggle ' + node.assetName"
+                >
+                    <mat-icon></mat-icon>
+                </button>
+                <span>{{ node.assetName }}</span>
+            </div>
+        </mat-tree-node>
+    </mat-tree>
+</div>
+
+<!-- If no assets available -->
+<div *ngIf="!assetsData?.length">
+    <p>No assets available</p>
+</div>
diff --git 
a/ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.scss
 
b/ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.scss
new file mode 100644
index 0000000000..5a515d41e3
--- /dev/null
+++ 
b/ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.scss
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+.components-container {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    padding: 10px;
+}
+
+.component-row {
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+    gap: 20px;
+}
+
+mat-form-field {
+    flex: 1;
+    margin-right: 10px;
+}
+
+mat-checkbox {
+    margin-right: 20px;
+}
+.asset-tree {
+    font-family: 'Roboto', sans-serif;
+    padding-left: 10px;
+    margin-left: 10px;
+}
+
+.mat-tree-node {
+    display: flex;
+    align-items: center;
+    padding-left: 10px;
+}
+
+.mat-nested-tree-node {
+    display: block;
+    margin-left: 8px;
+}
+
+button[mat-icon-button] {
+    margin-right: 8px;
+}
+
+mat-icon {
+    font-size: 18px;
+    color: #3f51b5;
+}
+
+.mat-tree-node span {
+    font-weight: bold;
+}
+
+.mat-tree-node:hover {
+    background-color: #e8e8e8;
+}
+
+.mat-nested-tree-node span {
+    margin-left: 8px;
+}
+
+.mat-nested-tree-node mat-icon {
+    margin-left: 16px;
+}
+
+.transparent-btn {
+    background: transparent;
+    border: none;
+    padding: 0;
+    margin-left: 10px;
+    cursor: pointer;
+}
+
+.transparent-btn:hover {
+    background-color: rgba(63, 81, 181, 0.1);
+}
+
+.transparent-btn mat-icon {
+    font-size: 18px;
+    opacity: 0.4;
+}
+
+.transparent-btn:hover mat-icon {
+    opacity: 1;
+}
+
+.selected-node {
+    background-color: #e0f7fa;
+    border-radius: 4px;
+    padding: 5px;
+    font-family: 'Roboto-Bold', serif;
+    font-weight: bolder;
+}
+
+.selected-node:hover {
+    background-color: #b2ebf2;
+}
+
+.placeholder-icon {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.placeholder-icon .mat-icon.invisible {
+    visibility: hidden;
+}
diff --git 
a/ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.ts
 
b/ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.ts
new file mode 100644
index 0000000000..f48e53c270
--- /dev/null
+++ 
b/ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.ts
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
+import { NestedTreeControl } from '@angular/cdk/tree';
+import { MatTreeNestedDataSource } from '@angular/material/tree';
+import {
+    AssetManagementService,
+    LinkageData,
+    SpAssetModel,
+    AssetLinkType,
+    SpAsset,
+    SpAssetTreeNode,
+} from '@streampipes/platform-services';
+import { MatStepper } from '@angular/material/stepper';
+import { Observable } from 'rxjs';
+
+@Component({
+    selector: 'sp-adapter-asset-configuration',
+    templateUrl: './adapter-asset-configuration.component.html',
+    styleUrls: ['./adapter-asset-configuration.component.scss'],
+    standalone: false,
+})
+export class AdapterAssetConfigurationComponent implements OnInit {
+    @Input() linkageData: LinkageData[] = [];
+    @Input() stepper: MatStepper;
+
+    @Output() adapterStartedEmitter: EventEmitter<void> =
+        new EventEmitter<void>();
+
+    @Output() selectedAssetsChange = new EventEmitter<SpAssetTreeNode[]>();
+
+    treeControl: NestedTreeControl<SpAssetTreeNode>;
+    dataSource: MatTreeNestedDataSource<SpAssetTreeNode>;
+
+    treeDropdownOpen = false;
+
+    assetsData: SpAssetTreeNode[] = [];
+    currentAsset: SpAssetModel;
+    assetLinkTypes: AssetLinkType[] = [];
+    assetLinksLoaded = false;
+    updateObservable: Observable<SpAssetModel>;
+    selectedAssets: SpAssetTreeNode[] = [];
+
+    constructor(private assetService: AssetManagementService) {
+        this.treeControl = new NestedTreeControl<SpAssetTreeNode>(
+            node => node.assets,
+        );
+        this.dataSource = new MatTreeNestedDataSource<SpAssetTreeNode>();
+    }
+
+    hasChild = (_: number, node: any) =>
+        !!node.assets && node.assets.length > 0;
+
+    toggleTreeDropdown() {
+        this.treeDropdownOpen = !this.treeDropdownOpen;
+    }
+
+    onAssetSelect(node: SpAssetTreeNode): void {
+        const index = this.selectedAssets.findIndex(
+            asset => asset.assetId === node.assetId,
+        );
+
+        if (index > -1) {
+            this.selectedAssets.splice(index, 1);
+        } else {
+            this.selectedAssets.push(node);
+        }
+
+        this.selectedAssetsChange.emit(this.selectedAssets);
+    }
+
+    isSelected(node: SpAssetTreeNode): boolean {
+        return this.selectedAssets.some(
+            asset => asset.assetId === node.assetId,
+        );
+    }
+
+    ngOnInit(): void {
+        this.loadAssets();
+    }
+
+    private loadAssets(): void {
+        this.assetService.getAllAssets().subscribe({
+            next: assets => {
+                this.assetsData = this.mapAssets(assets);
+                this.dataSource.data = this.assetsData;
+            },
+        });
+    }
+    private mapAssets(
+        apiAssets: SpAsset[],
+        parentId: string = '',
+        index: any[] = [],
+    ): SpAssetTreeNode[] {
+        return apiAssets.map((asset, assetIndex) => {
+            const currentPath = [...index, assetIndex];
+            let flattenedPath = [];
+
+            if (asset['_id']) {
+                parentId = asset['_id'];
+                flattenedPath = [parentId, ...currentPath];
+            } else {
+                flattenedPath = [...currentPath];
+            }
+            const flattenedPathCopy = [...flattenedPath];
+            return {
+                spAssetModelId: parentId,
+                assetId: asset.assetId,
+                assetName: asset.assetName,
+                flattenPath: flattenedPath,
+                assets: asset.assets
+                    ? this.mapAssets(asset.assets, parentId, flattenedPathCopy)
+                    : [],
+            };
+        });
+    }
+}
diff --git 
a/ui/src/app/connect/components/adapter-configuration/adapter-configuration.component.html
 
b/ui/src/app/connect/components/adapter-configuration/adapter-configuration.component.html
index 5037978a73..ab9892eaff 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/adapter-configuration.component.html
+++ 
b/ui/src/app/connect/components/adapter-configuration/adapter-configuration.component.html
@@ -81,6 +81,7 @@
                     [adapterDescription]="adapter"
                     [eventSchema]="adapter.dataStream.eventSchema"
                     [isEditMode]="isEditMode"
+                    [stepper]="stepper"
                     (removeSelectionEmitter)="removeSelection()"
                     (goBackEmitter)="goBack()"
                     (adapterStartedEmitter)="adapterWasStarted()"
diff --git 
a/ui/src/app/connect/components/adapter-configuration/adapter-configuration.component.ts
 
b/ui/src/app/connect/components/adapter-configuration/adapter-configuration.component.ts
index a66d30e677..3f99e7a373 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/adapter-configuration.component.ts
+++ 
b/ui/src/app/connect/components/adapter-configuration/adapter-configuration.component.ts
@@ -19,7 +19,10 @@
 import { Component, Input, OnInit, ViewChild } from '@angular/core';
 import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
 import { MatStepper } from '@angular/material/stepper';
-import { AdapterDescription } from '@streampipes/platform-services';
+import {
+    AdapterDescription,
+    LinkageData,
+} from '@streampipes/platform-services';
 import { ShepherdService } from '../../../services/tour/shepherd.service';
 import { EventSchemaComponent } from 
'./schema-editor/event-schema/event-schema.component';
 import { TransformationRuleService } from 
'../../services/transformation-rule.service';
@@ -41,6 +44,7 @@ export class AdapterConfigurationComponent implements OnInit {
     @Input() adapter: AdapterDescription;
     @Input() isEditMode;
 
+    linkageData: LinkageData[];
     myStepper: MatStepper;
     parentForm: UntypedFormGroup;
 
diff --git 
a/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.html
 
b/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.html
index 7ccc9dab2c..fe2fd92658 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.html
+++ 
b/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.html
@@ -104,8 +104,25 @@
         </sp-adapter-options-panel>
 
         <sp-adapter-options-panel
-            optionTitle="Remove Duplicates"
-            optionDescription="Avoid duplicated events within a certain time 
interval"
+            [optionTitle]="'Add to Asset' | translate"
+            [optionDescription]="'Add Adapter to an existing Asset' | 
translate"
+            optionIcon="precision_manufacturing"
+            dataCy="show-asset-checkbox"
+            (optionSelectedEmitter)="showAsset = $event"
+        >
+            <sp-adapter-asset-configuration
+                *ngIf="showAsset"
+                (selectedAssetsChange)="onSelectedAssetsChange($event)"
+            >
+            </sp-adapter-asset-configuration>
+        </sp-adapter-options-panel>
+
+        <sp-adapter-options-panel
+            [optionTitle]="'Remove Duplicates' | translate"
+            [optionDescription]="
+                'Avoid duplicated events within a certain time interval'
+                    | translate
+            "
             optionIcon="cleaning_services"
             dataCy="connect-remove-duplicates-box"
             [isChecked]="removeDuplicates"
@@ -120,7 +137,7 @@
                     matInput
                     id="input-removeDuplicatesTime"
                     [ngModelOptions]="{ standalone: true }"
-                    placeholder="Remove Duplicates Time Window"
+                    [placeholder]="'Remove Duplicates Time Window' | translate"
                     [(ngModel)]="removeDuplicatesTime"
                     data-cy="connect-remove-duplicates-input"
                 />
@@ -128,8 +145,11 @@
         </sp-adapter-options-panel>
 
         <sp-adapter-options-panel
-            optionTitle="Reduce event rate"
-            optionDescription="Send maximum one event in the specified time 
window"
+            [optionTitle]="'Reduce event rate' | translate"
+            [optionDescription]="
+                'Send maximum one event in the specified time window'
+                    | translate
+            "
             optionIcon="speed"
             dataCy="connect-reduce-event-rate-box"
             [isChecked]="eventRateReduction"
@@ -146,13 +166,13 @@
                     id="input-evenRateTime"
                     [ngModelOptions]="{ standalone: true }"
                     [(ngModel)]="eventRateTime"
-                    placeholder="Time Window (Milliseconds)"
+                    [placeholder]="'Time Window (Milliseconds)' | translate"
                     matTooltipPosition="above"
                     data-cy="connect-reduce-event-input"
                 />
             </mat-form-field>
             <mat-form-field *ngIf="eventRateReduction" color="accent">
-                <mat-label>Event Aggregation</mat-label>
+                <mat-label>{{ 'Event Aggregation' | translate }}</mat-label>
                 <mat-select
                     [(ngModel)]="eventRateMode"
                     [ngModelOptions]="{ standalone: true }"
@@ -171,15 +191,18 @@
 
         <!-- Start pipeline template to store raw events in data lake -->
         <sp-adapter-options-panel
-            optionTitle="Persist events"
-            optionDescription="Store all events of this source in the internal 
data store"
+            [optionTitle]="'Persist events' | translate"
+            [optionDescription]="
+                'Store all events of this source in the internal data store'
+                    | translate
+            "
             optionIcon="save"
             dataCy="sp-store-in-datalake"
             *ngIf="!isEditMode"
             (optionSelectedEmitter)="handlePersistOption($event)"
         >
             <mat-form-field *ngIf="saveInDataLake" color="accent" 
class="mt-10">
-                <mat-label>Select Time Field</mat-label>
+                <mat-label>{{ 'Select Time Field' | translate }}</mat-label>
                 <mat-select
                     [(ngModel)]="dataLakeTimestampField"
                     [ngModelOptions]="{ standalone: true }"
@@ -201,8 +224,11 @@
         </sp-adapter-options-panel>
 
         <sp-adapter-options-panel
-            optionTitle="Show code"
-            optionDescription="Show code to programmatically deploy this 
adapter over the API"
+            [optionTitle]="'Show code' | translate"
+            [optionDescription]="
+                'Show code to programmatically deploy this adapter over the 
API'
+                    | translate
+            "
             optionIcon="code"
             dataCy="show-code-checkbox"
             (optionSelectedEmitter)="showCode = $event"
@@ -219,7 +245,7 @@
 
     <div fxLayoutAlign="end" style="margin-top: 10px">
         <button class="mat-basic" mat-flat-button (click)="removeSelection()">
-            Cancel
+            {{ 'Cancel' | translate }}
         </button>
         <button
             style="margin-left: 10px"
@@ -227,7 +253,7 @@
             mat-flat-button
             (click)="goBack()"
         >
-            Back
+            {{ 'Back' | translate }}
         </button>
         <button
             *ngIf="!isEditMode"
@@ -239,7 +265,7 @@
             mat-button
             style="margin-left: 10px"
         >
-            Start Adapter
+            {{ 'Start Adapter' | translate }}
         </button>
         <button
             *ngIf="isEditMode"
@@ -251,7 +277,7 @@
             mat-button
             style="margin-left: 10px"
         >
-            Update Adapter
+            {{ 'Update Adapter' | translate }}
         </button>
     </div>
 </div>
diff --git 
a/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.ts
 
b/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.ts
index 97b92372aa..476877c11c 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.ts
+++ 
b/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.ts
@@ -16,9 +16,18 @@
  *
  */
 
-import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import {
+    Component,
+    EventEmitter,
+    Input,
+    OnInit,
+    Output,
+    SimpleChanges,
+    OnChanges,
+} from '@angular/core';
 import {
     AdapterDescription,
+    SpAssetTreeNode,
     EventRateTransformationRuleDescription,
     EventSchema,
     RemoveDuplicatesTransformationRuleDescription,
@@ -58,6 +67,8 @@ export class StartAdapterConfigurationComponent implements 
OnInit {
 
     @Input() isEditMode: boolean;
 
+    @Input() stepper: MatStepper;
+
     /**
      * Cancels the adapter configuration process
      */
@@ -69,7 +80,6 @@ export class StartAdapterConfigurationComponent implements 
OnInit {
      */
     @Output() adapterStartedEmitter: EventEmitter<void> =
         new EventEmitter<void>();
-
     /**
      * Go to next configuration step when this is complete
      */
@@ -98,6 +108,8 @@ export class StartAdapterConfigurationComponent implements 
OnInit {
 
     startAdapterNow = true;
     showCode = false;
+    showAsset = false;
+    selectedAssets = [];
 
     constructor(
         private dialogService: DialogService,
@@ -108,7 +120,6 @@ export class StartAdapterConfigurationComponent implements 
OnInit {
     ) {}
 
     ngOnInit(): void {
-        // initialize form for validation
         this.startAdapterForm = this._formBuilder.group({});
         this.startAdapterForm.addControl(
             'adapterName',
@@ -199,15 +210,20 @@ export class StartAdapterConfigurationComponent 
implements OnInit {
                 dataLakeTimestampField: this.dataLakeTimestampField,
                 editMode: false,
                 startAdapterNow: this.startAdapterNow,
+                selectedAssets: this.selectedAssets,
             },
         });
-        this.shepherdService.trigger('adapter-settings-adapter-started');
-
+        const dialogInstance =
+            dialogRef.componentInstance as unknown as AdapterStartedDialog;
         dialogRef.afterClosed().subscribe(() => {
             this.adapterStartedEmitter.emit();
         });
     }
 
+    onSelectedAssetsChange(updatedAssets: SpAssetTreeNode[]): void {
+        this.selectedAssets = updatedAssets;
+    }
+
     private checkAndApplyStreamRules(): void {
         if (this.removeDuplicates) {
             const removeDuplicates: 
RemoveDuplicatesTransformationRuleDescription =
diff --git a/ui/src/app/connect/connect.module.ts 
b/ui/src/app/connect/connect.module.ts
index e973068aeb..d6b5701047 100644
--- a/ui/src/app/connect/connect.module.ts
+++ b/ui/src/app/connect/connect.module.ts
@@ -52,6 +52,7 @@ import { ErrorMessageComponent } from 
'./components/adapter-configuration/schema
 import { LoadingMessageComponent } from 
'./components/adapter-configuration/schema-editor/loading-message/loading-message.component';
 import { SchemaEditorHeaderComponent } from 
'./components/adapter-configuration/schema-editor/schema-editor-header/schema-editor-header.component';
 import { StartAdapterConfigurationComponent } from 
'./components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component';
+import { AdapterAssetConfigurationComponent } from 
'./components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component';
 import { DeleteAdapterDialogComponent } from 
'./dialog/delete-adapter-dialog/delete-adapter-dialog.component';
 import { PlatformServicesModule } from '@streampipes/platform-services';
 import { RouterModule } from '@angular/router';
@@ -105,9 +106,11 @@ import { AdapterDetailsDataComponent } from 
'./components/adapter-details/adapte
 import { EditRegexTransformationComponent } from 
'./dialog/edit-event-property/components/edit-regex-transformation/edit-regex-transformation.component';
 import { AdapterCodePanelComponent } from 
'./components/adapter-code-panel/adapter-code-panel.component';
 import { AdapterDetailsCodeComponent } from 
'./components/adapter-details/adapter-details-code/adapter-details-code.component';
-
+import { MatTreeModule } from '@angular/material/tree';
+import { TranslateModule } from '@ngx-translate/core';
 @NgModule({
     imports: [
+        MatTreeModule,
         MatCardModule,
         MatCheckboxModule,
         MatDialogModule,
@@ -145,6 +148,7 @@ import { AdapterDetailsCodeComponent } from 
'./components/adapter-details/adapte
         MatSnackBarModule,
         PlatformServicesModule,
         TreeModule,
+        TranslateModule.forChild(),
         RouterModule.forChild([
             {
                 path: '',
@@ -230,6 +234,7 @@ import { AdapterDetailsCodeComponent } from 
'./components/adapter-details/adapte
         SchemaEditorHeaderComponent,
         SpEpSettingsSectionComponent,
         StartAdapterConfigurationComponent,
+        AdapterAssetConfigurationComponent,
         SpAdapterDeploymentSettingsComponent,
         SpAdapterDetailsLogsComponent,
         SpAdapterDetailsMetricsComponent,
diff --git 
a/ui/src/app/connect/dialog/adapter-started/adapter-started-dialog.component.html
 
b/ui/src/app/connect/dialog/adapter-started/adapter-started-dialog.component.html
index f0b8b6707a..0f01497bc3 100644
--- 
a/ui/src/app/connect/dialog/adapter-started/adapter-started-dialog.component.html
+++ 
b/ui/src/app/connect/dialog/adapter-started/adapter-started-dialog.component.html
@@ -44,6 +44,7 @@
                     "
                     [pipelineOperationStatus]="pipelineOperationStatus"
                     [saveInDataLake]="saveInDataLake"
+                    [saveInAsset]="addToAssetText"
                     [templateErrorMessage]="templateErrorMessage"
                     [adapterErrorMessage]="adapterErrorMessage"
                 ></sp-adapter-started-success>
diff --git 
a/ui/src/app/connect/dialog/adapter-started/adapter-started-dialog.component.ts 
b/ui/src/app/connect/dialog/adapter-started/adapter-started-dialog.component.ts
index 130e752c66..7193931339 100644
--- 
a/ui/src/app/connect/dialog/adapter-started/adapter-started-dialog.component.ts
+++ 
b/ui/src/app/connect/dialog/adapter-started/adapter-started-dialog.component.ts
@@ -16,13 +16,22 @@
  *
  */
 
-import { Component, Input, OnInit } from '@angular/core';
+import {
+    Component,
+    Input,
+    OnInit,
+    EventEmitter,
+    Output,
+    inject,
+} from '@angular/core';
 import { ShepherdService } from '../../../services/tour/shepherd.service';
 import {
     AdapterDescription,
     AdapterService,
+    SpAssetTreeNode,
     CompactPipeline,
     CompactPipelineElement,
+    DatalakeRestService,
     ErrorMessage,
     Message,
     PipelineOperationStatus,
@@ -31,7 +40,14 @@ import {
     SpLogMessage,
 } from '@streampipes/platform-services';
 import { DialogRef } from '@streampipes/shared-ui';
-import { CompactPipelineService } from '@streampipes/platform-services';
+import {
+    CompactPipelineService,
+    LinkageData,
+} from '@streampipes/platform-services';
+import { AssetSaveService } from 
'../../services/adapter-asset-configuration.service';
+
+import { firstValueFrom } from 'rxjs';
+import { TranslateService } from '@ngx-translate/core';
 
 @Component({
     selector: 'sp-dialog-adapter-started-dialog',
@@ -39,8 +55,10 @@ import { CompactPipelineService } from 
'@streampipes/platform-services';
     standalone: false,
 })
 export class AdapterStartedDialog implements OnInit {
+    translateService = inject(TranslateService);
+
     adapterInstalled = false;
-    pollingActive = false;
+
     public pipelineOperationStatus: PipelineOperationStatus;
 
     /**
@@ -48,6 +66,11 @@ export class AdapterStartedDialog implements OnInit {
      */
     @Input() adapter: AdapterDescription;
 
+    /**
+     * Assets selectedAsset to link the adapter tp
+     */
+    @Input() selectedAssets: SpAssetTreeNode[];
+
     /**
      * Indicates if a pipeline to store the adapter events should be started
      */
@@ -68,6 +91,12 @@ export class AdapterStartedDialog implements OnInit {
      */
     @Input() startAdapterNow = true;
 
+    @Input()
+    allResourcesAlias = this.translateService.instant('Resources');
+
+    @Output() linkageDataEmitter: EventEmitter<LinkageData[]> =
+        new EventEmitter<LinkageData[]>();
+
     templateErrorMessage: ErrorMessage;
     adapterUpdatePreflight = false;
     adapterPipelineUpdateInfos: PipelineUpdateInfo[];
@@ -77,6 +106,7 @@ export class AdapterStartedDialog implements OnInit {
     adapterInstallationSuccessMessage = '';
     adapterElementId = '';
     adapterErrorMessage: SpLogMessage;
+    addToAssetText = '';
 
     constructor(
         public dialogRef: DialogRef<AdapterStartedDialog>,
@@ -84,6 +114,8 @@ export class AdapterStartedDialog implements OnInit {
         private shepherdService: ShepherdService,
         private pipelineTemplateService: PipelineTemplateService,
         private compactPipelineService: CompactPipelineService,
+        private assetSaveService: AssetSaveService,
+        private dataLakeService: DatalakeRestService,
     ) {}
 
     ngOnInit() {
@@ -111,7 +143,13 @@ export class AdapterStartedDialog implements OnInit {
     }
 
     updateAdapter(): void {
-        this.loadingText = `Updating adapter ${this.adapter.name}`;
+        this.loadingText = this.translateService.instant(
+            'Updating adapter {{adapterName}}',
+            {
+                adapterName: this.adapter.name,
+            },
+        );
+
         this.loading = true;
         this.adapterService.updateAdapter(this.adapter).subscribe({
             next: status => {
@@ -132,16 +170,23 @@ export class AdapterStartedDialog implements OnInit {
     }
 
     addAdapter() {
-        this.loadingText = `Creating adapter ${this.adapter.name}`;
+        this.loadingText = this.translateService.instant(
+            'Creating adapter {{adapterName}}',
+            {
+                adapterName: this.adapter.name,
+            },
+        );
         this.loading = true;
         this.adapterService.addAdapter(this.adapter).subscribe(
             status => {
                 if (status.success) {
                     const adapterElementId = status.notifications[0].title;
+                    this.adapterElementId = adapterElementId;
                     if (this.saveInDataLake) {
                         this.startSaveInDataLakePipeline(adapterElementId);
                     } else {
                         this.startAdapter(adapterElementId, true);
+                        this.addToAsset();
                     }
                 } else {
                     const errorMsg: SpLogMessage =
@@ -175,7 +220,12 @@ export class AdapterStartedDialog implements OnInit {
             'Your new data stream is now available in the pipeline editor.';
         if (this.startAdapterNow) {
             this.adapterElementId = adapterElementId;
-            this.loadingText = `Starting adapter ${this.adapter.name}`;
+            this.loadingText = this.translateService.instant(
+                'Starting adapter {{adapterName}}',
+                {
+                    adapterName: this.adapter.name,
+                },
+            );
             this.adapterService
                 .startAdapterByElementId(adapterElementId)
                 .subscribe(
@@ -209,11 +259,100 @@ export class AdapterStartedDialog implements OnInit {
     }
 
     onCloseConfirm() {
-        this.pollingActive = false;
         this.dialogRef.close('Confirm');
         this.shepherdService.trigger('confirm_adapter_started_button');
     }
 
+    async addToAsset(): Promise<void> {
+        try {
+            const adapter = await this.getAdapter();
+            const linkageData: LinkageData[] = this.createLinkageData(adapter);
+
+            if (this.saveInDataLake) {
+                await this.addDataLakeLinkageData(adapter, linkageData);
+            }
+
+            await this.saveAssets(linkageData);
+            this.setSuccessMessage(linkageData);
+        } catch (err) {
+            console.error('Error in addToAsset:', err);
+        }
+    }
+
+    private async getAdapter(): Promise<AdapterDescription> {
+        return await firstValueFrom(
+            this.adapterService.getAdapter(this.adapterElementId),
+        );
+    }
+
+    private createLinkageData(adapter: AdapterDescription): LinkageData[] {
+        return [
+            {
+                type: 'adapter',
+                id: this.adapterElementId,
+                name: adapter.name,
+            },
+            {
+                type: 'data-source',
+                id: adapter.correspondingDataStreamElementId,
+                name: adapter.name,
+            },
+        ];
+    }
+
+    private async addDataLakeLinkageData(
+        adapter: AdapterDescription,
+        linkageData: LinkageData[],
+    ): Promise<void> {
+        const pipelineId = `persist-${this.adapter.name.replaceAll(' ', '-')}`;
+        linkageData.push({
+            type: 'pipeline',
+            id: pipelineId,
+            name: pipelineId,
+        });
+
+        const res = await this.dataLakeService
+            .getMeasurementByName(adapter.name)
+            .toPromise();
+
+        linkageData.push({
+            type: 'measurement',
+            id: res.elementId,
+            name: adapter.name,
+        });
+    }
+
+    private async saveAssets(linkageData: LinkageData[]): Promise<void> {
+        await this.assetSaveService.saveSelectedAssets(
+            this.selectedAssets,
+            linkageData,
+        );
+    }
+
+    private setSuccessMessage(linkageData: LinkageData[]): void {
+        const assetTypesList = this.formatWithAnd(
+            linkageData.map(data => data.type),
+        );
+
+        const assetIdsList = this.formatWithAnd(
+            this.selectedAssets.map(asset => asset.assetName),
+        );
+
+        this.addToAssetText = this.translateService.instant(
+            'Your {{assetTypes}} were successfully added to {{assetIds}}.',
+            {
+                assetTypes: assetTypesList,
+                assetIds: assetIdsList,
+            },
+        );
+    }
+
+    private formatWithAnd(list: string[]): string {
+        if (list.length === 1) return list[0];
+        const lastItem = list.pop();
+        return `${list.join(', ')}, and ${lastItem}`;
+    }
+
     private startSaveInDataLakePipeline(adapterElementId: string) {
         this.loadingText = 'Creating pipeline to persist data stream';
         this.adapterService.getAdapter(adapterElementId).subscribe(adapter => {
@@ -241,6 +380,7 @@ export class AdapterStartedDialog implements OnInit {
                                 this.pipelineOperationStatus =
                                     pipelineOperationStatus;
                                 this.startAdapter(adapterElementId, true);
+                                this.addToAsset();
                             },
                             error => {
                                 this.onAdapterFailure(error.error);
diff --git 
a/ui/src/app/connect/dialog/adapter-started/adapter-started-success/adapter-started-success.component.html
 
b/ui/src/app/connect/dialog/adapter-started/adapter-started-success/adapter-started-success.component.html
index 39896f689e..1e3dd059d5 100644
--- 
a/ui/src/app/connect/dialog/adapter-started/adapter-started-success/adapter-started-success.component.html
+++ 
b/ui/src/app/connect/dialog/adapter-started/adapter-started-success/adapter-started-success.component.html
@@ -65,6 +65,28 @@
         </sp-pipeline-started-status>
         <mat-divider fxFlex="100"></mat-divider>
     </div>
+
+    <div
+        fxLayout="column"
+        fxLayoutAlign="center center"
+        fxFlex="100"
+        *ngIf="saveInAsset !== ''"
+    >
+        <div>
+            <div
+                class="info-message"
+                fxFlex="100"
+                fxLayoutAlign="center center"
+                fxLayout="row"
+            >
+                <i class="material-icons">done</i>
+                <span data-cy="sp-connect-adapter-success-added"
+                    >&nbsp;{{ saveInAsset }}</span
+                >
+            </div>
+        </div>
+        <mat-divider fxFlex="100"></mat-divider>
+    </div>
     <sp-exception-details
         *ngIf="adapterErrorMessage"
         [message]="adapterErrorMessage"
diff --git 
a/ui/src/app/connect/dialog/adapter-started/adapter-started-success/adapter-started-success.component.ts
 
b/ui/src/app/connect/dialog/adapter-started/adapter-started-success/adapter-started-success.component.ts
index ed6b984417..51fd017df1 100644
--- 
a/ui/src/app/connect/dialog/adapter-started/adapter-started-success/adapter-started-success.component.ts
+++ 
b/ui/src/app/connect/dialog/adapter-started/adapter-started-success/adapter-started-success.component.ts
@@ -36,6 +36,9 @@ export class SpAdapterStartedSuccessComponent {
     @Input()
     adapterInstallationSuccessMessage = '';
 
+    @Input()
+    saveInAsset = '';
+
     @Input()
     pipelineOperationStatus: PipelineOperationStatus;
 
diff --git a/ui/src/app/connect/services/adapter-asset-configuration.service.ts 
b/ui/src/app/connect/services/adapter-asset-configuration.service.ts
new file mode 100644
index 0000000000..67d374b730
--- /dev/null
+++ b/ui/src/app/connect/services/adapter-asset-configuration.service.ts
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { Injectable, Output, EventEmitter } from '@angular/core';
+import {
+    AssetConstants,
+    AssetManagementService,
+    AssetLink,
+    LinkageData,
+    SpAssetModel,
+    AssetLinkType,
+    GenericStorageService,
+    SpAssetTreeNode,
+} from '@streampipes/platform-services';
+
+@Injectable({
+    providedIn: 'root',
+})
+export class AssetSaveService {
+    assetLinkTypes: AssetLinkType[] = [];
+    currentAsset: SpAssetModel;
+    constructor(
+        private assetService: AssetManagementService,
+        private storageService: GenericStorageService,
+    ) {
+        this.loadAssetLinkTypes();
+    }
+
+    @Output() adapterStartedEmitter: EventEmitter<void> =
+        new EventEmitter<void>();
+
+    saveSelectedAssets(
+        selectedAssets: SpAssetTreeNode[],
+        linkageData: LinkageData[],
+    ): void {
+        const uniqueAssetIDsDict = this.getAssetPaths(selectedAssets);
+        const uniqueAssetIDs = Object.keys(uniqueAssetIDsDict);
+
+        uniqueAssetIDs.forEach(spAssetModelId => {
+            this.assetService.getAsset(spAssetModelId).subscribe({
+                next: current => {
+                    this.currentAsset = current;
+
+                    const links = this.buildLinks(linkageData);
+
+                    uniqueAssetIDsDict[spAssetModelId].forEach(path => {
+                        if (path.length === 2) {
+                            current.assetLinks = [
+                                ...(current.assetLinks ?? []),
+                                ...links,
+                            ];
+                        }
+                        if (path.length > 2) {
+                            this.updateDictValue(current, path, links);
+                        }
+                    });
+
+                    const updateObservable =
+                        this.assetService.updateAsset(current);
+                    updateObservable?.subscribe({
+                        next: updated => {
+                            this.adapterStartedEmitter.emit();
+                        },
+                    });
+                },
+            });
+        });
+    }
+
+    private updateDictValue(
+        dict: SpAssetModel,
+        path: (string | number)[],
+        newValue: any,
+    ) {
+        const result: any = { ...dict };
+        let current = result;
+        let parent: any = null;
+        for (let i = 2; i < path.length; i++) {
+            const key = path[i];
+
+            if (i === path.length - 1) {
+                current.assets[key].assetLinks = [
+                    ...(current.assets[key].assetLinks ?? []),
+                    ...newValue,
+                ];
+
+                break;
+            }
+
+            if (Array.isArray(current.assets)) {
+                parent = current;
+                current = { ...current.assets[key as number] };
+            }
+        }
+
+        return result;
+    }
+
+    private getAssetPaths(apiAssets: SpAssetTreeNode[]): {
+        [key: string]: Array<Array<string | number>>;
+    } {
+        const idPaths = {};
+        apiAssets.forEach(item => {
+            if (item.spAssetModelId && item.flattenPath) {
+                if (!idPaths[item.spAssetModelId]) {
+                    idPaths[item.spAssetModelId] = [];
+                }
+                idPaths[item.spAssetModelId].push(item.flattenPath);
+            }
+        });
+        return idPaths;
+    }
+
+    private buildLinks(data: LinkageData[]): AssetLink[] {
+        return data.map(item => {
+            const linkType = this.getAssetLinkTypeById(item.type);
+            return {
+                linkLabel: item.name,
+                linkType: item.type,
+                editingDisabled: false,
+                queryHint: item.type,
+                navigationActive: linkType?.navigationActive ?? false,
+                resourceId: item.id,
+            };
+        });
+    }
+
+    private getAssetLinkTypeById(linkType: string): AssetLinkType | undefined {
+        return this.assetLinkTypes.find(a => a.linkType === linkType);
+    }
+
+    private loadAssetLinkTypes(): void {
+        this.storageService
+            .getAllDocuments(AssetConstants.ASSET_LINK_TYPES_DOC_NAME)
+            .subscribe(linkTypes => {
+                this.assetLinkTypes = linkTypes.sort((a, b) =>
+                    a.linkLabel.localeCompare(b.linkLabel),
+                );
+            });
+    }
+}

Reply via email to