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

riemer pushed a commit to branch add-public-link-creation-to-dashboard
in repository https://gitbox.apache.org/repos/asf/streampipes.git

commit 8fd7050d00229cd9e6eeaf6e821b71fbd53d53d4
Author: Dominik Riemer <[email protected]>
AuthorDate: Thu Aug 21 18:55:03 2025 +0200

    feat(#3725): Add feature to mark dashboards as public
---
 .../streampipes/model/client/user/Permission.java  |   9 +
 .../management/PermissionResourceManager.java      |   8 +
 .../rest/security/SpPermissionEvaluator.java       | 122 +++++---
 .../service/core/UnauthenticatedInterfaces.java    |   3 +-
 .../management/util/GrantedPermissionsBuilder.java |   2 +-
 .../src/lib/model/gen/streampipes-model-client.ts  |   4 +-
 .../existing-adapters.component.html               |   2 +-
 .../object-permission-dialog.component.html        | 325 ++++++++++++---------
 .../object-permission-dialog.component.scss        |   9 +
 .../object-permission-dialog.component.ts          |  40 ++-
 .../dashboard-overview-table.component.html        |   2 +-
 .../dashboard-overview-table.component.ts          |   6 +
 .../services/data-explorer-shared.service.ts       |   9 +-
 .../data-explorer-overview-table.component.html    |   2 +-
 ui/src/app/pipelines/pipelines.component.html      |   4 +-
 15 files changed, 353 insertions(+), 194 deletions(-)

diff --git 
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Permission.java
 
b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Permission.java
index 4e20ca0daa..6bbec97c34 100644
--- 
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Permission.java
+++ 
b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Permission.java
@@ -40,6 +40,7 @@ public class Permission implements Storable {
   private String objectInstanceId;
   private String objectClassName;
   private boolean publicElement;
+  private boolean readAnonymous;
 
   private String ownerSid;
 
@@ -126,4 +127,12 @@ public class Permission implements Storable {
   public void setPublicElement(boolean publicElement) {
     this.publicElement = publicElement;
   }
+
+  public boolean isReadAnonymous() {
+    return readAnonymous;
+  }
+
+  public void setReadAnonymous(boolean readAnonymous) {
+    this.readAnonymous = readAnonymous;
+  }
 }
diff --git 
a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/PermissionResourceManager.java
 
b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/PermissionResourceManager.java
index 0d48e15949..53b5a36f5c 100644
--- 
a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/PermissionResourceManager.java
+++ 
b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/PermissionResourceManager.java
@@ -19,6 +19,7 @@ package org.apache.streampipes.resource.management;
 
 import org.apache.streampipes.model.client.user.Permission;
 import org.apache.streampipes.model.client.user.PermissionBuilder;
+import org.apache.streampipes.model.dashboard.DashboardModel;
 import org.apache.streampipes.storage.api.IPermissionStorage;
 import org.apache.streampipes.storage.management.StorageDispatcher;
 
@@ -26,6 +27,10 @@ import java.util.List;
 
 public class PermissionResourceManager extends 
AbstractResourceManager<IPermissionStorage> {
 
+  private final List<String> readAnonymousAllowedClasses = List.of(
+      DashboardModel.class.getCanonicalName()
+  );
+
   public PermissionResourceManager() {
     super(StorageDispatcher.INSTANCE.getNoSqlStore().getPermissionStorage());
   }
@@ -59,6 +64,9 @@ public class PermissionResourceManager extends 
AbstractResourceManager<IPermissi
   }
 
   public void update(Permission permission) {
+    if 
(!readAnonymousAllowedClasses.contains(permission.getObjectClassName())) {
+      permission.setReadAnonymous(false);
+    }
     db.updateElement(permission);
   }
 
diff --git 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/security/SpPermissionEvaluator.java
 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/security/SpPermissionEvaluator.java
index 86c0a74d80..cfe8bcb6a2 100644
--- 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/security/SpPermissionEvaluator.java
+++ 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/security/SpPermissionEvaluator.java
@@ -21,20 +21,29 @@ import org.apache.streampipes.model.client.user.DefaultRole;
 import org.apache.streampipes.model.client.user.Permission;
 import org.apache.streampipes.model.pipeline.PipelineElementRecommendation;
 import 
org.apache.streampipes.model.pipeline.PipelineElementRecommendationMessage;
+import org.apache.streampipes.storage.api.IPermissionStorage;
 import org.apache.streampipes.storage.management.StorageDispatcher;
 import org.apache.streampipes.user.management.model.PrincipalUserDetails;
 
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.access.PermissionEvaluator;
+import 
org.springframework.security.authentication.AnonymousAuthenticationToken;
 import org.springframework.security.core.Authentication;
 
 import java.io.Serializable;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Predicate;
 
 @Configuration
 public class SpPermissionEvaluator implements PermissionEvaluator {
 
+  private final IPermissionStorage permissionStorage;
+
+  public SpPermissionEvaluator() {
+    this.permissionStorage = 
StorageDispatcher.INSTANCE.getNoSqlStore().getPermissionStorage();
+  }
+
   /**
    * Evaluates whether the user has the necessary permissions for a given 
resource.
    *
@@ -50,19 +59,22 @@ public class SpPermissionEvaluator implements 
PermissionEvaluator {
       Object targetDomainObject,
       Object permission
   ) {
-    PrincipalUserDetails<?> userDetails = getUserDetails(authentication);
-    if (targetDomainObject instanceof PipelineElementRecommendationMessage) {
-      return isAdmin(userDetails) || filterRecommendation(
-          authentication,
-          (PipelineElementRecommendationMessage) targetDomainObject
-      );
-    } else {
-      String objectInstanceId = (String) targetDomainObject;
-      if (isAdmin(userDetails)) {
-        return true;
-      }
-      return hasPermission(authentication, objectInstanceId);
+    if (targetDomainObject instanceof PipelineElementRecommendationMessage 
msg) {
+      return handleRecommendationMessage(authentication, msg);
+    }
+
+    String objectId = String.valueOf(targetDomainObject);
+    List<Permission> perms = getObjectPermission(objectId);
+
+    if (isAnonymousAccess(perms)) {
+      return true;
     }
+
+    if (isAdmin(authentication)) {
+      return true;
+    }
+
+    return hasPermissionForId(authentication, perms, objectId);
   }
 
   /**
@@ -81,45 +93,75 @@ public class SpPermissionEvaluator implements 
PermissionEvaluator {
       String targetType,
       Object permission
   ) {
-    PrincipalUserDetails<?> userDetails = getUserDetails(authentication);
-    if (isAdmin(userDetails)) {
-      return true;
-    }
-    return hasPermission(authentication, targetId.toString());
+    // We do not use targetType in this implementation
+    return hasPermission(authentication, targetId, permission);
   }
 
-  private boolean filterRecommendation(Authentication auth, 
PipelineElementRecommendationMessage message) {
-    Predicate<PipelineElementRecommendation> isForbidden = r -> 
!hasPermission(auth, r.getElementId());
-    message.getPossibleElements()
-           .removeIf(isForbidden);
+  private boolean handleRecommendationMessage(Authentication auth,
+                                              
PipelineElementRecommendationMessage message) {
+    Predicate<PipelineElementRecommendation> isForbidden = rec -> {
+      String elementId = rec.getElementId();
+      List<Permission> perms = getObjectPermission(elementId);
 
+      if (isAnonymousAccess(perms)) {
+        return false;
+      }
+      if (isAdmin(auth)) {
+        return false;
+      }
+      return !hasPermissionForId(auth, perms, elementId); // remove if not 
allowed
+    };
+
+    message.getPossibleElements().removeIf(isForbidden);
     return true;
   }
 
-  private boolean hasPermission(Authentication auth, String objectInstanceId) {
-    return isPublicElement(objectInstanceId)
-        || getUserDetails(auth).getAllObjectPermissions()
-                               .contains(objectInstanceId);
+  private boolean hasPermissionForId(Authentication auth,
+                                     List<Permission> permissions,
+                                     String objectInstanceId) {
+    if (isPublicOrAnonymousElement(permissions)) {
+      return true;
+    }
+
+    PrincipalUserDetails<?> user = getUserDetailsOrNull(auth);
+    if (user == null) {
+      return false;
+    }
+
+    return user.getAllObjectPermissions().contains(objectInstanceId);
+  }
+
+  private PrincipalUserDetails<?> getUserDetailsOrNull(Authentication 
authentication) {
+    if (authentication == null
+        || authentication instanceof AnonymousAuthenticationToken) {
+      return null;
+    }
+    Object principal = authentication.getPrincipal();
+    return (principal instanceof PrincipalUserDetails) ? 
(PrincipalUserDetails<?>) principal : null;
+  }
+
+  private boolean isAdmin(Authentication authentication) {
+    PrincipalUserDetails<?> userDetails = getUserDetailsOrNull(authentication);
+    if (userDetails == null) {
+      return false;
+    }
+
+    return userDetails.getAuthorities().stream()
+        .anyMatch(a ->
+            Objects.equals(a.getAuthority(), 
DefaultRole.Constants.ROLE_ADMIN_VALUE)
+        );
   }
 
-  private PrincipalUserDetails<?> getUserDetails(Authentication 
authentication) {
-    return (PrincipalUserDetails<?>) authentication.getPrincipal();
+  private boolean isPublicOrAnonymousElement(List<Permission> permissions) {
+    return !permissions.isEmpty()
+        && (permissions.get(0).isPublicElement() || 
permissions.get(0).isReadAnonymous());
   }
 
-  private boolean isPublicElement(String objectInstanceId) {
-    List<Permission> permissions =
-        StorageDispatcher.INSTANCE.getNoSqlStore()
-                                  .getPermissionStorage()
-                                  
.getUserPermissionsForObject(objectInstanceId);
-    return permissions.size() > 0 && permissions.get(0)
-                                                .isPublicElement();
+  private boolean isAnonymousAccess(List<Permission> permissions) {
+    return !permissions.isEmpty() && permissions.get(0).isReadAnonymous();
   }
 
-  private boolean isAdmin(PrincipalUserDetails<?> userDetails) {
-    return userDetails
-        .getAuthorities()
-        .stream()
-        .anyMatch(a -> a.getAuthority()
-                        .equals(DefaultRole.Constants.ROLE_ADMIN_VALUE));
+  private List<Permission> getObjectPermission(String objectInstanceId) {
+    return permissionStorage.getUserPermissionsForObject(objectInstanceId);
   }
 }
diff --git 
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/UnauthenticatedInterfaces.java
 
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/UnauthenticatedInterfaces.java
index ebadac6489..359de1170d 100644
--- 
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/UnauthenticatedInterfaces.java
+++ 
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/UnauthenticatedInterfaces.java
@@ -42,7 +42,8 @@ public class UnauthenticatedInterfaces {
         "/error",
         "/",
         "/streampipes-backend/",
-        "/streampipes-backend/index.html"
+        "/streampipes-backend/index.html",
+        "/api/v3/datalake/dashboard/*/composite"
     );
   }
 }
diff --git 
a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/util/GrantedPermissionsBuilder.java
 
b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/util/GrantedPermissionsBuilder.java
index 83fbc85d28..1674849767 100644
--- 
a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/util/GrantedPermissionsBuilder.java
+++ 
b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/util/GrantedPermissionsBuilder.java
@@ -25,7 +25,7 @@ import java.util.Set;
 
 public class GrantedPermissionsBuilder {
 
-  private Principal principal;
+  private final Principal principal;
 
   public GrantedPermissionsBuilder(Principal principal) {
     this.principal = principal;
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
 
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
index 09c378e914..bb58115952 100644
--- 
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
+++ 
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
@@ -20,7 +20,7 @@
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2025-08-19 
20:00:50.
+// Generated using typescript-generator version 3.2.1263 on 2025-08-21 
14:22:05.
 
 import { Storable } from './streampipes-model';
 
@@ -83,6 +83,7 @@ export class Permission implements Storable {
     ownerSid: string;
     permissionId: string;
     publicElement: boolean;
+    readAnonymous: boolean;
     rev: string;
 
     static fromData(data: Permission, target?: Permission): Permission {
@@ -99,6 +100,7 @@ export class Permission implements Storable {
         instance.ownerSid = data.ownerSid;
         instance.permissionId = data.permissionId;
         instance.publicElement = data.publicElement;
+        instance.readAnonymous = data.readAnonymous;
         instance.rev = data.rev;
         return instance;
     }
diff --git 
a/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html
 
b/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html
index 25d93d4d95..057e54fd02 100644
--- 
a/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html
+++ 
b/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html
@@ -99,7 +99,7 @@
             ></sp-basic-header-title-component>
             <div fxFlex="100" fxLayout="row" fxLayoutAlign="center start">
                 <sp-table
-                    fxFlex="90"
+                    fxFlex="100"
                     [columns]="displayedColumns"
                     [dataSource]="dataSource"
                     data-cy="all-adapters-table"
diff --git 
a/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.html
 
b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.html
index 460e0d9162..08095d79fc 100644
--- 
a/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.html
+++ 
b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.html
@@ -17,140 +17,201 @@
   -->
 
 <div class="sp-dialog-container">
-    <div class="sp-dialog-content" *ngIf="usersLoaded">
-        <div fxFlex="100" fxLayout="column" class="p-15">
-            <h4>{{ headerTitle }}</h4>
-            <form [formGroup]="parentForm" fxFlex="100" fxLayout="column">
-                <div class="general-options-panel" fxLayout="column">
-                    <span class="general-options-header">{{
-                        'Basics' | translate
-                    }}</span>
-                    <mat-form-field color="accent">
-                        <mat-label>{{ 'Owner' | translate }}</mat-label>
-                        <mat-select formControlName="owner" fxFlex required>
-                            <mat-option
-                                *ngFor="let user of allUsers"
-                                [value]="user.principalId"
-                                >{{ user.username }}</mat-option
-                            >
-                        </mat-select>
-                    </mat-form-field>
-                    <mat-checkbox
-                        data-cy="permission-public-element"
-                        formControlName="publicElement"
-                    >
-                        {{ 'Public Element' | translate }}
-                    </mat-checkbox>
-                </div>
-                <div
-                    fxLayout="column"
-                    class="general-options-panel"
-                    *ngIf="!permission.publicElement"
-                >
-                    <span class="general-options-header">{{
-                        'Users' | translate
-                    }}</span>
-                    <mat-form-field color="accent">
-                        <mat-label>{{
-                            'Authorized Users' | translate
-                        }}</mat-label>
-                        <mat-chip-grid
-                            #chipList
-                            [attr.aria-label]="'User selection' | translate"
-                        >
-                            <mat-chip-row
-                                *ngFor="let user of grantedUserAuthorities"
-                                selectable="true"
-                                removable="true"
-                                (removed)="removeUser(user)"
-                            >
-                                {{ user.username }}
-                                <button matChipRemove>
-                                    <mat-icon>cancel</mat-icon>
-                                </button>
-                            </mat-chip-row>
-                            <input
-                                [placeholder]="'Add' | translate"
-                                #userInput
-                                [formControl]="userCtrl"
-                                [matAutocomplete]="auto"
-                                [matChipInputFor]="chipList"
-                                [matChipInputSeparatorKeyCodes]="
-                                    separatorKeysCodes
-                                "
-                                data-cy="authorized-user"
-                                (matChipInputTokenEnd)="addUser($event)"
-                            />
-                        </mat-chip-grid>
-                        <mat-autocomplete
-                            #auto="matAutocomplete"
-                            (optionSelected)="userSelected($event)"
-                        >
-                            <mat-option
-                                *ngFor="let user of filteredUsers | async"
-                                [value]="user"
-                                [attr.data-cy]="'user-option-' + user.username"
-                            >
-                                {{ user.username }}
-                            </mat-option>
-                        </mat-autocomplete>
-                    </mat-form-field>
-                </div>
-                <div
-                    fxLayout="column"
-                    class="general-options-panel"
-                    *ngIf="!permission.publicElement"
-                >
-                    <span class="general-options-header">{{
-                        'Groups' | translate
-                    }}</span>
-                    <mat-form-field color="accent">
-                        <mat-label data-cy="authorized-groups-label">{{
-                            'Authorized Groups' | translate
-                        }}</mat-label>
-                        <mat-chip-grid
-                            #chipList
-                            [attr.aria-label]="'Group selection' | translate"
-                        >
-                            <mat-chip-row
-                                *ngFor="let group of grantedGroupAuthorities"
-                                selectable="true"
-                                removable="true"
-                                (removed)="removeGroup(group)"
-                            >
-                                {{ group.groupName }}
-                                <button matChipRemove>
-                                    <mat-icon>cancel</mat-icon>
-                                </button>
-                            </mat-chip-row>
-                            <input
-                                [placeholder]="'Add' | translate"
-                                #groupInput
-                                [formControl]="groupCtrl"
-                                [matAutocomplete]="auto"
-                                [matChipInputFor]="chipList"
-                                [matChipInputSeparatorKeyCodes]="
-                                    separatorKeysCodes
-                                "
-                                (matChipInputTokenEnd)="addGroup($event)"
-                            />
-                        </mat-chip-grid>
-                        <mat-autocomplete
-                            #auto="matAutocomplete"
-                            (optionSelected)="groupSelected($event)"
+    @if (usersLoaded) {
+        <div class="sp-dialog-content">
+            <div fxFlex="100" fxLayout="column" class="p-15">
+                <h4>{{ headerTitle }}</h4>
+                <form [formGroup]="parentForm" fxFlex="100" fxLayout="column">
+                    <div class="general-options-panel" fxLayout="column">
+                        <span class="general-options-header">{{
+                            'Basics' | translate
+                        }}</span>
+                        <mat-form-field color="accent">
+                            <mat-label>{{ 'Owner' | translate }}</mat-label>
+                            <mat-select formControlName="owner" fxFlex 
required>
+                                <mat-option
+                                    *ngFor="let user of allUsers"
+                                    [value]="user.principalId"
+                                    >{{ user.username }}</mat-option
+                                >
+                            </mat-select>
+                        </mat-form-field>
+                        <mat-checkbox
+                            data-cy="permission-public-element"
+                            formControlName="publicElement"
                         >
-                            <mat-option
-                                *ngFor="let group of filteredGroups | async"
-                                [value]="group"
+                            {{ 'Public Element' | translate }} ({{
+                                'visible to registered users' | translate
+                            }})
+                        </mat-checkbox>
+                    </div>
+                    @if (!parentForm.get('publicElement')?.value) {
+                        <div fxLayout="column" class="general-options-panel">
+                            <span class="general-options-header">{{
+                                'Users' | translate
+                            }}</span>
+                            <mat-form-field color="accent">
+                                <mat-label>{{
+                                    'Authorized Users' | translate
+                                }}</mat-label>
+                                <mat-chip-grid
+                                    #chipList
+                                    [attr.aria-label]="
+                                        'User selection' | translate
+                                    "
+                                >
+                                    <mat-chip-row
+                                        *ngFor="
+                                            let user of grantedUserAuthorities
+                                        "
+                                        selectable="true"
+                                        removable="true"
+                                        (removed)="removeUser(user)"
+                                    >
+                                        {{ user.username }}
+                                        <button matChipRemove>
+                                            <mat-icon>cancel</mat-icon>
+                                        </button>
+                                    </mat-chip-row>
+                                    <input
+                                        matInput
+                                        [placeholder]="'Add' | translate"
+                                        #userInput
+                                        [formControl]="userCtrl"
+                                        [matAutocomplete]="userAuto"
+                                        [matChipInputFor]="chipList"
+                                        [matChipInputSeparatorKeyCodes]="
+                                            separatorKeysCodes
+                                        "
+                                        data-cy="authorized-user"
+                                        
(matChipInputTokenEnd)="addUser($event)"
+                                    />
+                                </mat-chip-grid>
+                                <mat-autocomplete
+                                    #userAuto="matAutocomplete"
+                                    (optionSelected)="userSelected($event)"
+                                >
+                                    <mat-option
+                                        *ngFor="
+                                            let user of filteredUsers | async
+                                        "
+                                        [value]="user"
+                                        [attr.data-cy]="
+                                            'user-option-' + user.username
+                                        "
+                                    >
+                                        {{ user.username }}
+                                    </mat-option>
+                                </mat-autocomplete>
+                            </mat-form-field>
+                        </div>
+                    }
+                    @if (!parentForm.get('publicElement')?.value) {
+                        <div fxLayout="column" class="general-options-panel">
+                            <span class="general-options-header">{{
+                                'Groups' | translate
+                            }}</span>
+                            <mat-form-field color="accent">
+                                <mat-label data-cy="authorized-groups-label">{{
+                                    'Authorized Groups' | translate
+                                }}</mat-label>
+                                <mat-chip-grid
+                                    #chipList
+                                    [attr.aria-label]="
+                                        'Group selection' | translate
+                                    "
+                                >
+                                    <mat-chip-row
+                                        *ngFor="
+                                            let group of 
grantedGroupAuthorities
+                                        "
+                                        selectable="true"
+                                        removable="true"
+                                        (removed)="removeGroup(group)"
+                                    >
+                                        {{ group.groupName }}
+                                        <button matChipRemove>
+                                            <mat-icon>cancel</mat-icon>
+                                        </button>
+                                    </mat-chip-row>
+                                    <input
+                                        matInput
+                                        [placeholder]="'Add' | translate"
+                                        #groupInput
+                                        [formControl]="groupCtrl"
+                                        [matAutocomplete]="groupAuto"
+                                        [matChipInputFor]="chipList"
+                                        [matChipInputSeparatorKeyCodes]="
+                                            separatorKeysCodes
+                                        "
+                                        (matChipInputTokenEnd)="
+                                            addGroup($event)
+                                        "
+                                    />
+                                </mat-chip-grid>
+                                <mat-autocomplete
+                                    #groupAuto="matAutocomplete"
+                                    (optionSelected)="groupSelected($event)"
+                                >
+                                    <mat-option
+                                        *ngFor="
+                                            let group of filteredGroups | async
+                                        "
+                                        [value]="group"
+                                    >
+                                        {{ group.groupName }}
+                                    </mat-option>
+                                </mat-autocomplete>
+                            </mat-form-field>
+                        </div>
+                    }
+                    @if (
+                        anonymousReadSupported &&
+                        parentForm.contains('readAnonymous')
+                    ) {
+                        <div fxLayout="column" class="general-options-panel">
+                            <span class="general-options-header">{{
+                                'Public Link' | translate
+                            }}</span>
+                            <mat-checkbox
+                                data-cy="permission-anonymous-read"
+                                formControlName="readAnonymous"
                             >
-                                {{ group.groupName }}
-                            </mat-option>
-                        </mat-autocomplete>
-                    </mat-form-field>
-                </div>
-            </form>
+                                {{
+                                    'Allow anonymous access through public 
link'
+                                        | translate
+                                }}
+                            </mat-checkbox>
+                            @if (parentForm.get('readAnonymous')?.value) {
+                                <div
+                                    fxLayout="row"
+                                    fxFlex="100"
+                                    class="mt-10"
+                                    fxLayoutGap="10px"
+                                    fxLayoutAlign="start center"
+                                >
+                                    <span>
+                                        {{ 'URL' | translate }}
+                                    </span>
+                                    <span class="public-link" fxFlex>
+                                        {{ publicLink }}
+                                    </span>
+                                    <button
+                                        mat-icon-button
+                                        color="accent"
+                                        [matTooltip]="'Copy' | translate"
+                                        [cdkCopyToClipboard]="publicLink"
+                                    >
+                                        <mat-icon>content_copy</mat-icon>
+                                    </button>
+                                </div>
+                            }
+                        </div>
+                    }
+                </form>
+            </div>
         </div>
-    </div>
+    }
     <mat-divider></mat-divider>
     <div class="sp-dialog-actions">
         <div fxLayout="row">
@@ -160,7 +221,7 @@
                 color="accent"
                 (click)="save()"
                 style="margin-right: 10px"
-                [disabled]="!parentForm.valid"
+                [disabled]="parentForm.invalid"
                 data-cy="sp-manage-permissions-save"
             >
                 <i class="material-icons">save</i
diff --git 
a/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.scss
 
b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.scss
index c38d325ac5..9925783cfb 100644
--- 
a/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.scss
+++ 
b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.scss
@@ -23,3 +23,12 @@
 .form-field .mat-form-field-infix {
     border-top: 0;
 }
+
+.public-link {
+    background: var(--color-bg-2);
+    border: 1px solid var(--color-bg-1);
+    padding: 10px;
+    color: var(--color-default-text);
+    margin-left: 10px;
+    margin-right: 10px;
+}
diff --git 
a/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.ts
 
b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.ts
index 02d104a3dc..2dbda6e745 100644
--- 
a/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.ts
+++ 
b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.ts
@@ -55,6 +55,12 @@ export class ObjectPermissionDialogComponent implements 
OnInit {
     @Input()
     headerTitle: string;
 
+    @Input()
+    anonymousReadSupported = false;
+
+    @Input()
+    publicLink = '';
+
     parentForm: UntypedFormGroup;
 
     permission: Permission;
@@ -90,17 +96,6 @@ export class ObjectPermissionDialogComponent implements 
OnInit {
     ngOnInit(): void {
         this.loadUsersAndGroups();
         this.parentForm = this.fb.group({});
-        this.parentForm.valueChanges.subscribe(v => {
-            this.permission.publicElement = v.publicElement;
-            if (v.publicElement) {
-                this.permission.grantedAuthorities = [];
-                this.grantedGroupAuthorities = [];
-                this.grantedUserAuthorities = [];
-            }
-            if (v.owner) {
-                this.permission.ownerSid = v.owner;
-            }
-        });
     }
 
     loadUsersAndGroups() {
@@ -137,6 +132,12 @@ export class ObjectPermissionDialogComponent implements 
OnInit {
                     Validators.required,
                 ),
             );
+            if (this.anonymousReadSupported) {
+                this.parentForm.addControl(
+                    'readAnonymous',
+                    new UntypedFormControl(this.permission.readAnonymous),
+                );
+            }
             this.filteredUsers = this.userCtrl.valueChanges.pipe(
                 startWith(null),
                 map((username: string | null) => {
@@ -166,12 +167,25 @@ export class ObjectPermissionDialogComponent implements 
OnInit {
                     this.addUserToSelection(authority);
                 }
             });
-        } else {
-            console.log('No permission entry found for item');
         }
     }
 
     save() {
+        const { owner, publicElement, readAnonymous } =
+            this.parentForm.getRawValue();
+        this.permission.publicElement = publicElement;
+        if (this.anonymousReadSupported) {
+            this.permission.readAnonymous = readAnonymous || false;
+        }
+        if (this.permission.publicElement) {
+            this.permission.grantedAuthorities = [];
+            this.grantedGroupAuthorities = [];
+            this.grantedUserAuthorities = [];
+        }
+        if (owner) {
+            this.permission.ownerSid = owner;
+        }
+
         this.permission.grantedAuthorities = this.grantedUserAuthorities
             .map(u => {
                 return { principalType: u.principalType, sid: u.principalId };
diff --git 
a/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.html
 
b/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.html
index 112ba222b1..f3117836fe 100644
--- 
a/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.html
+++ 
b/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.html
@@ -22,7 +22,7 @@
     ></sp-basic-header-title-component>
     <div fxFlex="100" fxLayout="row" fxLayoutAlign="center start">
         <sp-table
-            fxFlex="90"
+            fxFlex="100"
             [columns]="displayedColumns"
             [dataSource]="dataSource"
         >
diff --git 
a/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.ts
 
b/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.ts
index b2c53014aa..43053f6068 100644
--- 
a/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.ts
+++ 
b/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.ts
@@ -71,6 +71,8 @@ export class DashboardOverviewTableComponent extends 
SpDataExplorerOverviewDirec
             this.translateService.instant(
                 `Manage permissions for dashboard ${dashboard.name}`,
             ),
+            true,
+            this.makeDashboardKioskUrl(dashboard.elementId),
         );
 
         dialogRef.afterClosed().subscribe(refresh => {
@@ -152,4 +154,8 @@ export class DashboardOverviewTableComponent extends 
SpDataExplorerOverviewDirec
     openDashboardInKioskMode(dashboard: Dashboard) {
         this.router.navigate(['dashboard-kiosk', dashboard.elementId]);
     }
+
+    makeDashboardKioskUrl(dashboardId: string): string {
+        return 
`${window.location.protocol}//${window.location.host}/#/dashboard-kiosk/${dashboardId}`;
+    }
 }
diff --git 
a/ui/src/app/data-explorer-shared/services/data-explorer-shared.service.ts 
b/ui/src/app/data-explorer-shared/services/data-explorer-shared.service.ts
index b3d77875c7..c164d058aa 100644
--- a/ui/src/app/data-explorer-shared/services/data-explorer-shared.service.ts
+++ b/ui/src/app/data-explorer-shared/services/data-explorer-shared.service.ts
@@ -38,7 +38,12 @@ export class DataExplorerSharedService {
         private translateService: TranslateService,
     ) {}
 
-    openPermissionsDialog(elementId: string, headerTitle: string) {
+    openPermissionsDialog(
+        elementId: string,
+        headerTitle: string,
+        anonymousReadSupported: boolean = false,
+        publicLink: string = '',
+    ) {
         return this.dialogService.open(ObjectPermissionDialogComponent, {
             panelType: PanelType.SLIDE_IN_PANEL,
             title: this.translateService.instant('Manage permissions'),
@@ -46,6 +51,8 @@ export class DataExplorerSharedService {
             data: {
                 objectInstanceId: elementId,
                 headerTitle,
+                anonymousReadSupported,
+                publicLink,
             },
         });
     }
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 65a615065c..6857516236 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
@@ -22,7 +22,7 @@
     ></sp-basic-header-title-component>
     <div fxFlex="100" fxLayout="row" fxLayoutAlign="center start">
         <sp-table
-            fxFlex="90"
+            fxFlex="100"
             [columns]="displayedColumns"
             [dataSource]="dataSource"
         >
diff --git a/ui/src/app/pipelines/pipelines.component.html 
b/ui/src/app/pipelines/pipelines.component.html
index dd7ddf1096..077b3e861e 100644
--- a/ui/src/app/pipelines/pipelines.component.html
+++ b/ui/src/app/pipelines/pipelines.component.html
@@ -89,7 +89,7 @@
                     title="Pipelines"
                 ></sp-basic-header-title-component>
                 <div fxFlex="100" fxLayout="row" fxLayoutAlign="center start">
-                    <div fxFlex="90">
+                    <div fxFlex="100">
                         <sp-pipeline-overview
                             [pipelines]="filteredPipelines"
                             (refreshPipelinesEmitter)="getPipelines()"
@@ -106,7 +106,7 @@
                         fxLayout="row"
                         fxLayoutAlign="center start"
                     >
-                        <div fxFlex="90">
+                        <div fxFlex="100">
                             <sp-functions-overview
                                 [functions]="functions"
                                 *ngIf="functionsReady"


Reply via email to