This is an automated email from the ASF dual-hosted git repository.
riemer 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 4ff27c3f40 feat(#3725): Support public dashboards (#3741)
4ff27c3f40 is described below
commit 4ff27c3f40bc347a36c873d1b9a88923e07c325b
Author: Dominik Riemer <[email protected]>
AuthorDate: Mon Aug 25 13:30:20 2025 +0200
feat(#3725): Support public dashboards (#3741)
* feat(#3725): Add feature to mark dashboards as public
* feat: Add authorization to public dashboards
---
.../streampipes/model/client/user/Permission.java | 9 +
.../management/PermissionResourceManager.java | 8 +
.../impl/dashboard/DataLakeDashboardResource.java | 19 +-
.../datalake/KioskDashboardDataLakeResource.java | 116 ++++++++
.../rest/security/SpPermissionEvaluator.java | 122 +++++---
.../service/core/UnauthenticatedInterfaces.java | 5 +-
.../management/util/GrantedPermissionsBuilder.java | 2 +-
.../src/lib/apis/dashboard-kiosk.service.ts | 52 ++++
.../src/lib/apis/datalake-rest.service.ts | 17 +-
.../src/lib/model/gen/streampipes-model-client.ts | 4 +-
.../lib/query/data-view-query-generator.service.ts | 38 ++-
.../platform-services/src/public-api.ts | 1 +
.../existing-adapters.component.html | 2 +-
.../object-permission-dialog.component.html | 325 ++++++++++++---------
.../object-permission-dialog.component.scss | 9 +
.../object-permission-dialog.component.ts | 40 ++-
.../kiosk/dashboard-kiosk.component.html | 2 +-
.../components/kiosk/dashboard-kiosk.component.ts | 8 +
.../chart-view/abstract-chart-view.directive.ts | 4 +
.../grid-view/dashboard-grid-view.component.html | 1 +
.../slide-view/dashboard-slide-view.component.html | 1 +
.../slide-view/dashboard-slide-view.component.ts | 7 +-
.../dashboard-overview-table.component.html | 2 +-
.../dashboard-overview-table.component.ts | 6 +
.../overview/dashboard-overview.component.ts | 18 +-
.../panel/dashboard-panel.component.html | 2 +
.../components/panel/dashboard-panel.component.ts | 5 +
.../data-explorer-chart-container.component.html | 2 -
.../data-explorer-chart-container.component.ts | 10 +-
.../base/base-data-explorer-widget.directive.ts | 35 +--
.../models/dataview-dashboard.model.ts | 12 +
.../services/data-explorer-shared.service.ts | 62 +++-
.../data-explorer-chart-view.component.html | 1 +
.../data-explorer-chart-view.component.ts | 26 +-
.../data-explorer-overview-table.component.html | 2 +-
ui/src/app/pipelines/pipelines.component.html | 4 +-
36 files changed, 719 insertions(+), 260 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/impl/dashboard/DataLakeDashboardResource.java
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/dashboard/DataLakeDashboardResource.java
index 0fcb6e2162..ec015f2390 100644
---
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/dashboard/DataLakeDashboardResource.java
+++
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/dashboard/DataLakeDashboardResource.java
@@ -23,6 +23,7 @@ import
org.apache.streampipes.model.client.user.DefaultPrivilege;
import org.apache.streampipes.model.dashboard.DashboardModel;
import org.apache.streampipes.resource.management.DataExplorerResourceManager;
import
org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource;
+import org.apache.streampipes.storage.api.IPermissionStorage;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpStatus;
@@ -47,6 +48,12 @@ import java.util.Objects;
@RequestMapping("/api/v3/datalake/dashboard")
public class DataLakeDashboardResource extends AbstractAuthGuardedRestResource
{
+ private final IPermissionStorage permissionStorage;
+
+ public DataLakeDashboardResource() {
+ this.permissionStorage = getNoSqlStorage().getPermissionStorage();
+ }
+
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("this.hasReadAuthority()")
@PostFilter("hasPermission(filterObject.couchDbId, 'READ')")
@@ -61,7 +68,7 @@ public class DataLakeDashboardResource extends
AbstractAuthGuardedRestResource {
}
@GetMapping(path = "/{dashboardId}/composite", produces =
MediaType.APPLICATION_JSON_VALUE)
- @PreAuthorize("this.hasReadAuthority() and hasPermission(#dashboardId,
'READ')")
+ @PreAuthorize("this.hasReadAuthorityOrAnonymous(#dashboardId) and
hasPermission(#dashboardId, 'READ')")
public ResponseEntity<?>
getCompositeDashboardModel(@PathVariable("dashboardId") String dashboardId,
@RequestHeader(value =
"If-None-Match", required = false) String ifNoneMatch) {
var dashboard = getResourceManager().getCompositeDashboard(dashboardId);
@@ -113,4 +120,14 @@ public class DataLakeDashboardResource extends
AbstractAuthGuardedRestResource {
public boolean hasWriteAuthority() {
return
isAdminOrHasAnyAuthority(DefaultPrivilege.Constants.PRIVILEGE_WRITE_DASHBOARD_VALUE);
}
+
+ public boolean hasReadAuthorityOrAnonymous(String dashboardId) {
+ return hasReadAuthority()
+ || hasAnonymousAccessAuthority(dashboardId);
+ }
+
+ private boolean hasAnonymousAccessAuthority(String dashboardId) {
+ var perms = permissionStorage.getUserPermissionsForObject(dashboardId);
+ return !perms.isEmpty() && perms.get(0).isReadAnonymous();
+ }
}
diff --git
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/KioskDashboardDataLakeResource.java
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/KioskDashboardDataLakeResource.java
new file mode 100644
index 0000000000..d6c0258620
--- /dev/null
+++
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/KioskDashboardDataLakeResource.java
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.streampipes.rest.impl.datalake;
+
+import org.apache.streampipes.dataexplorer.api.IDataExplorerQueryManagement;
+import org.apache.streampipes.dataexplorer.api.IDataExplorerSchemaManagement;
+import org.apache.streampipes.dataexplorer.management.DataExplorerDispatcher;
+import org.apache.streampipes.model.client.user.DefaultPrivilege;
+import org.apache.streampipes.model.dashboard.DashboardModel;
+import org.apache.streampipes.model.datalake.DataExplorerWidgetModel;
+import org.apache.streampipes.model.datalake.SpQueryResult;
+import org.apache.streampipes.model.datalake.param.ProvidedRestQueryParams;
+import org.apache.streampipes.model.monitoring.SpLogMessage;
+import
org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource;
+import org.apache.streampipes.storage.api.CRUDStorage;
+import org.apache.streampipes.storage.management.StorageDispatcher;
+
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/v3/datalake/dashboard/kiosk")
+public class KioskDashboardDataLakeResource extends
AbstractAuthGuardedRestResource {
+
+ private final IDataExplorerQueryManagement dataExplorerQueryManagement;
+ private final IDataExplorerSchemaManagement dataExplorerSchemaManagement;
+ private final CRUDStorage<DashboardModel> dashboardStorage =
+
StorageDispatcher.INSTANCE.getNoSqlStore().getDataExplorerDashboardStorage();
+ private final CRUDStorage<DataExplorerWidgetModel> dataExplorerWidgetStorage;
+
+ public KioskDashboardDataLakeResource() {
+ this.dataExplorerSchemaManagement = new DataExplorerDispatcher()
+ .getDataExplorerManager()
+ .getSchemaManagement();
+ this.dataExplorerQueryManagement = new DataExplorerDispatcher()
+ .getDataExplorerManager()
+ .getQueryManagement(this.dataExplorerSchemaManagement);
+ this.dataExplorerWidgetStorage = StorageDispatcher.INSTANCE
+ .getNoSqlStore()
+ .getDataExplorerWidgetStorage();
+ }
+
+ @PostMapping(path = "/{dashboardId}/{widgetId}/data",
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE)
+ @PreAuthorize("this.hasReadAuthority() and hasPermission(#dashboardId,
'READ')")
+ public ResponseEntity<?> getData(@PathVariable("dashboardId") String
dashboardId,
+ @PathVariable("widgetId") String widgetId,
+ @RequestBody Map<String, String>
queryParams) {
+ var dashboard = dashboardStorage.getElementById(dashboardId);
+ if (dashboard.getWidgets().stream().noneMatch(w ->
w.getId().equals(widgetId))) {
+ return badRequest(String.format("Widget with id %s not found in
dashboard", widgetId));
+ }
+ var widget = dataExplorerWidgetStorage.getElementById(widgetId);
+ var measureName = queryParams.get("measureName");
+ if (!checkMeasureNameInWidget(widget, measureName)) {
+ return badRequest("Measure name not found in widget configuration");
+ } else {
+ ProvidedRestQueryParams sanitizedParams = new
ProvidedRestQueryParams(measureName, queryParams);
+ try {
+ SpQueryResult result =
+ this.dataExplorerQueryManagement.getData(sanitizedParams, true);
+ return ok(result);
+ } catch (RuntimeException e) {
+ return badRequest(SpLogMessage.from(e));
+ }
+ }
+ }
+
+ private boolean checkMeasureNameInWidget(DataExplorerWidgetModel widget,
+ String measureName) {
+ var sourceConfigs = widget.getDataConfig().get("sourceConfigs");
+ if (sourceConfigs instanceof List<?>) {
+ return ((List<?>) sourceConfigs)
+ .stream()
+ .anyMatch(config -> {
+ if (!(config instanceof Map<?, ?>)) {
+ return false;
+ } else {
+ return ((Map<?, ?>)
config).get("measureName").equals(measureName);
+ }
+ });
+ } else {
+ return false;
+ }
+ }
+
+ public boolean hasReadAuthority() {
+ return
isAdminOrHasAnyAuthority(DefaultPrivilege.Constants.PRIVILEGE_READ_DASHBOARD_VALUE);
+ }
+}
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..28b43f709a 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) {
+ PrincipalUserDetails<?> user = getUserDetailsOrNull(auth);
+ if (user == null) {
+ return false;
+ }
+
+ if (isPublicElement(permissions)) {
+ return true;
+ }
+
+ 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 isPublicElement(List<Permission> permissions) {
+ return !permissions.isEmpty()
+ && (permissions.get(0).isPublicElement());
}
- 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..1653f93e1f 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,10 @@ public class UnauthenticatedInterfaces {
"/error",
"/",
"/streampipes-backend/",
- "/streampipes-backend/index.html"
+ "/streampipes-backend/index.html",
+ // anonymous dashboard access is allowed
+ "/api/v3/datalake/dashboard/*/composite",
+ "/api/v3/datalake/dashboard/kiosk/*/*/data"
);
}
}
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/apis/dashboard-kiosk.service.ts
b/ui/projects/streampipes/platform-services/src/lib/apis/dashboard-kiosk.service.ts
new file mode 100644
index 0000000000..be79b32851
--- /dev/null
+++
b/ui/projects/streampipes/platform-services/src/lib/apis/dashboard-kiosk.service.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 { inject, Injectable } from '@angular/core';
+import { HttpClient, HttpContext } from '@angular/common/http';
+import { SpQueryResult } from '../model/gen/streampipes-model';
+import { Observable } from 'rxjs';
+import { NGX_LOADING_BAR_IGNORED } from '@ngx-loading-bar/http-client';
+import { PlatformServicesCommons } from './commons.service';
+import { DatalakeQueryParameters } from
'../model/datalake/DatalakeQueryParameters';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class DashboardKioskRestService {
+ private http = inject(HttpClient);
+ private platformServicesCommons = inject(PlatformServicesCommons);
+
+ getData(
+ dashboardId: string,
+ widgetId: string,
+ queryParams: DatalakeQueryParameters,
+ ): Observable<SpQueryResult> {
+ const context = new HttpContext().set(NGX_LOADING_BAR_IGNORED, true);
+ const url =
`${this.dashboardKioskBasePath}/${dashboardId}/${widgetId}/data`;
+ return this.http.post<SpQueryResult>(url, queryParams, {
+ context,
+ });
+ }
+
+ private get dashboardKioskBasePath() {
+ return (
+ this.platformServicesCommons.basePath +
+ '/api/v3/datalake/dashboard/kiosk'
+ );
+ }
+}
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 4b54c3e802..9b865257cd 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
@@ -17,11 +17,17 @@
*/
import { Injectable } from '@angular/core';
-import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http';
+import {
+ HttpClient,
+ HttpContext,
+ HttpParams,
+ HttpRequest,
+} from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { DataLakeMeasure, SpQueryResult } from
'../model/gen/streampipes-model';
import { map } from 'rxjs/operators';
import { DatalakeQueryParameters } from
'../model/datalake/DatalakeQueryParameters';
+import { NGX_LOADING_BAR_IGNORED } from '@ngx-loading-bar/http-client';
@Injectable({
providedIn: 'root',
@@ -81,20 +87,21 @@ export class DatalakeRestService {
getData(
index: string,
queryParams: DatalakeQueryParameters,
- ignoreLoadingBar?: boolean,
+ ignoreLoadingBar = false,
): Observable<SpQueryResult> {
const columns = queryParams.columns;
+ const context = ignoreLoadingBar
+ ? new HttpContext().set(NGX_LOADING_BAR_IGNORED, true)
+ : undefined;
if (columns === '') {
const emptyQueryResult = new SpQueryResult();
emptyQueryResult.total = 0;
return of(emptyQueryResult);
} else {
const url = this.dataLakeUrl + '/measurements/' + index;
- const headers = ignoreLoadingBar ? { ignoreLoadingBar: '' } : {};
- // @ts-ignore
return this.http.get<SpQueryResult>(url, {
params: queryParams as unknown as HttpParams,
- headers,
+ context,
});
}
}
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/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts
b/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts
index 2c2c28c4f7..deb9c14e1b 100644
---
a/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts
+++
b/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts
@@ -16,7 +16,7 @@
*
*/
-import { Injectable } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { DatalakeRestService } from '../apis/datalake-rest.service';
import {
@@ -26,12 +26,14 @@ import {
import { SpQueryResult } from '../model/gen/streampipes-model';
import { DatalakeQueryParameters } from
'../model/datalake/DatalakeQueryParameters';
import { DatalakeQueryParameterBuilder } from
'./DatalakeQueryParameterBuilder';
+import { DashboardKioskRestService } from '../apis/dashboard-kiosk.service';
@Injectable({
providedIn: 'root',
})
export class DataViewQueryGeneratorService {
- constructor(protected dataLakeRestService: DatalakeRestService) {}
+ protected dataLakeRestService = inject(DatalakeRestService);
+ protected dashboardKioskRestService = inject(DashboardKioskRestService);
generateObservables(
startTime: number,
@@ -56,19 +58,45 @@ export class DataViewQueryGeneratorService {
});
}
+ generateObservablesForKioskMode(
+ startTime: number,
+ endTime: number,
+ dataConfig: DataExplorerDataConfig,
+ dashboardId: string,
+ widgetId: string,
+ maximumResultingEvents = -1,
+ ): Observable<SpQueryResult>[] {
+ return dataConfig.sourceConfigs.map(sourceConfig => {
+ const dataLakeConfiguration = this.generateQuery(
+ startTime,
+ endTime,
+ sourceConfig,
+ dataConfig.ignoreMissingValues,
+ maximumResultingEvents,
+ true,
+ );
+
+ return this.dashboardKioskRestService.getData(
+ dashboardId,
+ widgetId,
+ dataLakeConfiguration,
+ );
+ });
+ }
+
generateQuery(
startTime: number,
endTime: number,
sourceConfig: SourceConfig,
ignoreEventsWithMissingValues: boolean,
maximumResultingEvents: number = -1,
+ includeMeasureName = false,
): DatalakeQueryParameters {
const queryBuilder = DatalakeQueryParameterBuilder.create(
startTime,
endTime,
);
const queryConfig = sourceConfig.queryConfig;
-
queryBuilder.withColumnFilter(
queryConfig.fields.filter(f => f.selected),
sourceConfig.queryType === 'aggregated' ||
@@ -122,6 +150,10 @@ export class DataViewQueryGeneratorService {
queryBuilder.withMaximumAmountOfEvents(maximumResultingEvents);
}
+ if (includeMeasureName) {
+ dataLakeQueryParameter.measureName = sourceConfig.measureName;
+ }
+
return dataLakeQueryParameter;
}
}
diff --git a/ui/projects/streampipes/platform-services/src/public-api.ts
b/ui/projects/streampipes/platform-services/src/public-api.ts
index ec91f1a39a..583573db3c 100644
--- a/ui/projects/streampipes/platform-services/src/public-api.ts
+++ b/ui/projects/streampipes/platform-services/src/public-api.ts
@@ -30,6 +30,7 @@ export * from './lib/apis/compact-pipeline.service';
export * from './lib/apis/certificate.service';
export * from './lib/apis/chart.service';
export * from './lib/apis/dashboard.service';
+export * from './lib/apis/dashboard-kiosk.service';
export * from './lib/apis/datalake-rest.service';
export * from './lib/apis/files.service';
export * from './lib/apis/functions.service';
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-kiosk/components/kiosk/dashboard-kiosk.component.html
b/ui/src/app/dashboard-kiosk/components/kiosk/dashboard-kiosk.component.html
index c08275e9c7..13e9c8f850 100644
--- a/ui/src/app/dashboard-kiosk/components/kiosk/dashboard-kiosk.component.html
+++ b/ui/src/app/dashboard-kiosk/components/kiosk/dashboard-kiosk.component.html
@@ -58,11 +58,11 @@
<div fxLayout="column" style="height: calc(100vh - 40px)">
@if (dashboard) {
<sp-dashboard-grid-view
- #dashboardGrid
*ngIf="dashboard?.widgets.length > 0"
[editMode]="false"
[kioskMode]="true"
[dashboard]="dashboard"
+ [observableGenerator]="observableGenerator"
[widgets]="widgets"
[timeSettings]="dashboard.dashboardTimeSettings"
class="dashboard-grid"
diff --git
a/ui/src/app/dashboard-kiosk/components/kiosk/dashboard-kiosk.component.ts
b/ui/src/app/dashboard-kiosk/components/kiosk/dashboard-kiosk.component.ts
index 871967aeb2..34b7a86df0 100644
--- a/ui/src/app/dashboard-kiosk/components/kiosk/dashboard-kiosk.component.ts
+++ b/ui/src/app/dashboard-kiosk/components/kiosk/dashboard-kiosk.component.ts
@@ -29,6 +29,8 @@ import { of, Subscription, timer } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { TimeSelectionService } from '@streampipes/shared-ui';
import { DataExplorerDashboardService } from
'../../../dashboard-shared/services/dashboard.service';
+import { DataExplorerSharedService } from
'../../../data-explorer-shared/services/data-explorer-shared.service';
+import { ObservableGenerator } from
'../../../data-explorer-shared/models/dataview-dashboard.model';
@Component({
selector: 'sp-dashboard-kiosk',
@@ -41,7 +43,9 @@ export class DashboardKioskComponent implements OnInit,
OnDestroy {
private dashboardService = inject(DashboardService);
private timeSelectionService = inject(TimeSelectionService);
private dataExplorerDashboardService =
inject(DataExplorerDashboardService);
+ private dataExplorerSharedService = inject(DataExplorerSharedService);
+ observableGenerator: ObservableGenerator;
dashboard: Dashboard;
widgets: DataExplorerWidgetModel[] = [];
refresh$: Subscription;
@@ -49,6 +53,10 @@ export class DashboardKioskComponent implements OnInit,
OnDestroy {
ngOnInit() {
const dashboardId = this.route.snapshot.params.dashboardId;
+ this.observableGenerator =
+ this.dataExplorerSharedService.kioskModeObservableGenerator(
+ dashboardId,
+ );
this.dashboardService
.getCompositeDashboard(dashboardId)
.subscribe(res => {
diff --git
a/ui/src/app/dashboard-shared/components/chart-view/abstract-chart-view.directive.ts
b/ui/src/app/dashboard-shared/components/chart-view/abstract-chart-view.directive.ts
index 8a22e809f6..324bae85f1 100644
---
a/ui/src/app/dashboard-shared/components/chart-view/abstract-chart-view.directive.ts
+++
b/ui/src/app/dashboard-shared/components/chart-view/abstract-chart-view.directive.ts
@@ -26,6 +26,7 @@ import {
} from '@streampipes/platform-services';
import { ResizeService } from
'../../../data-explorer-shared/services/resize.service';
import { DataExplorerChartRegistry } from
'../../../data-explorer-shared/registry/data-explorer-chart-registry';
+import { ObservableGenerator } from
'../../../data-explorer-shared/models/dataview-dashboard.model';
@Directive()
export abstract class AbstractChartViewDirective {
@@ -45,6 +46,9 @@ export abstract class AbstractChartViewDirective {
@Input()
currentlyConfiguredWidgetId: string;
+ @Input()
+ observableGenerator: ObservableGenerator;
+
configuredWidgets: Map<string, DataExplorerWidgetModel> = new Map<
string,
DataExplorerWidgetModel
diff --git
a/ui/src/app/dashboard-shared/components/chart-view/grid-view/dashboard-grid-view.component.html
b/ui/src/app/dashboard-shared/components/chart-view/grid-view/dashboard-grid-view.component.html
index 43f50a8dda..cfa89e8de7 100644
---
a/ui/src/app/dashboard-shared/components/chart-view/grid-view/dashboard-grid-view.component.html
+++
b/ui/src/app/dashboard-shared/components/chart-view/grid-view/dashboard-grid-view.component.html
@@ -48,6 +48,7 @@
[dashboardItem]="item"
[configuredWidget]="configuredWidgets.get(item.id)"
[dataLakeMeasure]="dataLakeMeasures.get(item.id)"
+ [observableGenerator]="observableGenerator"
[editMode]="editMode"
[kioskMode]="kioskMode"
[gridMode]="true"
diff --git
a/ui/src/app/dashboard-shared/components/chart-view/slide-view/dashboard-slide-view.component.html
b/ui/src/app/dashboard-shared/components/chart-view/slide-view/dashboard-slide-view.component.html
index 1640260e3a..e7fd8d59b2 100644
---
a/ui/src/app/dashboard-shared/components/chart-view/slide-view/dashboard-slide-view.component.html
+++
b/ui/src/app/dashboard-shared/components/chart-view/slide-view/dashboard-slide-view.component.html
@@ -61,6 +61,7 @@
[dashboardItem]="currentDashboardItem"
[configuredWidget]="currentWidget"
[dataLakeMeasure]="currentMeasure"
+ [observableGenerator]="observableGenerator"
[editMode]="editMode"
[gridMode]="false"
[widgetIndex]="i"
diff --git
a/ui/src/app/dashboard-shared/components/chart-view/slide-view/dashboard-slide-view.component.ts
b/ui/src/app/dashboard-shared/components/chart-view/slide-view/dashboard-slide-view.component.ts
index 7d6dd7b283..c8c76a1f6e 100644
---
a/ui/src/app/dashboard-shared/components/chart-view/slide-view/dashboard-slide-view.component.ts
+++
b/ui/src/app/dashboard-shared/components/chart-view/slide-view/dashboard-slide-view.component.ts
@@ -25,6 +25,7 @@ import {
} from '@angular/core';
import { AbstractChartViewDirective } from '../abstract-chart-view.directive';
import {
+ ClientDashboardItem,
DashboardItem,
DataExplorerWidgetModel,
DataLakeMeasure,
@@ -46,7 +47,7 @@ export class DashboardSlideViewComponent
currentWidget: DataExplorerWidgetModel;
currentMeasure: DataLakeMeasure;
- currentDashboardItem: DashboardItem;
+ currentDashboardItem: ClientDashboardItem;
displayWidget = false;
@@ -62,9 +63,7 @@ export class DashboardSlideViewComponent
this.selectedWidgetIndex = index;
this.currentWidget = this.configuredWidgets.get(widgetId);
this.currentMeasure = this.dataLakeMeasures.get(widgetId);
- this.currentDashboardItem = this.dashboard.widgets[
- index
- ] as unknown as DashboardItem;
+ this.currentDashboardItem = this.dashboard.widgets[index];
this.currentlyConfiguredWidgetId = widgetId;
this.displayWidget = true;
});
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/dashboard/components/overview/dashboard-overview.component.ts
b/ui/src/app/dashboard/components/overview/dashboard-overview.component.ts
index 7fb66fbdec..4d6bfc337e 100644
--- a/ui/src/app/dashboard/components/overview/dashboard-overview.component.ts
+++ b/ui/src/app/dashboard/components/overview/dashboard-overview.component.ts
@@ -16,7 +16,7 @@
*
*/
-import { Component, OnInit, ViewChild } from '@angular/core';
+import { Component, inject, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import {
CurrentUserService,
@@ -30,6 +30,7 @@ import { Dashboard } from '@streampipes/platform-services';
import { DataExplorerDashboardService } from
'../../../dashboard-shared/services/dashboard.service';
import { DashboardOverviewTableComponent } from
'./dashboard-overview-table/dashboard-overview-table.component';
import { TranslateService } from '@ngx-translate/core';
+import { DataExplorerSharedService } from
'../../../data-explorer-shared/services/data-explorer-shared.service';
@Component({
selector: 'sp-dashboard-overview',
@@ -47,14 +48,13 @@ export class DashboardOverviewComponent implements OnInit {
@ViewChild(DashboardOverviewTableComponent)
dashboardOverview: DashboardOverviewTableComponent;
- constructor(
- public dialog: MatDialog,
- private dataExplorerDashboardService: DataExplorerDashboardService,
- private authService: AuthService,
- private currentUserService: CurrentUserService,
- private breadcrumbService: SpBreadcrumbService,
- private translateService: TranslateService,
- ) {}
+ public dialog = inject(MatDialog);
+ private dataExplorerDashboardService =
inject(DataExplorerDashboardService);
+ private dataExplorerSharedService = inject(DataExplorerSharedService);
+ private authService = inject(AuthService);
+ private currentUserService = inject(CurrentUserService);
+ private breadcrumbService = inject(SpBreadcrumbService);
+ private translateService = inject(TranslateService);
ngOnInit(): void {
this.breadcrumbService.updateBreadcrumb(
diff --git
a/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
b/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
index d2627f69c7..fc3d97d855 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
+++ b/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
@@ -80,6 +80,7 @@
[editMode]="editMode"
[dashboard]="dashboard"
[widgets]="widgets"
+ [observableGenerator]="observableGenerator"
[timeSettings]="timeSettings"
(deleteCallback)="removeChartFromDashboard($event)"
(startEditModeEmitter)="startEditMode($event)"
@@ -92,6 +93,7 @@
[editMode]="editMode"
[dashboard]="dashboard"
[widgets]="widgets"
+ [observableGenerator]="observableGenerator"
[timeSettings]="timeSettings"
(deleteCallback)="removeChartFromDashboard($event)"
(startEditModeEmitter)="startEditMode($event)"
diff --git a/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts
b/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts
index 57aa4e6c35..d56e0f2f9d 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts
+++ b/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts
@@ -51,6 +51,7 @@ import { DataExplorerDetectChangesService } from
'../../../data-explorer/service
import { SupportsUnsavedChangeDialog } from
'../../../data-explorer-shared/models/dataview-dashboard.model';
import { TranslateService } from '@ngx-translate/core';
import { DataExplorerDashboardService } from
'../../../dashboard-shared/services/dashboard.service';
+import { DataExplorerSharedService } from
'../../../data-explorer-shared/services/data-explorer-shared.service';
@Component({
selector: 'sp-dashboard-panel',
@@ -97,6 +98,10 @@ export class DashboardPanelComponent
private breadcrumbService = inject(SpBreadcrumbService);
private translateService = inject(TranslateService);
private dataExplorerDashboardService =
inject(DataExplorerDashboardService);
+ private dataExplorerSharedService = inject(DataExplorerSharedService);
+
+ observableGenerator =
+ this.dataExplorerSharedService.defaultObservableGenerator();
public ngOnInit() {
const params = this.route.snapshot.params;
diff --git
a/ui/src/app/data-explorer-shared/components/chart-container/data-explorer-chart-container.component.html
b/ui/src/app/data-explorer-shared/components/chart-container/data-explorer-chart-container.component.html
index 1dc6c0a031..73a2e635c6 100644
---
a/ui/src/app/data-explorer-shared/components/chart-container/data-explorer-chart-container.component.html
+++
b/ui/src/app/data-explorer-shared/components/chart-container/data-explorer-chart-container.component.html
@@ -64,7 +64,6 @@
<button
mat-icon-button
[matMenuTriggerFor]="menu"
- [aria-label]="'More options' | translate"
[matTooltip]="'More options' | translate"
*ngIf="!dataViewMode"
[attr.data-cy]="
@@ -102,7 +101,6 @@
mat-icon-button
[matMenuTriggerFor]="optMenu"
*ngIf="!globalTimeEnabled"
- [aria-label]="'Options' | translate"
data-cy="options-data-explorer"
#menuTrigger="matMenuTrigger"
[matTooltip]="tooltipText"
diff --git
a/ui/src/app/data-explorer-shared/components/chart-container/data-explorer-chart-container.component.ts
b/ui/src/app/data-explorer-shared/components/chart-container/data-explorer-chart-container.component.ts
index 91f35ac40c..de5144a8c3 100644
---
a/ui/src/app/data-explorer-shared/components/chart-container/data-explorer-chart-container.component.ts
+++
b/ui/src/app/data-explorer-shared/components/chart-container/data-explorer-chart-container.component.ts
@@ -53,7 +53,10 @@ import {
TimeSelectionService,
TimeSelectorLabel,
} from '@streampipes/shared-ui';
-import { BaseWidgetData } from '../../models/dataview-dashboard.model';
+import {
+ BaseWidgetData,
+ ObservableGenerator,
+} from '../../models/dataview-dashboard.model';
import { DataExplorerSharedService } from
'../../services/data-explorer-shared.service';
import { MatMenuTrigger } from '@angular/material/menu';
@@ -108,6 +111,9 @@ export class DataExplorerChartContainerComponent
@Input()
globalTimeEnabled = true;
+ @Input()
+ observableGenerator: ObservableGenerator;
+
@Output() deleteCallback: EventEmitter<number> = new
EventEmitter<number>();
@Output() startEditModeEmitter: EventEmitter<DataExplorerWidgetModel> =
new EventEmitter<DataExplorerWidgetModel>();
@@ -261,6 +267,8 @@ export class DataExplorerChartContainerComponent
this.componentRef.instance.previewMode = this.previewMode;
this.componentRef.instance.gridMode = this.gridMode;
this.componentRef.instance.widgetIndex = this.widgetIndex;
+ this.componentRef.instance.observableGenerator =
+ this.observableGenerator;
const removeSub =
this.componentRef.instance.removeWidgetCallback.subscribe(ev =>
this.removeWidget(),
diff --git
a/ui/src/app/data-explorer-shared/components/charts/base/base-data-explorer-widget.directive.ts
b/ui/src/app/data-explorer-shared/components/charts/base/base-data-explorer-widget.directive.ts
index 2eebe05707..37d9a719a0 100644
---
a/ui/src/app/data-explorer-shared/components/charts/base/base-data-explorer-widget.directive.ts
+++
b/ui/src/app/data-explorer-shared/components/charts/base/base-data-explorer-widget.directive.ts
@@ -41,6 +41,7 @@ import { ResizeService } from
'../../../services/resize.service';
import {
BaseWidgetData,
FieldProvider,
+ ObservableGenerator,
} from '../../../models/dataview-dashboard.model';
import { Observable, Subject, Subscription, zip } from 'rxjs';
import { DataExplorerFieldProviderService } from
'../../../services/data-explorer-field-provider-service';
@@ -71,6 +72,7 @@ export abstract class BaseDataExplorerWidgetDirective<
@Input() gridsterItemComponent: GridsterItemComponent;
@Input() editMode: boolean;
@Input() kioskMode: boolean;
+ @Input() observableGenerator: ObservableGenerator;
@Input() timeSettings: TimeSettings;
@@ -245,29 +247,18 @@ export abstract class BaseDataExplorerWidgetDirective<
}
private loadData(includeTooMuchEventsParameter: boolean) {
- let observables: Observable<SpQueryResult>[];
- if (
+ const returnCompleteResult =
includeTooMuchEventsParameter &&
- !this.dataExplorerWidget.dataConfig.ignoreTooMuchDataWarning
- ) {
- observables =
- this.dataViewQueryGeneratorService.generateObservables(
- this.timeSettings.startTime,
- this.timeSettings.endTime,
- this.dataExplorerWidget
- .dataConfig as DataExplorerDataConfig,
- BaseDataExplorerWidgetDirective.TOO_MUCH_DATA_PARAMETER,
- );
- } else {
- observables =
- this.dataViewQueryGeneratorService.generateObservables(
- this.timeSettings.startTime,
- this.timeSettings.endTime,
- this.dataExplorerWidget
- .dataConfig as DataExplorerDataConfig,
- );
- }
-
+ !this.dataExplorerWidget.dataConfig.ignoreTooMuchDataWarning;
+ const observables = this.observableGenerator.generateObservables(
+ this.timeSettings.startTime,
+ this.timeSettings.endTime,
+ this.dataExplorerWidget.dataConfig as DataExplorerDataConfig,
+ this.dataExplorerWidget.elementId,
+ returnCompleteResult
+ ? BaseDataExplorerWidgetDirective.TOO_MUCH_DATA_PARAMETER
+ : undefined,
+ );
this.timerCallback.emit(true);
this.requestQueue$.next(observables);
}
diff --git a/ui/src/app/data-explorer-shared/models/dataview-dashboard.model.ts
b/ui/src/app/data-explorer-shared/models/dataview-dashboard.model.ts
index 56fd261462..86c996c499 100644
--- a/ui/src/app/data-explorer-shared/models/dataview-dashboard.model.ts
+++ b/ui/src/app/data-explorer-shared/models/dataview-dashboard.model.ts
@@ -23,6 +23,7 @@ import {
} from 'angular-gridster2';
import {
ClientDashboardItem,
+ DataExplorerDataConfig,
DataExplorerField,
DataExplorerWidgetModel,
SpLogMessage,
@@ -48,6 +49,7 @@ export interface BaseWidgetData<T extends
DataExplorerWidgetModel> {
gridsterItemComponent: GridsterItemComponent;
editMode: boolean;
kioskMode: boolean;
+ observableGenerator: ObservableGenerator;
timeSettings: TimeSettings;
@@ -60,6 +62,16 @@ export interface BaseWidgetData<T extends
DataExplorerWidgetModel> {
cleanupSubscriptions(): void;
}
+export interface ObservableGenerator {
+ generateObservables(
+ startTime: number,
+ endTime: number,
+ dataConfig: DataExplorerDataConfig,
+ widgetId: string,
+ maxRowCountPerTag: number,
+ ): Observable<SpQueryResult>[];
+}
+
export interface SpEchartsRenderer<T extends DataExplorerWidgetModel> {
render(
queryResult: SpQueryResult[],
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..12e555ebb4 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
@@ -16,10 +16,11 @@
*
*/
-import { Injectable } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import {
DataExplorerDataConfig,
DataExplorerWidgetModel,
+ DataViewQueryGeneratorService,
DateRange,
TimeSettings,
} from '@streampipes/platform-services';
@@ -30,15 +31,22 @@ import {
} from '@streampipes/shared-ui';
import { ObjectPermissionDialogComponent } from
'../../core-ui/object-permission-dialog/object-permission-dialog.component';
import { TranslateService } from '@ngx-translate/core';
+import { ObservableGenerator } from '../models/dataview-dashboard.model';
@Injectable({ providedIn: 'root' })
export class DataExplorerSharedService {
- constructor(
- private dialogService: DialogService,
- private translateService: TranslateService,
- ) {}
+ private dialogService = inject(DialogService);
+ private translateService = inject(TranslateService);
+ private dataViewQueryGeneratorService = inject(
+ DataViewQueryGeneratorService,
+ );
- 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 +54,8 @@ export class DataExplorerSharedService {
data: {
objectInstanceId: elementId,
headerTitle,
+ anonymousReadSupported,
+ publicLink,
},
});
}
@@ -68,4 +78,44 @@ export class DataExplorerSharedService {
},
});
}
+
+ defaultObservableGenerator(): ObservableGenerator {
+ return {
+ generateObservables: (
+ startTime: number,
+ endTime: number,
+ dataConfig: DataExplorerDataConfig,
+ widgetId: string,
+ maxRowCountPerTag: number,
+ ) => {
+ return this.dataViewQueryGeneratorService.generateObservables(
+ startTime,
+ endTime,
+ dataConfig,
+ maxRowCountPerTag,
+ );
+ },
+ };
+ }
+
+ kioskModeObservableGenerator(dashboardId: string): ObservableGenerator {
+ return {
+ generateObservables: (
+ startTime: number,
+ endTime: number,
+ dataConfig: DataExplorerDataConfig,
+ widgetId: string,
+ maxRowCountPerTag: number,
+ ) => {
+ return
this.dataViewQueryGeneratorService.generateObservablesForKioskMode(
+ startTime,
+ endTime,
+ dataConfig,
+ dashboardId,
+ widgetId,
+ maxRowCountPerTag,
+ );
+ },
+ };
+ }
}
diff --git
a/ui/src/app/data-explorer/components/chart-view/data-explorer-chart-view.component.html
b/ui/src/app/data-explorer/components/chart-view/data-explorer-chart-view.component.html
index 9bb81bf9b0..91f9f7b542 100644
---
a/ui/src/app/data-explorer/components/chart-view/data-explorer-chart-view.component.html
+++
b/ui/src/app/data-explorer/components/chart-view/data-explorer-chart-view.component.html
@@ -78,6 +78,7 @@
[configuredWidget]="dataView"
[gridsterItemComponent]="gridsterItemComponent"
[timeSettings]="timeSettings"
+ [observableGenerator]="observableGenerator"
[dataLakeMeasure]="
dataView.dataConfig.sourceConfigs[0].measure
"
diff --git
a/ui/src/app/data-explorer/components/chart-view/data-explorer-chart-view.component.ts
b/ui/src/app/data-explorer/components/chart-view/data-explorer-chart-view.component.ts
index 839a35fa39..0e9c817d9d 100644
---
a/ui/src/app/data-explorer/components/chart-view/data-explorer-chart-view.component.ts
+++
b/ui/src/app/data-explorer/components/chart-view/data-explorer-chart-view.component.ts
@@ -21,7 +21,6 @@ import {
ElementRef,
inject,
OnInit,
- signal,
ViewChild,
} from '@angular/core';
import {
@@ -71,18 +70,19 @@ export class DataExplorerChartViewComponent
resizeEchartsService = inject(ResizeEchartsService);
- @ViewChild('panel', { static: false }) outerPanel: ElementRef;
+ private dataExplorerSharedService = inject(DataExplorerSharedService);
+ private detectChangesService = inject(DataExplorerDetectChangesService);
+ private route = inject(ActivatedRoute);
+ private dialog = inject(MatDialog);
+ private routingService = inject(DataExplorerRoutingService);
+ private dataViewService = inject(ChartService);
+ private timeSelectionService = inject(TimeSelectionService);
+ private translateService = inject(TranslateService);
+
+ observableGenerator =
+ this.dataExplorerSharedService.defaultObservableGenerator();
- constructor(
- private dashboardService: DataExplorerSharedService,
- private detectChangesService: DataExplorerDetectChangesService,
- private route: ActivatedRoute,
- private dialog: MatDialog,
- private routingService: DataExplorerRoutingService,
- private dataViewService: ChartService,
- private timeSelectionService: TimeSelectionService,
- private translateService: TranslateService,
- ) {}
+ @ViewChild('panel', { static: false }) outerPanel: ElementRef;
ngOnInit() {
const dataViewId = this.route.snapshot.params.id;
@@ -233,7 +233,7 @@ export class DataExplorerChartViewComponent
}
downloadDataAsFile() {
- this.dashboardService.downloadDataAsFile(
+ this.dataExplorerSharedService.downloadDataAsFile(
this.timeSettings,
this.dataView,
);
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"