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"
+ > {{ 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),
+ );
+ });
+ }
+}