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;