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 fe1ef7b4d6 Tests for PR #3829 and  #3817 (#3835)
fe1ef7b4d6 is described below

commit fe1ef7b4d63ae7ba800e1448b625c2a98592bb33
Author: Jacqueline Höllig <[email protected]>
AuthorDate: Wed Oct 15 12:25:26 2025 +0200

    Tests for PR #3829 and  #3817 (#3835)
    
    Co-authored-by: Philipp Zehnder <[email protected]>
    Co-authored-by: Dominik Riemer <[email protected]>
---
 ui/cypress/support/utils/GeneralUtils.ts           |   1 +
 ui/cypress/support/utils/asset/AssetBtns.ts        |   4 +
 ui/cypress/support/utils/asset/AssetUtils.ts       |  42 ++-
 ui/cypress/support/utils/connect/ConnectUtils.ts   |  53 ++++
 ui/cypress/support/utils/pipeline/PipelineUtils.ts |  55 +++-
 .../tests/connect/adapterWithAssets.smoke.spec.ts  | 106 +++++++
 .../tests/pipeline/pipelineAsset.smoke.spec.ts     |  97 ++++++
 .../asset-link-configuration.component.html        |  72 +++++
 .../asset-link-configuration.component.scss}       |   5 +
 .../asset-link-configuration.component.ts}         |  17 +-
 .../lib/services/asset-configuration.service.ts    | 337 +++++++++++++++++++++
 .../shared-ui/src/lib/shared-ui.module.ts          |   3 +
 .../streampipes/shared-ui/src/public-api.ts        |   2 +
 .../create-asset-dialog.component.html             |   2 +-
 .../adapter-asset-configuration.component.html     |  64 ----
 .../start-adapter-configuration.component.html     |   6 +-
 .../start-adapter-configuration.component.ts       |   1 -
 ui/src/app/connect/connect.module.ts               |   2 -
 .../adapter-started-dialog.component.ts            |  31 +-
 .../save-pipeline-settings.component.html          |  19 ++
 .../save-pipeline-settings.component.ts            |  43 ++-
 .../save-pipeline/save-pipeline.component.html     |   3 +
 .../save-pipeline/save-pipeline.component.ts       |  96 +++++-
 ui/src/app/editor/editor.module.ts                 |   1 +
 24 files changed, 941 insertions(+), 121 deletions(-)

diff --git a/ui/cypress/support/utils/GeneralUtils.ts 
b/ui/cypress/support/utils/GeneralUtils.ts
index 893240022d..1012f4be22 100644
--- a/ui/cypress/support/utils/GeneralUtils.ts
+++ b/ui/cypress/support/utils/GeneralUtils.ts
@@ -22,6 +22,7 @@ export class GeneralUtils {
     }
 
     public static openMenuForRow(rowText: string) {
+        cy.log('ROW TEXT', rowText);
         cy.contains('[role="row"], tr, mat-row', rowText) // be flexible on 
row element
             .scrollIntoView()
             .within(() => {
diff --git a/ui/cypress/support/utils/asset/AssetBtns.ts 
b/ui/cypress/support/utils/asset/AssetBtns.ts
index 8d574e0e7e..d958a3fe14 100644
--- a/ui/cypress/support/utils/asset/AssetBtns.ts
+++ b/ui/cypress/support/utils/asset/AssetBtns.ts
@@ -29,6 +29,10 @@ export class AssetBtns {
         return cy.dataCy('save-asset', { timeout: 10000 });
     }
 
+    public static createAssetPanelBtn() {
+        return cy.dataCy('create-asset-panel', { timeout: 10000 });
+    }
+
     public static editAssetBtn(assetName: string) {
         return cy.dataCy('edit-asset-' + assetName, { timeout: 10000 });
     }
diff --git a/ui/cypress/support/utils/asset/AssetUtils.ts 
b/ui/cypress/support/utils/asset/AssetUtils.ts
index 3b10320edf..bbed4c184a 100644
--- a/ui/cypress/support/utils/asset/AssetUtils.ts
+++ b/ui/cypress/support/utils/asset/AssetUtils.ts
@@ -23,6 +23,7 @@ import { GeneralUtils } from '../GeneralUtils';
 export class AssetUtils {
     public static goToAssets() {
         cy.visit('#/assets/overview');
+        cy.dataCy('create-new-asset-button').should('be.visible');
     }
 
     public static goBackToOverview() {
@@ -33,7 +34,14 @@ export class AssetUtils {
         AssetBtns.createAssetBtn().click();
         AssetBtns.assetNameInput().clear();
         AssetBtns.assetNameInput().type(assetName);
+        AssetBtns.createAssetPanelBtn().click();
+    }
+
+    public static addAndSaveAsset(assetName: string) {
+        AssetUtils.addNewAsset(assetName);
+
         AssetBtns.saveAssetBtn().click();
+        AssetBtns.createAssetBtn().should('be.visible');
     }
 
     public static openManageAssetLinks() {
@@ -59,9 +67,41 @@ export class AssetUtils {
             .should('have.length', amount);
     }
 
+    public static checkAmountOfAssetsGreaterThan(amount: number) {
+        cy.dataCy('assets-table', { timeout: 10000 }).should(
+            'have.length.greaterThan',
+            amount,
+        );
+    }
+
+    public static checkAmountOfLinkedResourcesByAssetName(
+        assetName: string,
+        amount: number,
+    ) {
+        AssetUtils.goToAssets();
+        cy.wait(400);
+        AssetUtils.editAsset(assetName);
+        cy.wait(400);
+        AssetBtns.assetLinksTab().click();
+        AssetUtils.checkAmountOfLinkedResources(amount);
+    }
+
+    public static checkResourceNamingByAssetName(
+        assetName: string,
+        name: string,
+    ) {
+        AssetUtils.goToAssets();
+        AssetUtils.editAsset(assetName);
+        AssetBtns.assetLinksTab().click();
+        cy.dataCy('linked-resources-list').children().contains(name);
+        //.should('have.length', amount);
+    }
+
     public static editAsset(assetName: string) {
         GeneralUtils.openMenuForRow(assetName);
-        AssetBtns.editAssetBtn(assetName).click();
+        cy.contains('button', 'Edit').click({ force: true });
+        //This is the old version and there in case above does not work for 
all tests
+        //AssetBtns.editAssetBtn(assetName).click({ force: true });
     }
 
     public static addAssetWithOneAdapter(assetName: string) {
diff --git a/ui/cypress/support/utils/connect/ConnectUtils.ts 
b/ui/cypress/support/utils/connect/ConnectUtils.ts
index a9c682a1eb..fcb4d0a779 100644
--- a/ui/cypress/support/utils/connect/ConnectUtils.ts
+++ b/ui/cypress/support/utils/connect/ConnectUtils.ts
@@ -84,6 +84,21 @@ export class ConnectUtils {
         ConnectEventSchemaUtils.finishEventSchemaConfiguration();
     }
 
+    public static addAdapterWithLinkedAssets(
+        adapterConfiguration: AdapterInput,
+        assetNameList,
+    ) {
+        ConnectUtils.addAdapter(adapterConfiguration);
+
+        ConnectUtils.startAdapter(
+            adapterConfiguration,
+            false,
+            false,
+            true,
+            assetNameList,
+        );
+    }
+
     private static configureDimensionProperties(
         adapterConfiguration: AdapterInput,
     ) {
@@ -98,6 +113,10 @@ export class ConnectUtils {
         }
     }
 
+    public static renameAdapter(newName: string) {
+        cy.dataCy('sp-adapter-name').clear().type(newName);
+        cy.dataCy('sp-adapter-name').should('have.value', newName);
+    }
     public static addMachineDataSimulator(
         name: string,
         persist: boolean = false,
@@ -128,6 +147,7 @@ export class ConnectUtils {
 
     public static goToConnect() {
         cy.visit('#/connect');
+        cy.dataCy('connect-create-new-adapter-button').should('be.visible');
     }
 
     public static goToNewAdapterPage() {
@@ -181,6 +201,8 @@ export class ConnectUtils {
         adapterInput: AdapterInput,
         noLiveDataView = false,
         adapterStartFails = false,
+        addToAsset = false,
+        assetNameList = [],
     ) {
         // Set adapter name
         cy.dataCy('sp-adapter-name').type(adapterInput.adapterName);
@@ -204,6 +226,12 @@ export class ConnectUtils {
             ConnectBtns.startAdapterNowCheckbox().click();
         }
 
+        //add the Adapter to an Asset
+
+        if (addToAsset) {
+            this.addToAsset(assetNameList);
+        }
+
         ConnectBtns.adapterSettingsStartAdapter().click();
 
         if (adapterStartFails) {
@@ -225,6 +253,31 @@ export class ConnectUtils {
         this.closeAdapterPreview();
     }
 
+    public static addToAsset(assetNameList = []) {
+        cy.dataCy('show-asset-checkbox').click();
+        cy.get('mat-tree.asset-tree', { timeout: 10000 }).should('exist');
+
+        assetNameList.forEach(assetName => {
+            cy.get('mat-tree.asset-tree')
+                .find('.mat-tree-node')
+                .contains(assetName)
+                .click();
+        });
+    }
+
+    public static editAsset(assetNameList = []) {
+        //cy.dataCy('show-asset-checkbox').click();
+        cy.get('mat-tree.asset-tree', { timeout: 10000 }).should('exist');
+
+        assetNameList.forEach(assetName => {
+            console.log(assetName);
+            cy.get('mat-tree.asset-tree')
+                .find('.mat-tree-node')
+                .contains(assetName)
+                .click();
+        });
+    }
+
     // Close adapter preview
     public static closeAdapterPreview() {
         cy.get('button').contains('Close').parent().click();
diff --git a/ui/cypress/support/utils/pipeline/PipelineUtils.ts 
b/ui/cypress/support/utils/pipeline/PipelineUtils.ts
index 5af6c42d7a..ebb619c216 100644
--- a/ui/cypress/support/utils/pipeline/PipelineUtils.ts
+++ b/ui/cypress/support/utils/pipeline/PipelineUtils.ts
@@ -37,6 +37,22 @@ export class PipelineUtils {
         PipelineUtils.startPipeline(pipelineInput);
     }
 
+    public static addPipelineWithAssetLinks(
+        pipelineInput: PipelineInput,
+        assetNameList: String[],
+    ) {
+        PipelineUtils.goToPipelineEditor();
+
+        PipelineUtils.selectDataStream(pipelineInput);
+
+        PipelineUtils.configurePipeline(pipelineInput);
+
+        PipelineUtils.startPipelineWithAssetLinkage(
+            pipelineInput,
+            assetNameList,
+        );
+    }
+
     /**
      * This method adds a sample adapter and pipeline
      */
@@ -146,6 +162,39 @@ export class PipelineUtils {
         PipelineUtils.finalizePipelineStart();
     }
 
+    public static startPipelineWithAssetLinkage(
+        pipelineInput?: PipelineInput,
+        assetNameList?: String[],
+    ) {
+        // Save and start pipeline
+        cy.dataCy('sp-editor-save-pipeline').click();
+        if (pipelineInput) {
+            cy.dataCy('sp-editor-pipeline-name').type(
+                pipelineInput.pipelineName,
+            );
+        }
+        PipelineUtils.finalizePipelineStart(assetNameList);
+    }
+
+    private static addToAsset(assetNameList) {
+        cy.dataCy('sp-show-pipeline-asset-checkbox')
+            .find('input[type="checkbox"]')
+            .then($checkbox => {
+                if (!$checkbox.prop('checked')) {
+                    cy.wrap($checkbox).click();
+                }
+            });
+
+        cy.get('mat-tree.asset-tree', { timeout: 10000 }).should('exist');
+        assetNameList.forEach(assetName => {
+            console.log(assetName);
+            cy.get('mat-tree.asset-tree')
+                .find('.mat-tree-node')
+                .contains(assetName)
+                .click();
+        });
+    }
+
     public static clonePipeline(newPipelineName: string) {
         cy.dataCy('pipeline-update-mode-clone').children().click();
         cy.dataCy('sp-editor-pipeline-name').type(newPipelineName);
@@ -156,9 +205,13 @@ export class PipelineUtils {
         cy.dataCy('sp-editor-pipeline-name').type(newPipelineName);
     }
 
-    public static finalizePipelineStart() {
+    public static finalizePipelineStart(assetNameList?: String[]) {
         
cy.dataCy('sp-editor-checkbox-navigate-to-overview').children().click();
+        if (assetNameList) {
+            PipelineUtils.addToAsset(assetNameList);
+        }
         cy.dataCy('sp-editor-apply').click();
+
         cy.dataCy('sp-pipeline-started-success', { timeout: 15000 }).should(
             'be.visible',
         );
diff --git a/ui/cypress/tests/connect/adapterWithAssets.smoke.spec.ts 
b/ui/cypress/tests/connect/adapterWithAssets.smoke.spec.ts
new file mode 100644
index 0000000000..c69d7d71b8
--- /dev/null
+++ b/ui/cypress/tests/connect/adapterWithAssets.smoke.spec.ts
@@ -0,0 +1,106 @@
+/*
+ * 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 { ConnectUtils } from '../../support/utils/connect/ConnectUtils';
+import { AdapterBuilder } from '../../support/builder/AdapterBuilder';
+import { AssetUtils } from '../../support/utils/asset/AssetUtils';
+import { ConnectBtns } from '../../support/utils/connect/ConnectBtns';
+import { AssetBtns } from '../../support/utils/asset/AssetBtns';
+
+describe('Creates a new adapter with a linked asset', () => {
+    const assetName1 = 'TestAsset1';
+    const assetName2 = 'TestAsset2';
+    const assetName3 = 'TestAsset3';
+    const adapterConfiguration = 
AdapterBuilder.create('Machine_Data_Simulator')
+        .setName('Machine Data Simulator Test')
+        .addInput('input', 'wait-time-ms', '1000')
+        .setStartAdapter(false)
+        .build();
+
+    beforeEach('Setup Test', () => {
+        cy.initStreamPipesTest();
+        AssetUtils.goToAssets();
+        AssetUtils.addAndSaveAsset(assetName3);
+        AssetUtils.addAndSaveAsset(assetName2);
+        AssetUtils.addAndSaveAsset(assetName1);
+    });
+
+    it('Add Assets during Adapter generation', () => {
+        // Create
+        ConnectUtils.goToConnect();
+        ConnectUtils.addAdapterWithLinkedAssets(adapterConfiguration, [
+            assetName1,
+        ]);
+
+        //Go Back to Asset
+        AssetUtils.goToAssets();
+        AssetUtils.checkAmountOfAssetsGreaterThan(0);
+
+        AssetUtils.editAsset(assetName1);
+        AssetBtns.assetLinksTab().click();
+
+        //Check if Link is there
+        AssetUtils.checkAmountOfLinkedResources(2);
+    });
+
+    it('Edit Assets during Adapter editing', () => {
+        // Add the first two Asssets by default
+        ConnectUtils.addAdapterWithLinkedAssets(adapterConfiguration, [
+            assetName1,
+            assetName2,
+        ]);
+
+        //Check if Added Correctly
+        AssetUtils.checkAmountOfLinkedResourcesByAssetName(assetName1, 2);
+        AssetUtils.checkAmountOfLinkedResourcesByAssetName(assetName2, 2);
+
+        //Edit
+        ConnectUtils.goToConnect();
+        ConnectBtns.openActionsMenu('Machine Data Simulator Test');
+        ConnectBtns.editAdapter().should('not.be.disabled');
+        ConnectBtns.editAdapter().click();
+
+        // Go over the first two steps
+        ConnectBtns.nextBtn().click();
+        ConnectUtils.finishEventSchemaConfiguration();
+
+        // Rename
+        ConnectUtils.renameAdapter('Changed');
+
+        // Deselect Asset 2
+        ConnectUtils.editAsset([assetName1]);
+
+        // Select Asset 3 //TODO Click on Asset
+        ConnectUtils.editAsset([assetName3]);
+
+        ConnectBtns.storeEditAdapter().click();
+
+        cy.dataCy('sp-connect-adapter-success-added', {
+            timeout: 60000,
+        }).should('be.visible');
+
+        ConnectUtils.closeAdapterPreview();
+
+        // Test Number of Asset Links
+        AssetUtils.checkAmountOfLinkedResourcesByAssetName(assetName2, 2);
+        AssetUtils.checkAmountOfLinkedResourcesByAssetName(assetName3, 2);
+
+        // Test Renaming
+        AssetUtils.checkResourceNamingByAssetName(assetName2, 'Changed');
+    });
+});
diff --git a/ui/cypress/tests/pipeline/pipelineAsset.smoke.spec.ts 
b/ui/cypress/tests/pipeline/pipelineAsset.smoke.spec.ts
new file mode 100644
index 0000000000..6638c182e2
--- /dev/null
+++ b/ui/cypress/tests/pipeline/pipelineAsset.smoke.spec.ts
@@ -0,0 +1,97 @@
+/*
+ * 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 { PipelineUtils } from '../../support/utils/pipeline/PipelineUtils';
+import { AssetUtils } from '../../support/utils/asset/AssetUtils';
+import { ConnectUtils } from '../../support/utils/connect/ConnectUtils';
+import { PipelineBuilder } from '../../support/builder/PipelineBuilder';
+import { PipelineElementBuilder } from 
'../../support/builder/PipelineElementBuilder';
+import { AssetBtns } from '../../support/utils/asset/AssetBtns';
+
+describe('Test Saving Pipeline with Asset Link', () => {
+    const assetName1 = 'Test1';
+    const assetName2 = 'Test2';
+    const assetName3 = 'Test3';
+    beforeEach('Setup Test', () => {
+        cy.initStreamPipesTest();
+        AssetUtils.goToAssets();
+        AssetUtils.addAndSaveAsset(assetName3);
+        AssetUtils.addAndSaveAsset(assetName2);
+        AssetUtils.addAndSaveAsset(assetName1);
+
+        // Generate A Pipeline
+        const adapterName = 'simulator';
+
+        ConnectUtils.addMachineDataSimulator(adapterName);
+
+        const pipelineInput = PipelineBuilder.create('Pipeline Test')
+            .addSource(adapterName)
+            .addSink(
+                PipelineElementBuilder.create('data_lake')
+                    .addInput('input', 'db_measurement', 'demo')
+                    .build(),
+            )
+            .build();
+
+        PipelineUtils.addPipelineWithAssetLinks(pipelineInput, [
+            assetName1,
+            assetName2,
+        ]);
+    });
+
+    it('Add Pipeline to Asset during creation', () => {
+        PipelineUtils.deletePipeline(`Pipeline Test`);
+
+        // Go Back to Asset
+        AssetUtils.goToAssets();
+        AssetUtils.checkAmountOfAssetsGreaterThan(0);
+
+        // CLick on Asset
+
+        AssetUtils.editAsset(assetName1);
+        AssetBtns.assetLinksTab().click();
+        AssetUtils.checkAmountOfLinkedResources(2);
+
+        // Go Back to Asset
+        AssetUtils.goToAssets();
+        AssetUtils.checkAmountOfAssetsGreaterThan(0);
+        AssetUtils.editAsset(assetName2);
+        AssetBtns.assetLinksTab().click();
+        AssetUtils.checkAmountOfLinkedResources(2);
+    });
+
+    it('Edit Pipeline to Asset during Edit', () => {
+        PipelineUtils.editPipeline('Pipeline Test');
+        cy.dataCy('sp-editor-save-pipeline', { timeout: 10000 })
+            .should('exist')
+            .click();
+        cy.dataCy('sp-editor-pipeline-name').clear();
+        PipelineUtils.updatePipeline('Renamed Pipeline');
+        PipelineUtils.finalizePipelineStart([assetName1, assetName3]);
+
+        // Test Number of Asset Links
+        AssetUtils.checkAmountOfLinkedResourcesByAssetName(assetName2, 2);
+        AssetUtils.checkAmountOfLinkedResourcesByAssetName(assetName3, 2);
+
+        // Test Renaming
+        AssetUtils.checkResourceNamingByAssetName(
+            assetName2,
+            'Renamed Pipeline',
+        );
+    });
+});
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.html
 
b/ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.html
new file mode 100644
index 0000000000..db309f64f4
--- /dev/null
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-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.
+  ~
+  -->
+
+@if (assetsData?.length) {
+    <div class="mt-10">
+        <div class="tree-container">
+            <mat-tree
+                [dataSource]="dataSource"
+                [treeControl]="treeControl"
+                class="asset-tree"
+                dataCy="sp-asset-linkage-tree"
+            >
+                <!-- Parent Node Definition -->
+                <mat-nested-tree-node
+                    *matTreeNodeDef="let node; when: hasChild"
+                >
+                    <div class="mat-tree-node" (click)="onAssetSelect(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 [class.selected-node]="isSelected(node)">{{
+                            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)">
+                        <span class="mat-icon-button placeholder-icon">
+                            <mat-icon class="invisible"></mat-icon>
+                        </span>
+                        <span [class.selected-node]="isSelected(node)">{{
+                            node.assetName
+                        }}</span>
+                    </div>
+                </mat-tree-node>
+            </mat-tree>
+        </div>
+    </div>
+} @else {
+    <!-- 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/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.scss
similarity index 95%
rename from 
ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.scss
rename to 
ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.scss
index 5e1df3b47e..f0a60ac442 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.scss
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.scss
@@ -42,6 +42,11 @@ mat-checkbox {
     padding-left: 10px;
     margin-left: 10px;
 }
+.tree-container {
+    max-height: 200px;
+    overflow-y: auto;
+    padding-right: 10px;
+}
 
 .mat-tree-node {
     display: flex;
diff --git 
a/ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.ts
 
b/ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.ts
similarity index 92%
rename from 
ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.ts
rename to 
ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.ts
index 8666dc0958..84f55d2530 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.ts
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.ts
@@ -26,22 +26,21 @@ import {
     AssetLinkType,
     SpAsset,
     SpAssetTreeNode,
-    AdapterDescription,
 } 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'],
+    selector: 'sp-asset-link-configuration',
+    templateUrl: './asset-link-configuration.component.html',
+    styleUrls: ['./asset-link-configuration.component.scss'],
     standalone: false,
 })
-export class AdapterAssetConfigurationComponent implements OnInit {
+export class AssetLinkConfigurationComponent implements OnInit {
     @Input() linkageData: LinkageData[] = [];
     @Input() stepper: MatStepper;
     @Input() isEdit: boolean;
-    @Input() adapter: AdapterDescription;
+    @Input() itemId: unknown;
 
     @Output() adapterStartedEmitter: EventEmitter<void> =
         new EventEmitter<void>();
@@ -142,7 +141,7 @@ export class AdapterAssetConfigurationComponent implements 
OnInit {
     }
 
     private setSelect() {
-        if (!this.adapter || !this.adapter.elementId) {
+        if (!this.itemId) {
             return;
         }
 
@@ -159,9 +158,7 @@ export class AdapterAssetConfigurationComponent implements 
OnInit {
 
         if (
             node.assetLinks &&
-            node.assetLinks.some(
-                link => link.resourceId === this.adapter.elementId,
-            )
+            node.assetLinks.some(link => link.resourceId === this.itemId)
         ) {
             if (!this.isSelected(node)) {
                 this.selectedAssets.push(node);
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/services/asset-configuration.service.ts
 
b/ui/projects/streampipes/shared-ui/src/lib/services/asset-configuration.service.ts
new file mode 100644
index 0000000000..c7dbe9ef9a
--- /dev/null
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/services/asset-configuration.service.ts
@@ -0,0 +1,337 @@
+/*
+ * 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';
+import { firstValueFrom } from 'rxjs';
+
+@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>();
+
+    async saveSelectedAssets(
+        selectedAssets: SpAssetTreeNode[],
+        linkageData: LinkageData[],
+        deselectedAssets: SpAssetTreeNode[] = [],
+        originalAssets: SpAssetTreeNode[] = [],
+    ): Promise<void> {
+        const links = this.buildLinks(linkageData);
+
+        if (deselectedAssets.length > 0) {
+            await this.deleteLinkOnDeselectAssets(deselectedAssets, links);
+        }
+        if (selectedAssets.length > 0) {
+            await this.setLinkOnSelectAssets(selectedAssets, links);
+        }
+
+        if (originalAssets.length > 0) {
+            //filter is necessary, otherwise conflicting database instances 
are produced
+            const filteredOriginal = this.filterAssets(
+                originalAssets,
+                deselectedAssets,
+                selectedAssets,
+            );
+
+            if (filteredOriginal.length > 0) {
+                this.renameLinkage(filteredOriginal, links);
+            }
+        }
+    }
+    private filterAssets(
+        originalAssets: SpAssetTreeNode[],
+        deselectedAssets: SpAssetTreeNode[],
+        selectedAssets: SpAssetTreeNode[],
+    ): SpAssetTreeNode[] {
+        const deselectedAssetIds = new Set(
+            deselectedAssets.map(asset => asset.assetId),
+        );
+        const selectedAssetIds = new Set(
+            selectedAssets.map(asset => asset.assetId),
+        );
+
+        return originalAssets.filter(
+            asset =>
+                !deselectedAssetIds.has(asset.assetId) &&
+                !selectedAssetIds.has(asset.assetId),
+        );
+    }
+
+    renameLinkage(originalAssets, links) {
+        const uniqueAssetIDsDict = this.getAssetPaths(originalAssets);
+        const uniqueAssetIDs = Object.keys(uniqueAssetIDsDict);
+
+        uniqueAssetIDs.forEach(spAssetModelId => {
+            this.assetService.getAsset(spAssetModelId).subscribe({
+                next: current => {
+                    this.currentAsset = current;
+
+                    uniqueAssetIDsDict[spAssetModelId].forEach(path => {
+                        if (path.length === 2) {
+                            current.assetLinks = (current.assetLinks ?? 
[]).map(
+                                (link: any) => {
+                                    const matchedLink = links.find(
+                                        l => l.resourceId === link.resourceId,
+                                    );
+                                    if (matchedLink) {
+                                        link.linkLabel = matchedLink.linkLabel;
+                                    }
+                                    return link;
+                                },
+                            );
+                        }
+
+                        if (path.length > 2) {
+                            links.forEach(linkToUpdate => {
+                                this.updateLinkLabelInDict(
+                                    current,
+                                    path,
+                                    linkToUpdate,
+                                );
+                            });
+                        }
+                    });
+
+                    const updateObservable =
+                        this.assetService.updateAsset(current);
+
+                    updateObservable?.subscribe({
+                        next: () => {
+                            this.adapterStartedEmitter.emit();
+                        },
+                    });
+                },
+            });
+        });
+    }
+
+    private updateLinkLabelInDict(
+        dict: SpAssetTreeNode,
+        path: (string | number)[],
+        linkToUpdate: any,
+    ) {
+        let current = dict;
+
+        for (let i = 2; i < path.length; i++) {
+            const key = path[i];
+            if (i === path.length - 1) {
+                if (current.assets?.[key]?.assetLinks) {
+                    current.assets[key].assetLinks = current.assets[
+                        key
+                    ].assetLinks.map((link: any) => {
+                        if (link.resourceId === linkToUpdate.resourceId) {
+                            link.linkLabel = linkToUpdate.linkLabel;
+                        }
+                        return link;
+                    });
+                }
+            } else {
+                if (Array.isArray(current.assets)) {
+                    current = current.assets[key as number];
+                }
+            }
+        }
+
+        return current;
+    }
+    async setLinkOnSelectAssets(
+        selectedAssets: SpAssetTreeNode[],
+        links: AssetLink[],
+    ): Promise<void> {
+        const uniqueAssetIDsDict = this.getAssetPaths(selectedAssets);
+        const uniqueAssetIDs = Object.keys(uniqueAssetIDsDict);
+
+        for (const spAssetModelId of uniqueAssetIDs) {
+            const current = await firstValueFrom(
+                this.assetService.getAsset(spAssetModelId),
+            );
+
+            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);
+            await firstValueFrom(updateObservable); // Ensure this completes 
before continuing
+        }
+    }
+
+    async deleteLinkOnDeselectAssets(
+        deselectedAssets: SpAssetTreeNode[],
+        links: AssetLink[],
+    ): Promise<void> {
+        const uniqueAssetIDsDict = this.getAssetPaths(deselectedAssets);
+        const uniqueAssetIDs = Object.keys(uniqueAssetIDsDict);
+
+        for (const spAssetModelId of uniqueAssetIDs) {
+            const current = await firstValueFrom(
+                this.assetService.getAsset(spAssetModelId),
+            );
+
+            uniqueAssetIDsDict[spAssetModelId].forEach(path => {
+                if (path.length === 2) {
+                    current.assetLinks = (current.assetLinks ?? []).filter(
+                        (link: any) =>
+                            !links.some(
+                                l =>
+                                    JSON.stringify(l.resourceId) ===
+                                    JSON.stringify(link.resourceId),
+                            ),
+                    );
+                }
+
+                if (path.length > 2) {
+                    links.forEach(linkToRemove => {
+                        this.deleteDictValue(current, path, linkToRemove);
+                    });
+                }
+            });
+
+            const updateObservable = this.assetService.updateAsset(current);
+            await firstValueFrom(updateObservable); // Ensure this completes 
before continuing
+        }
+    }
+
+    private deleteDictValue(
+        dict: SpAssetTreeNode,
+        path: (string | number)[],
+        linkToRemove: any,
+    ) {
+        let current = dict;
+
+        for (let i = 2; i < path.length; i++) {
+            const key = path[i];
+            if (i === path.length - 1) {
+                if (current.assets?.[key]?.assetLinks) {
+                    current.assets[key].assetLinks = current.assets[
+                        key
+                    ].assetLinks.filter(
+                        (link: any) =>
+                            JSON.stringify(link.resourceId) !==
+                            JSON.stringify(linkToRemove.resourceId),
+                    );
+                }
+            } else {
+                if (Array.isArray(current.assets)) {
+                    current = current.assets[key as number];
+                }
+            }
+        }
+
+        return current;
+    }
+
+    private updateDictValue(
+        dict: SpAssetModel,
+        path: (string | number)[],
+        newValue: any,
+    ) {
+        const result: any = { ...dict };
+        let current = result;
+        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),
+                );
+            });
+    }
+}
diff --git a/ui/projects/streampipes/shared-ui/src/lib/shared-ui.module.ts 
b/ui/projects/streampipes/shared-ui/src/lib/shared-ui.module.ts
index 0bbe262e61..176c333794 100644
--- a/ui/projects/streampipes/shared-ui/src/lib/shared-ui.module.ts
+++ b/ui/projects/streampipes/shared-ui/src/lib/shared-ui.module.ts
@@ -99,6 +99,7 @@ import { MatExpansionModule } from 
'@angular/material/expansion';
 import { SortByRuntimeNamePipe } from './pipes/sort-by-runtime-name.pipe';
 import { DragDropModule } from '@angular/cdk/drag-drop';
 import { SpTableActionsDirective } from 
'./components/sp-table/sp-table-actions.directive';
+import { AssetLinkConfigurationComponent } from 
'./components/asset-link-configuration/asset-link-configuration.component';
 
 @NgModule({
     declarations: [
@@ -151,6 +152,7 @@ import { SpTableActionsDirective } from 
'./components/sp-table/sp-table-actions.
         InputSchemaPropertyComponent,
         SortByRuntimeNamePipe,
         SpTableActionsDirective,
+        AssetLinkConfigurationComponent,
     ],
     imports: [
         CommonModule,
@@ -190,6 +192,7 @@ import { SpTableActionsDirective } from 
'./components/sp-table/sp-table-actions.
     ],
     exports: [
         AssetBrowserComponent,
+        AssetLinkConfigurationComponent,
         ConfirmDialogComponent,
         DataDownloadDialogComponent,
         DateInputComponent,
diff --git a/ui/projects/streampipes/shared-ui/src/public-api.ts 
b/ui/projects/streampipes/shared-ui/src/public-api.ts
index f416efe889..e25e3ad2b4 100644
--- a/ui/projects/streampipes/shared-ui/src/public-api.ts
+++ b/ui/projects/streampipes/shared-ui/src/public-api.ts
@@ -54,6 +54,7 @@ export * from 
'./lib/components/pipeline-element-documentation/pipeline-element-
 export * from './lib/components/pipeline-element/pipeline-element.component';
 export * from 
'./lib/components/input-schema-panel/input-schema-panel.component';
 export * from './lib/components/sidebar-resize/sidebar-resize.component';
+export * from 
'./lib/components/asset-link-configuration/asset-link-configuration.component';
 
 export * from './lib/models/sp-navigation.model';
 
@@ -66,3 +67,4 @@ export * from './lib/services/time-selection.service';
 export * from './lib/components/asset-browser/asset-browser.service';
 export * from './lib/services/date-format.service';
 export * from './lib/services/pipeline-element-schema.service';
+export * from './lib/services/asset-configuration.service';
diff --git 
a/ui/src/app/assets/dialog/create-asset/create-asset-dialog.component.html 
b/ui/src/app/assets/dialog/create-asset/create-asset-dialog.component.html
index 83e25dad6f..eb59282107 100644
--- a/ui/src/app/assets/dialog/create-asset/create-asset-dialog.component.html
+++ b/ui/src/app/assets/dialog/create-asset/create-asset-dialog.component.html
@@ -54,7 +54,7 @@
             mat-button
             mat-flat-button
             color="accent"
-            data-cy="save-asset"
+            data-cy="create-asset-panel"
             (click)="onSave()"
         >
             Create
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
deleted file mode 100644
index 1688dc5899..0000000000
--- 
a/ui/src/app/connect/components/adapter-configuration/adapter-asset-configuration/adapter-asset-configuration.component.html
+++ /dev/null
@@ -1,64 +0,0 @@
-<!--
-  ~ 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" class="mt-10">
-    <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)">
-                <button
-                    mat-icon-button
-                    matTreeNodeToggle
-                    [attr.aria-label]="'Toggle ' + node.assetName"
-                >
-                    <mat-icon>{{
-                        treeControl.isExpanded(node)
-                            ? 'expand_more'
-                            : 'chevron_right'
-                    }}</mat-icon>
-                </button>
-                <span [class.selected-node]="isSelected(node)">{{
-                    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)">
-                <span class="mat-icon-button placeholder-icon">
-                    <mat-icon class="invisible"></mat-icon>
-                </span>
-                <span [class.selected-node]="isSelected(node)">{{
-                    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/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 4f9c96965e..843ac4bc7d 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
@@ -113,15 +113,15 @@
             "
             (optionSelectedEmitter)="showAsset = $event"
         >
-            <sp-adapter-asset-configuration
+            <sp-asset-link-configuration
                 *ngIf="showAsset"
                 [isEdit]="isEditMode"
-                [adapter]="adapterDescription"
+                [itemId]="adapterDescription.elementId"
                 (selectedAssetsChange)="onSelectedAssetsChange($event)"
                 (deselectedAssetsChange)="onDeselectedAssetsChange($event)"
                 (originalAssetsEmitter)="onOriginalAssetsEmitted($event)"
             >
-            </sp-adapter-asset-configuration>
+            </sp-asset-link-configuration>
         </sp-adapter-options-panel>
 
         <sp-adapter-options-panel
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 5d2ccbaf39..f38436f3b8 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
@@ -15,7 +15,6 @@
  * limitations under the License.
  *
  */
-
 import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
 import {
     AdapterDescription,
diff --git a/ui/src/app/connect/connect.module.ts 
b/ui/src/app/connect/connect.module.ts
index a0cdb02303..260e3a12d7 100644
--- a/ui/src/app/connect/connect.module.ts
+++ b/ui/src/app/connect/connect.module.ts
@@ -52,7 +52,6 @@ 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';
@@ -237,7 +236,6 @@ import { TranslatePipe } from '@ngx-translate/core';
         SchemaEditorHeaderComponent,
         SpEpSettingsSectionComponent,
         StartAdapterConfigurationComponent,
-        AdapterAssetConfigurationComponent,
         SpAdapterDeploymentSettingsComponent,
         SpAdapterDetailsLogsComponent,
         SpAdapterDetailsMetricsComponent,
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 8f16df7572..d93ce246ee 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
@@ -24,7 +24,6 @@ import {
     Output,
     inject,
 } from '@angular/core';
-
 import { ShepherdService } from '../../../services/tour/shepherd.service';
 import {
     AdapterDescription,
@@ -42,11 +41,10 @@ import {
     LinkageData,
     CompactPipelineService,
 } from '@streampipes/platform-services';
-import { DialogRef } from '@streampipes/shared-ui';
+import { AssetSaveService, DialogRef } from '@streampipes/shared-ui';
 
 import { TranslateService } from '@ngx-translate/core';
-import { AssetSaveService } from 
'../../services/adapter-asset-configuration.service';
-import { firstValueFrom } from 'rxjs';
+import { firstValueFrom, lastValueFrom } from 'rxjs';
 
 @Component({
     selector: 'sp-dialog-adapter-started-dialog',
@@ -55,6 +53,13 @@ import { firstValueFrom } from 'rxjs';
 })
 export class AdapterStartedDialog implements OnInit {
     translateService = inject(TranslateService);
+    public dialogRef = inject(DialogRef<AdapterStartedDialog>);
+    private adapterService = inject(AdapterService);
+    private shepherdService = inject(ShepherdService);
+    private pipelineTemplateService = inject(PipelineTemplateService);
+    private compactPipelineService = inject(CompactPipelineService);
+    private assetSaveService = inject(AssetSaveService);
+    private dataLakeService = inject(DatalakeRestService);
 
     adapterInstalled = false;
 
@@ -110,16 +115,6 @@ export class AdapterStartedDialog implements OnInit {
     addToAssetText = '';
     deletedFromAssetText = '';
 
-    constructor(
-        public dialogRef: DialogRef<AdapterStartedDialog>,
-        private adapterService: AdapterService,
-        private shepherdService: ShepherdService,
-        private pipelineTemplateService: PipelineTemplateService,
-        private compactPipelineService: CompactPipelineService,
-        private assetSaveService: AssetSaveService,
-        private dataLakeService: DatalakeRestService,
-    ) {}
-
     ngOnInit() {
         if (this.editMode) {
             this.initAdapterUpdatePreflight();
@@ -205,7 +200,6 @@ export class AdapterStartedDialog implements OnInit {
                     } else {
                         this.startAdapter(adapterElementId, true);
                         this.addToAsset();
-                        this.addToAsset();
                     }
                 } else {
                     const errorMsg: SpLogMessage =
@@ -345,9 +339,9 @@ export class AdapterStartedDialog implements OnInit {
             name: pipelineId,
         });
 
-        const res = await this.dataLakeService
-            .getMeasurementByName(adapter.name)
-            .toPromise();
+        const res = await lastValueFrom(
+            this.dataLakeService.getMeasurementByName(adapter.name),
+        );
 
         linkageData.push({
             type: 'measurement',
@@ -430,7 +424,6 @@ export class AdapterStartedDialog implements OnInit {
                                     pipelineOperationStatus;
                                 this.startAdapter(adapterElementId, true);
                                 this.addToAsset();
-                                this.addToAsset();
                             },
                             error => {
                                 this.onAdapterFailure(error.error);
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.html
 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.html
index 320ff93ad3..cb88a730c0 100644
--- 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.html
+++ 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.html
@@ -122,6 +122,25 @@
     >
         Navigate to pipeline overview afterwards
     </mat-checkbox>
+    <mat-checkbox
+        [(ngModel)]="addToAssets"
+        color="accent"
+        data-cy="sp-show-pipeline-asset-checkbox"
+    >
+        Add Pipeline to Assets
+    </mat-checkbox>
+    @if (addToAssets) {
+        <div class="mt-10">
+            <sp-asset-link-configuration
+                [isEdit]="storageOptions.updateMode === 'update'"
+                [itemId]="pipeline._id"
+                (selectedAssetsChange)="onSelectedAssetsChange($event)"
+                (deselectedAssetsChange)="onDeselectedAssetsChange($event)"
+                (originalAssetsEmitter)="onOriginalAssetsEmitted($event)"
+            >
+            </sp-asset-link-configuration>
+        </div>
+    }
     <div class="mt-10">
         <mat-expansion-panel class="mat-elevation-z0 border-1">
             <mat-expansion-panel-header
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.ts
 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.ts
index e60883b7cc..59531bfa91 100644
--- 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.ts
+++ 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.ts
@@ -16,7 +16,14 @@
  *
  */
 
-import { Component, Input, OnInit } from '@angular/core';
+import {
+    Component,
+    EventEmitter,
+    inject,
+    Input,
+    OnInit,
+    Output,
+} from '@angular/core';
 import { ShepherdService } from '../../../../services/tour/shepherd.service';
 import {
     UntypedFormControl,
@@ -27,6 +34,7 @@ import {
     CompactPipeline,
     Pipeline,
     PipelineService,
+    SpAssetTreeNode,
 } from '@streampipes/platform-services';
 import { PipelineStorageOptions } from '../../../model/editor.model';
 import { ValidateName } from 
'../../../../core-ui/static-properties/input.validator';
@@ -50,12 +58,22 @@ export class SavePipelineSettingsComponent implements 
OnInit {
     @Input()
     currentPipelineName: string;
 
+    private shepherdService = inject(ShepherdService);
+    private pipelineService = inject(PipelineService);
+
     compactPipeline: CompactPipeline;
 
-    constructor(
-        private shepherdService: ShepherdService,
-        private pipelineService: PipelineService,
-    ) {}
+    addToAssets: boolean = false;
+    @Input()
+    selectedAssets: SpAssetTreeNode[];
+    @Input()
+    deselectedAssets: SpAssetTreeNode[];
+    @Input()
+    originalAssets: SpAssetTreeNode[];
+
+    @Output() selectedAssetsChange = new EventEmitter<SpAssetTreeNode[]>();
+    @Output() deselectedAssetsChange = new EventEmitter<SpAssetTreeNode[]>();
+    @Output() originalAssetsChange = new EventEmitter<SpAssetTreeNode[]>();
 
     ngOnInit() {
         this.submitPipelineForm.addControl(
@@ -90,6 +108,21 @@ export class SavePipelineSettingsComponent implements 
OnInit {
             .subscribe(p => (this.compactPipeline = p));
     }
 
+    onSelectedAssetsChange(updatedAssets: SpAssetTreeNode[]): void {
+        this.selectedAssets = updatedAssets;
+        this.selectedAssetsChange.emit(this.selectedAssets);
+    }
+
+    onDeselectedAssetsChange(updatedAssets: SpAssetTreeNode[]): void {
+        this.deselectedAssets = updatedAssets;
+        this.deselectedAssetsChange.emit(this.deselectedAssets);
+    }
+
+    onOriginalAssetsEmitted(updatedAssets: SpAssetTreeNode[]): void {
+        this.originalAssets = updatedAssets;
+        this.originalAssetsChange.emit(this.originalAssets);
+    }
+
     triggerTutorial() {
         if (this.shepherdService.isTourActive()) {
             this.shepherdService.trigger('save-pipeline-dialog');
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html
index a2bd2573e1..684b3a23c6 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html
+++ b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html
@@ -25,6 +25,9 @@
                 [submitPipelineForm]="submitPipelineForm"
                 [pipeline]="pipeline"
                 [storageOptions]="storageOptions"
+                [(selectedAssets)]="selectedAssets"
+                [(deselectedAssets)]="deselectedAssets"
+                [(originalAssets)]="originalAssets"
             >
             </sp-save-pipeline-settings>
 
diff --git a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts
index fc603bf532..efe116e521 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts
+++ b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts
@@ -16,15 +16,19 @@
  *
  */
 
-import { Component, Input, OnInit } from '@angular/core';
-import { DialogRef } from '@streampipes/shared-ui';
+import { Component, inject, Input, OnInit } from '@angular/core';
+import { AssetSaveService, DialogRef } from '@streampipes/shared-ui';
 import {
+    DatalakeRestService,
+    DataSinkInvocation,
+    LinkageData,
     Message,
     Pipeline,
     PipelineCanvasMetadata,
     PipelineCanvasMetadataService,
     PipelineOperationStatus,
     PipelineService,
+    SpAssetTreeNode,
 } from '@streampipes/platform-services';
 import { EditorService } from '../../services/editor.service';
 import { ShepherdService } from '../../../services/tour/shepherd.service';
@@ -35,7 +39,7 @@ import {
     PipelineStorageOptions,
 } from '../../model/editor.model';
 import { IdGeneratorService } from 
'../../../core-services/id-generator/id-generator.service';
-import { Observable, of, tap } from 'rxjs';
+import { firstValueFrom, lastValueFrom, Observable, of, tap } from 'rxjs';
 import { filter, switchMap } from 'rxjs/operators';
 import {
     Status,
@@ -50,12 +54,26 @@ import { PipelineAction } from 
'../../../pipelines/model/pipeline-model';
     standalone: false,
 })
 export class SavePipelineComponent implements OnInit {
+    private editorService = inject(EditorService);
+    private dialogRef = inject(DialogRef<SavePipelineComponent>);
+    private idGeneratorService = inject(IdGeneratorService);
+    private pipelineService = inject(PipelineService);
+    private router = inject(Router);
+    private shepherdService = inject(ShepherdService);
+    private pipelineCanvasService = inject(PipelineCanvasMetadataService);
+    private assetSaveService = inject(AssetSaveService);
+    private dataLakeService = inject(DatalakeRestService);
+
     @Input()
     pipeline: Pipeline;
 
     @Input()
     originalPipeline: Pipeline;
 
+    selectedAssets: SpAssetTreeNode[];
+    deselectedAssets: SpAssetTreeNode[];
+    originalAssets: SpAssetTreeNode[];
+
     @Input()
     pipelineCanvasMetadata: PipelineCanvasMetadata;
 
@@ -78,16 +96,6 @@ export class SavePipelineComponent implements OnInit {
     finalPipelineOperationStatus: PipelineOperationStatus;
     pipelineAction: PipelineAction;
 
-    constructor(
-        private editorService: EditorService,
-        private dialogRef: DialogRef<SavePipelineComponent>,
-        private idGeneratorService: IdGeneratorService,
-        private pipelineService: PipelineService,
-        private router: Router,
-        private shepherdService: ShepherdService,
-        private pipelineCanvasService: PipelineCanvasMetadataService,
-    ) {}
-
     ngOnInit() {
         this.storageOptions.updateModeActive =
             this.originalPipeline !== undefined;
@@ -139,7 +147,11 @@ export class SavePipelineComponent implements OnInit {
                 switchMap(() => this.getStartPipeline$()),
             )
             .subscribe({
-                next: message => this.onSuccess(message),
+                next: message => {
+                    this.onSuccess(message);
+                    // Add Asset as soon as pipelineId is known
+                    this.addToAsset();
+                },
                 error: msg => {
                     this.onFailure(msg);
                 },
@@ -174,6 +186,7 @@ export class SavePipelineComponent implements OnInit {
                 );
             }
         }
+
         this.performStorageOperations(stopPipeline$, savePipeline$);
     }
 
@@ -316,4 +329,59 @@ export class SavePipelineComponent implements OnInit {
         }
         this.dialogRef.close(reloadConfig);
     }
+
+    async addToAsset(): Promise<void> {
+        let linkageData: LinkageData[] = [];
+        linkageData = await this.addPipelineLinkageData(linkageData);
+
+        await this.saveAssets(linkageData);
+    }
+    private async addPipelineLinkageData(
+        linkageData: LinkageData[],
+    ): Promise<LinkageData[]> {
+        const pipeline = await firstValueFrom(
+            this.pipelineService.getPipelineById(this.pipelineId),
+        );
+
+        linkageData.push({
+            type: 'pipeline',
+            id: this.pipelineId,
+            name: pipeline.name,
+        });
+
+        const serviceList: DataSinkInvocation[] =
+            pipeline.actions as DataSinkInvocation[];
+        const dataSinkServices: DataSinkInvocation[] = serviceList.filter(
+            action => action.serviceTagPrefix === 'DATA_SINK',
+        );
+
+        for (const service of dataSinkServices) {
+            const staticProperty = service.staticProperties.find(
+                prop => prop.internalName === 'db_measurement',
+            );
+
+            const measureFromPipeline = (staticProperty as { value: string })
+                .value;
+
+            const measure = await lastValueFrom(
+                this.dataLakeService.getMeasurementByName(measureFromPipeline),
+            );
+
+            linkageData.push({
+                type: 'measurement',
+                id: measure.elementId,
+                name: measureFromPipeline,
+            });
+        }
+        return linkageData;
+    }
+
+    private async saveAssets(linkageData: LinkageData[]): Promise<void> {
+        await this.assetSaveService.saveSelectedAssets(
+            this.selectedAssets,
+            linkageData,
+            this.deselectedAssets,
+            this.originalAssets,
+        );
+    }
 }
diff --git a/ui/src/app/editor/editor.module.ts 
b/ui/src/app/editor/editor.module.ts
index f9c735cef7..f78a1691c2 100644
--- a/ui/src/app/editor/editor.module.ts
+++ b/ui/src/app/editor/editor.module.ts
@@ -125,6 +125,7 @@ import { TranslatePipe } from '@ngx-translate/core';
         PlatformServicesModule,
         SharedUiModule,
         TranslatePipe,
+        SharedUiModule,
     ],
     declarations: [
         AddTemplateDialogComponent,

Reply via email to