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 a7d4813d8c feat: add clone button to data explorer overview (#3594)
a7d4813d8c is described below

commit a7d4813d8cf0f9922f3455a131709d3bb07b9ac3
Author: Marcel Früholz <[email protected]>
AuthorDate: Wed May 7 08:41:17 2025 +0200

    feat: add clone button to data explorer overview (#3594)
    
    * feat: add clone button to data explorer overview
    
    * fix: Change formatting in resource
    
    ---------
    
    Co-authored-by: Philipp Zehnder <[email protected]>
---
 .../apache/streampipes/rest/impl/UserResource.java | 100 +++++++++++++--------
 ui/deployment/i18n/de.json                         |   4 +-
 ui/deployment/i18n/en.json                         |   4 +-
 .../src/lib/apis/chart.service.ts                  |  32 ++++++-
 .../data-explorer-overview-table.component.html    |   9 ++
 .../data-explorer-overview-table.component.ts      |   6 ++
 6 files changed, 115 insertions(+), 40 deletions(-)

diff --git 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java
 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java
index 7f65202dc8..201aa8cf76 100644
--- 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java
+++ 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java
@@ -166,8 +166,10 @@ public class UserResource extends 
AbstractAuthGuardedRestResource {
       path = "{userId}/tokens",
       produces = MediaType.APPLICATION_JSON_VALUE,
       consumes = MediaType.APPLICATION_JSON_VALUE)
-  public ResponseEntity<?> createNewApiToken(@PathVariable("userId") String 
username,
-                                             @RequestBody RawUserApiToken 
rawToken) {
+  public ResponseEntity<?> createNewApiToken(
+      @PathVariable("userId") String username,
+      @RequestBody RawUserApiToken rawToken
+  ) {
     String authenticatedUserName = getAuthenticatedUsername();
     if (authenticatedUserName.equals(username)) {
       RawUserApiToken generatedToken = new 
TokenService().createAndStoreNewToken(username, rawToken);
@@ -181,15 +183,21 @@ public class UserResource extends 
AbstractAuthGuardedRestResource {
       path = "user/{principalId}",
       produces = MediaType.APPLICATION_JSON_VALUE,
       consumes = MediaType.APPLICATION_JSON_VALUE)
-  public ResponseEntity<?> 
updateUserAccountDetails(@PathVariable("principalId") String principalId,
-                                                    @RequestBody UserAccount 
user) {
+  public ResponseEntity<?> updateUserAccountDetails(
+      @PathVariable("principalId") String principalId,
+      @RequestBody UserAccount user
+  ) {
     String authenticatedUserId = getAuthenticatedUserSid();
     if (user != null && (authenticatedUserId.equals(principalId) || 
isAdmin())) {
       UserAccount existingUser = (UserAccount) getPrincipalById(principalId);
-      updateUser(existingUser, user, isAdmin(), existingUser.getPassword());
-      user.setRev(existingUser.getRev());
-      getUserStorage().updateUser(user);
-      return ok(Notifications.success("User updated"));
+      if (isUsernameAvailable(existingUser.getUsername())) {
+        updateUser(existingUser, user, isAdmin(), existingUser.getPassword());
+        user.setRev(existingUser.getRev());
+        getUserStorage().updateUser(user);
+        return ok(Notifications.success("User updated"));
+      } else {
+        return badRequest(Notifications.error("Username is not available"));
+      }
     } else {
       return statusMessage(Notifications.error("User not found"));
     }
@@ -199,8 +207,10 @@ public class UserResource extends 
AbstractAuthGuardedRestResource {
       path = "user/{principalId}/username",
       produces = MediaType.APPLICATION_JSON_VALUE,
       consumes = MediaType.APPLICATION_JSON_VALUE)
-  public ResponseEntity<?> updateUsername(@PathVariable("principalId") String 
principalId,
-                                 @RequestBody UserAccount user) {
+  public ResponseEntity<?> updateUsername(
+      @PathVariable("principalId") String principalId,
+      @RequestBody UserAccount user
+  ) {
     String authenticatedUserId = getAuthenticatedUserSid();
     if (user != null && (authenticatedUserId.equals(principalId) || 
isAdmin())) {
       UserAccount existingUser = (UserAccount) getPrincipalById(principalId);
@@ -208,15 +218,12 @@ public class UserResource extends 
AbstractAuthGuardedRestResource {
         if (PasswordUtil.validatePassword(user.getPassword(), 
existingUser.getPassword())) {
           existingUser.setUsername(user.getUsername());
 
-          if (getUserStorage()
-              .getAllUserAccounts()
-              .stream()
-              .noneMatch(u -> 
u.getUsername().equalsIgnoreCase(user.getUsername()))) {
+          if (isUsernameAvailable(user.getUsername())) {
             updateUser(existingUser, user, isAdmin(), 
existingUser.getPassword());
             getUserStorage().updateUser(existingUser);
             return ok();
           } else {
-            return badRequest(Notifications.error("Username already taken"));
+            return badRequest(Notifications.error("Username is not 
available"));
           }
         } else {
           return badRequest(Notifications.error("Incorrect password"));
@@ -226,14 +233,17 @@ public class UserResource extends 
AbstractAuthGuardedRestResource {
       }
     }
 
-    return ResponseEntity.status(HttpStatus.SC_UNAUTHORIZED).build();
+    return ResponseEntity.status(HttpStatus.SC_UNAUTHORIZED)
+                         .build();
   }
 
   @PutMapping(path = "user/{principalId}/password",
-      produces = MediaType.APPLICATION_JSON_VALUE,
-      consumes = MediaType.APPLICATION_JSON_VALUE)
-  public ResponseEntity<?> updatePassword(@PathVariable("principalId") String 
principalId,
-                                 @RequestBody ChangePasswordRequest 
passwordRequest) {
+              produces = MediaType.APPLICATION_JSON_VALUE,
+              consumes = MediaType.APPLICATION_JSON_VALUE)
+  public ResponseEntity<?> updatePassword(
+      @PathVariable("principalId") String principalId,
+      @RequestBody ChangePasswordRequest passwordRequest
+  ) {
     String authenticatedUserId = getAuthenticatedUserSid();
     UserAccount existingUser = (UserAccount) getPrincipalById(principalId);
     if (principalId.equals(authenticatedUserId) || isAdmin()) {
@@ -260,8 +270,10 @@ public class UserResource extends 
AbstractAuthGuardedRestResource {
       path = "service/{principalId}",
       produces = MediaType.APPLICATION_JSON_VALUE,
       consumes = MediaType.APPLICATION_JSON_VALUE)
-  public ResponseEntity<? extends Message> 
updateServiceAccountDetails(@PathVariable("principalId") String principalId,
-                                              @RequestBody ServiceAccount 
user) {
+  public ResponseEntity<? extends Message> updateServiceAccountDetails(
+      @PathVariable("principalId") String principalId,
+      @RequestBody ServiceAccount user
+  ) {
     String authenticatedUserId = getAuthenticatedUserSid();
     if (user != null && (authenticatedUserId.equals(principalId) || 
isAdmin())) {
       Principal existingUser = getPrincipalById(principalId);
@@ -286,16 +298,20 @@ public class UserResource extends 
AbstractAuthGuardedRestResource {
         .getAuthentication()
         .getAuthorities()
         .stream()
-        .anyMatch(r -> r.getAuthority().equals(DefaultRole.ROLE_ADMIN.name()));
+        .anyMatch(r -> r.getAuthority()
+                        .equals(DefaultRole.ROLE_ADMIN.name()));
   }
 
-  private void updateUser(UserAccount existingUser,
-                          UserAccount user,
-                          boolean adminPrivileges,
-                          String property) {
+  private void updateUser(
+      UserAccount existingUser,
+      UserAccount user,
+      boolean adminPrivileges,
+      String property
+  ) {
     user.setPassword(property);
     user.setProvider(existingUser.getProvider());
-    if (!existingUser.getProvider().equals(UserAccount.LOCAL)) {
+    if (!existingUser.getProvider()
+                     .equals(UserAccount.LOCAL)) {
       // These settings are managed externally
       user.setUsername(existingUser.getUsername());
       user.setFullName(existingUser.getFullName());
@@ -304,18 +320,28 @@ public class UserResource extends 
AbstractAuthGuardedRestResource {
       replacePermissions(user, existingUser);
     }
     user.setUserApiTokens(existingUser
-        .getUserApiTokens()
+                              .getUserApiTokens()
+                              .stream()
+                              .filter(existingToken -> user.getUserApiTokens()
+                                                           .stream()
+                                                           
.anyMatch(updatedToken -> existingToken
+                                                               .getTokenId()
+                                                               
.equals(updatedToken.getTokenId())))
+                              .collect(Collectors.toList()));
+  }
+
+  private boolean isUsernameAvailable(String username) {
+    return getUserStorage()
+        .getAllUserAccounts()
         .stream()
-        .filter(existingToken -> user.getUserApiTokens()
-            .stream()
-            .anyMatch(updatedToken -> existingToken
-                .getTokenId()
-                .equals(updatedToken.getTokenId())))
-        .collect(Collectors.toList()));
+        .noneMatch(u -> u.getUsername()
+                         .equalsIgnoreCase(username));
   }
 
-  private void encryptAndStore(UserAccount userAccount,
-                               String property) throws 
NoSuchAlgorithmException, InvalidKeySpecException {
+  private void encryptAndStore(
+      UserAccount userAccount,
+      String property
+  ) throws NoSuchAlgorithmException, InvalidKeySpecException {
     String encryptedProperty = PasswordUtil.encryptPassword(property);
     userAccount.setPassword(encryptedProperty);
     getUserStorage().storeUser(userAccount);
diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index 5352dd885b..5b08c9c1d0 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -155,6 +155,7 @@
   "Edit chart": "Diagramm bearbeiten",
   "Manage permissions": "Berechtigungen verwalten",
   "Delete chart": "Diagramm löschen",
+  "Clone chart": "Diagramm kopieren",
   "Chart Name": "Diagrammname",
   "Save": "Speichern",
   "Discard": "Verwerfen",
@@ -410,5 +411,6 @@
   "30 min": "30 Minuten",
   "Error Details": "Fehler-Details",
   "Resources": "Ressourcen",
-  "All {{allResourcesAlias}}": "Alle {{allResourcesAlias}}"
+  "All {{allResourcesAlias}}": "Alle {{allResourcesAlias}}",
+  "{{ widgetTitle }} Clone": "{{ widgetTitle }} Kopie"
 }
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index 9264e48626..fd8a9cc6c0 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -155,6 +155,7 @@
   "Edit chart": null,
   "Manage permissions": null,
   "Delete chart": null,
+  "Clone chart": null,
   "Chart Name": null,
   "Save": null,
   "Discard": null,
@@ -410,5 +411,6 @@
   "30 min": null,
   "Error Details": null,
   "Resources": null,
-  "All {{allResourcesAlias}}": "All {{allResourcesAlias}}"
+  "All {{allResourcesAlias}}": "All {{allResourcesAlias}}",
+  "{{ widgetTitle }} Clone": "{{ widgetTitle }} Clone"
 }
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/apis/chart.service.ts 
b/ui/projects/streampipes/platform-services/src/lib/apis/chart.service.ts
index 357b2fe655..f14946c697 100644
--- a/ui/projects/streampipes/platform-services/src/lib/apis/chart.service.ts
+++ b/ui/projects/streampipes/platform-services/src/lib/apis/chart.service.ts
@@ -24,12 +24,16 @@ import {
     DataExplorerWidgetModel,
     DataLakeMeasure,
 } from '../model/gen/streampipes-model';
+import { TranslateService } from '@ngx-translate/core';
 
 @Injectable({
     providedIn: 'root',
 })
 export class ChartService {
-    constructor(private http: HttpClient) {}
+    constructor(
+        private http: HttpClient,
+        private translateService: TranslateService,
+    ) {}
 
     getAllCharts(): Observable<DataExplorerWidgetModel[]> {
         return this.http
@@ -66,6 +70,32 @@ export class ChartService {
         return this.http.delete(`${this.dashboardWidgetUrl}/${widgetId}`);
     }
 
+    cloneChart(
+        widget: DataExplorerWidgetModel,
+    ): Observable<DataExplorerWidgetModel> {
+        const clone = JSON.parse(JSON.stringify(widget));
+        clone.elementId = undefined;
+        clone.rev = undefined;
+
+        clone.baseAppearanceConfig.widgetTitle = this.translateService.instant(
+            '{{ widgetTitle }} Clone',
+            { widgetTitle: widget.baseAppearanceConfig.widgetTitle },
+        );
+
+        clone.metadata = {
+            createdAtEpochMs: Date.now(),
+            lastModifiedEpochMs: Date.now(),
+        };
+
+        return this.http.post(this.dashboardWidgetUrl, clone).pipe(
+            map(response => {
+                return DataExplorerWidgetModel.fromData(
+                    response as DataExplorerWidgetModel,
+                );
+            }),
+        );
+    }
+
     updateChart(widget: DataExplorerWidgetModel): Observable<any> {
         return this.http.put(
             this.dashboardWidgetUrl + '/' + widget.elementId,
diff --git 
a/ui/src/app/data-explorer/components/overview/data-explorer-overview-table/data-explorer-overview-table.component.html
 
b/ui/src/app/data-explorer/components/overview/data-explorer-overview-table/data-explorer-overview-table.component.html
index 0f6581437f..65a615065c 100644
--- 
a/ui/src/app/data-explorer/components/overview/data-explorer-overview-table/data-explorer-overview-table.component.html
+++ 
b/ui/src/app/data-explorer/components/overview/data-explorer-overview-table/data-explorer-overview-table.component.html
@@ -143,6 +143,15 @@
                         >
                             <i class="material-icons">share</i>
                         </button>
+                        <button
+                            mat-icon-button
+                            color="accent"
+                            [matTooltip]="'Clone chart' | translate"
+                            *ngIf="hasDataExplorerWritePrivileges"
+                            (click)="cloneDataView(element)"
+                        >
+                            <i class="material-icons">flip_to_front</i>
+                        </button>
                         <button
                             mat-icon-button
                             color="accent"
diff --git 
a/ui/src/app/data-explorer/components/overview/data-explorer-overview-table/data-explorer-overview-table.component.ts
 
b/ui/src/app/data-explorer/components/overview/data-explorer-overview-table/data-explorer-overview-table.component.ts
index c8b9d12733..4080dd632e 100644
--- 
a/ui/src/app/data-explorer/components/overview/data-explorer-overview-table/data-explorer-overview-table.component.ts
+++ 
b/ui/src/app/data-explorer/components/overview/data-explorer-overview-table/data-explorer-overview-table.component.ts
@@ -131,6 +131,12 @@ export class SpDataExplorerDataViewOverviewComponent 
extends SpDataExplorerOverv
         });
     }
 
+    cloneDataView(dataView: DataExplorerWidgetModel) {
+        this.dataViewService.cloneChart(dataView).subscribe(() => {
+            this.getDataViews();
+        });
+    }
+
     applyChartFilters(elementIds: Set<string> = new Set<string>()): void {
         if (elementIds.size == 0) {
             this.filteredCharts = this.charts;

Reply via email to