This is an automated email from the ASF dual-hosted git repository. dominikriemer pushed a commit to branch add-resource-dtos in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit d7b47eeaab9a4df2ac5aad6aea12f730344ff0b8 Author: Dominik Riemer <[email protected]> AuthorDate: Fri Jun 12 21:36:13 2026 +0200 feat: Add chart resource dto --- .../model/datalake/ChartSummaryDto.java | 30 +--- .../DataExplorerWidgetResourceManager.java | 39 ++++- .../rest/impl/datalake/DataLakeWidgetResource.java | 24 ++- .../src/lib/apis/chart.service.ts | 16 +- .../lib/model/resource/resource-summary.model.ts | 9 ++ .../chart-overview-table.component.html | 75 ++++----- .../chart-overview-table.component.ts | 167 ++++++++++++--------- 7 files changed, 212 insertions(+), 148 deletions(-) diff --git a/ui/projects/streampipes/platform-services/src/lib/model/resource/resource-summary.model.ts b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/ChartSummaryDto.java similarity index 58% copy from ui/projects/streampipes/platform-services/src/lib/model/resource/resource-summary.model.ts copy to streampipes-model/src/main/java/org/apache/streampipes/model/datalake/ChartSummaryDto.java index c3913ce834..859f8b7b09 100644 --- a/ui/projects/streampipes/platform-services/src/lib/model/resource/resource-summary.model.ts +++ b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/ChartSummaryDto.java @@ -16,28 +16,12 @@ * */ -import { PipelineHealthStatus } from '../gen/streampipes-model'; +package org.apache.streampipes.model.datalake; -export interface ResourceSummaryDto<T> { - totalCount: number; - resources: T[]; -} - -export interface DashboardSummaryDto { - elementId: string; - name: string; - description: string; - createdAtEpochMs: number; - lastModifiedEpochMs: number; -} - -export interface PipelineSummaryDto { - elementId: string; - name: string; - description: string; - createdAt: number; - running: boolean; - healthStatus: PipelineHealthStatus; - pipelineNotifications: string[]; - valid: true; +public record ChartSummaryDto(String elementId, + String name, + long createdAtEpochMs, + long lastModifiedEpochMs, + String widgetType, + boolean multiSourceChart) { } diff --git a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DataExplorerWidgetResourceManager.java b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DataExplorerWidgetResourceManager.java index 54d766f152..37831266e0 100644 --- a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DataExplorerWidgetResourceManager.java +++ b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DataExplorerWidgetResourceManager.java @@ -18,19 +18,42 @@ package org.apache.streampipes.resource.management; +import org.apache.streampipes.model.datalake.ChartSummaryDto; import org.apache.streampipes.model.datalake.DataExplorerWidgetModel; +import org.apache.streampipes.model.resource.ResourceSummaryDto; import org.apache.streampipes.storage.api.explorer.IDataExplorerWidgetStorage; +import org.springframework.security.core.Authentication; + +import java.util.Collection; + public class DataExplorerWidgetResourceManager extends CrudResourceManager<DataExplorerWidgetModel> { private final DataExplorerResourceManager dashboardManager; public DataExplorerWidgetResourceManager(DataExplorerResourceManager dashboardManager, - IDataExplorerWidgetStorage db) { + IDataExplorerWidgetStorage db) { super(db, DataExplorerWidgetModel.class); this.dashboardManager = dashboardManager; } + public ResourceSummaryDto<ChartSummaryDto> getSummary(Authentication auth) { + var charts = findAll() + .stream() + .filter(chart -> permissionEvaluator.hasPermission(auth, chart.getElementId(), "READ")) + .map(chart -> new ChartSummaryDto( + chart.getElementId(), + chart.getBaseAppearanceConfig().get("widgetTitle").toString(), + chart.getMetadata().getCreatedAtEpochMs(), + chart.getMetadata().getLastModifiedEpochMs(), + chart.getWidgetType(), + isMultiSourceChart(chart) + )) + .toList(); + + return new ResourceSummaryDto<>(charts, charts.size()); + } + @Override public void delete(String elementId) { deleteDataViewsFromDashboard(elementId); @@ -42,4 +65,18 @@ public class DataExplorerWidgetResourceManager extends CrudResourceManager<DataE .filter(dashboard -> dashboard.getWidgets().removeIf(w -> w.getDataViewElementId().equals(widgetElementId))) .forEach(dashboardManager::update); } + + private boolean isMultiSourceChart(DataExplorerWidgetModel chart) { + if (chart == null || chart.getDataConfig() == null) { + return false; + } + + Object sourceConfigs = chart.getDataConfig().get("sourceConfigs"); + + if (sourceConfigs instanceof Collection<?>) { + return ((Collection<?>) sourceConfigs).size() > 1; + } + + return false; + } } diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeWidgetResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeWidgetResource.java index fc840505c8..ca2ebe4222 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeWidgetResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeWidgetResource.java @@ -19,7 +19,9 @@ package org.apache.streampipes.rest.impl.datalake; import org.apache.streampipes.model.client.user.DefaultPrivilege; +import org.apache.streampipes.model.datalake.ChartSummaryDto; import org.apache.streampipes.model.datalake.DataExplorerWidgetModel; +import org.apache.streampipes.model.resource.ResourceSummaryDto; import org.apache.streampipes.resource.management.DataExplorerResourceManager; import org.apache.streampipes.resource.management.DataExplorerWidgetResourceManager; import org.apache.streampipes.resource.management.SpResourceManager; @@ -58,13 +60,19 @@ public class DataLakeWidgetResource extends AbstractAuthGuardedRestResource { @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize(AuthConstants.HAS_READ_DATA_EXPLORER_PRIVILEGE) @PostFilter("hasPermission(filterObject.elementId, 'READ')") - public List<DataExplorerWidgetModel> getAllDataExplorerWidgets() { + public List<DataExplorerWidgetModel> getAllCharts() { return resourceManager.findAll(); } - @GetMapping(path = "/{widgetId}", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(path = "/summary", produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("this.hasReadAuthority()") + public ResourceSummaryDto<ChartSummaryDto> getChartSummary() { + return resourceManager.getSummary(getAuthentication()); + } + + @GetMapping(path = "/{chartId}", produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("this.hasReadAuthority() and hasPermission(#elementId, 'READ')") - public ResponseEntity<DataExplorerWidgetModel> getDataExplorerWidget(@PathVariable("widgetId") String elementId) { + public ResponseEntity<DataExplorerWidgetModel> getChart(@PathVariable("chartId") String elementId) { var widget = resourceManager.find(elementId); if (widget != null) { return ok(widget); @@ -74,19 +82,19 @@ public class DataLakeWidgetResource extends AbstractAuthGuardedRestResource { } @PutMapping( - path = "/{widgetId}", + path = "/{chartId}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("this.hasWriteAuthority() and hasPermission(#dataExplorerWidgetModel.elementId, 'WRITE')") - public ResponseEntity<DataExplorerWidgetModel> modifyDataExplorerWidget( + public ResponseEntity<DataExplorerWidgetModel> modifyChart( @RequestBody DataExplorerWidgetModel dataExplorerWidgetModel) { resourceManager.update(dataExplorerWidgetModel); return ok(resourceManager.find(dataExplorerWidgetModel.getElementId())); } - @DeleteMapping(path = "/{widgetId}") + @DeleteMapping(path = "/{chartId}") @PreAuthorize("this.hasWriteAuthority() and hasPermission(#elementId, 'WRITE')") - public ResponseEntity<Void> deleteDataExplorerWidget(@PathVariable("widgetId") String elementId) { + public ResponseEntity<Void> deleteChart(@PathVariable("chartId") String elementId) { resourceManager.delete(elementId); return ok(); } @@ -96,7 +104,7 @@ public class DataLakeWidgetResource extends AbstractAuthGuardedRestResource { consumes = MediaType.APPLICATION_JSON_VALUE ) @PreAuthorize("this.hasWriteAuthority()") - public ResponseEntity<DataExplorerWidgetModel> createDataExplorerWidget( + public ResponseEntity<DataExplorerWidgetModel> createChart( @RequestBody DataExplorerWidgetModel dataExplorerWidgetModel) { return ok(resourceManager.create(dataExplorerWidgetModel, getAuthenticatedUserSid())); } diff --git a/ui/projects/streampipes/platform-services/src/lib/apis/chart.service.ts b/ui/projects/streampipes/platform-services/src/lib/apis/chart.service.ts index 57d77b9d90..a6336f7132 100644 --- a/ui/projects/streampipes/platform-services/src/lib/apis/chart.service.ts +++ b/ui/projects/streampipes/platform-services/src/lib/apis/chart.service.ts @@ -25,6 +25,10 @@ import { DataLakeMeasure, } from '../model/gen/streampipes-model'; import { TranslateService } from '@ngx-translate/core'; +import { + ChartSummaryDto, + ResourceSummaryDto, +} from '../model/resource/resource-summary.model'; @Injectable({ providedIn: 'root', @@ -34,9 +38,15 @@ export class ChartService { private translateService = inject(TranslateService); getAllCharts(): Observable<DataExplorerWidgetModel[]> { - return this.http - .get(this.dashboardWidgetUrl) - .pipe(map(res => res as DataExplorerWidgetModel[])); + return this.http.get<DataExplorerWidgetModel[]>( + this.dashboardWidgetUrl, + ); + } + + getChartSummary(): Observable<ResourceSummaryDto<ChartSummaryDto>> { + return this.http.get<ResourceSummaryDto<ChartSummaryDto>>( + `${this.dashboardWidgetUrl}/summary`, + ); } getChart(widgetId: string): Observable<DataExplorerWidgetModel> { diff --git a/ui/projects/streampipes/platform-services/src/lib/model/resource/resource-summary.model.ts b/ui/projects/streampipes/platform-services/src/lib/model/resource/resource-summary.model.ts index c3913ce834..4f7f73e047 100644 --- a/ui/projects/streampipes/platform-services/src/lib/model/resource/resource-summary.model.ts +++ b/ui/projects/streampipes/platform-services/src/lib/model/resource/resource-summary.model.ts @@ -41,3 +41,12 @@ export interface PipelineSummaryDto { pipelineNotifications: string[]; valid: true; } + +export interface ChartSummaryDto { + elementId: string; + name: string; + createdAtEpochMs: number; + lastModifiedEpochMs: number; + multiSourceChart: boolean; + widgetType: string; +} diff --git a/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.html b/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.html index 351402f118..f1462eab3b 100644 --- a/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.html +++ b/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.html @@ -29,7 +29,7 @@ featureCardId="chart" [showActionsMenu]="true" [rowsClickable]="true" - (rowClicked)="openDataView($event, true)" + (rowClicked)="openChart($event, true)" matSort > <ng-container matColumnDef="name"> @@ -68,29 +68,39 @@ > } <div fxLayout="column" fxLayoutAlign="start start"> - <span class="text-sm">{{ - element.baseAppearanceConfig.widgetTitle - }}</span> + <span class="text-sm">{{ element.name }}</span> </div> </div> </td> </ng-container> + <ng-container matColumnDef="chartType"> + <th mat-header-cell mat-sort-header *matHeaderCellDef> + {{ 'Type' | translate }} + </th> + <td mat-cell *matCellDef="let element"> + <div fxLayout="row" fxLayoutGap="15px" class="text-sm"> + <span fxLayoutAlign="start center" + ><mat-icon>{{ + getChartTypeIcon(element) + }}</mat-icon></span + > + <span fxFlex fxLayoutAlign="start center">{{ + getChartTypeName(element) + }}</span> + </div> + </td> + </ng-container> <ng-container matColumnDef="lastModified"> <th mat-header-cell mat-sort-header *matHeaderCellDef> {{ 'Last modified' | translate }} </th> <td mat-cell *matCellDef="let element"> - @if (element.metadata) { + @if (element.lastModifiedEpochMs !== null) { <div> - {{ - this.formatDate( - element.metadata.lastModifiedEpochMs - ) - }} + {{ this.formatDate(element.lastModifiedEpochMs) }} </div> - } - @if (!element.metadata) { + } @else { <div>–</div> } </td> @@ -101,16 +111,11 @@ {{ 'Created' | translate }} </th> <td mat-cell *matCellDef="let element"> - @if (element.metadata) { + @if (element.createdAtEpochMs !== null) { <div> - {{ - this.formatDate( - element.metadata.createdAtEpochMs - ) - }} + {{ this.formatDate(element.createdAtEpochMs) }} </div> - } - @if (!element.metadata) { + } @else { <div>–</div> } </td> @@ -120,13 +125,9 @@ <button mat-menu-item [attr.data-cy]=" - 'show-data-view-' + - element.baseAppearanceConfig.widgetTitle.replaceAll( - ' ', - '' - ) + 'show-data-view-' + element.name.replaceAll(' ', '') " - (click)="openDataView(element, false)" + (click)="openChart(element, false)" > <mat-icon>visibility</mat-icon> <span>{{ 'Show' | translate }}</span> @@ -135,13 +136,9 @@ <button mat-menu-item [attr.data-cy]=" - 'edit-data-view-' + - element.baseAppearanceConfig.widgetTitle.replaceAll( - ' ', - '' - ) + 'edit-data-view-' + element.name.replaceAll(' ', '') " - (click)="openDataView(element, true)" + (click)="openChart(element, true)" > <mat-icon>edit</mat-icon> <span>{{ 'Edit chart' | translate }}</span> @@ -151,10 +148,7 @@ <button [attr.data-cy]=" 'open-manage-permissions-' + - element.baseAppearanceConfig.widgetTitle.replaceAll( - ' ', - '' - ) + element.name.replaceAll(' ', '') " mat-menu-item (click)="showManageDialog(element)" @@ -164,7 +158,7 @@ </button> } @if (hasDataExplorerWritePrivileges) { - <button mat-menu-item (click)="cloneDataView(element)"> + <button mat-menu-item (click)="cloneChart(element)"> <mat-icon>flip_to_front</mat-icon> <span>{{ 'Clone chart' | translate }}</span> </button> @@ -173,11 +167,8 @@ <button mat-menu-item [matTooltip]="" - [attr.data-cy]=" - 'delete-data-view-' + - element.baseAppearanceConfig.widgetTitle - " - (click)="deleteDataView(element)" + [attr.data-cy]="'delete-data-view-' + element.name" + (click)="deleteChart(element)" > <mat-icon>delete</mat-icon> <span>{{ 'Delete chart' | translate }}</span> diff --git a/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.ts b/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.ts index 1ff0ca0604..3ea21c05ec 100644 --- a/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.ts +++ b/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.ts @@ -27,6 +27,7 @@ import { } from '@angular/material/table'; import { ChartService, + ChartSummaryDto, DataExplorerWidgetModel, } from '@streampipes/platform-services'; import { @@ -56,6 +57,7 @@ import { import { MatMenuItem } from '@angular/material/menu'; import { MatIcon } from '@angular/material/icon'; import { MatTooltip } from '@angular/material/tooltip'; +import { ChartRegistry } from '../../../../chart-shared/registry/chart-registry.service'; type ManageableChart = DataExplorerWidgetModel & { name: string; @@ -94,9 +96,10 @@ export class ChartOverviewTableComponent implements OnInit { @ViewChild(MatSort) sort: MatSort; - dataSource = new MatTableDataSource<DataExplorerWidgetModel>(); + dataSource = new MatTableDataSource<ChartSummaryDto>(); displayedColumns: string[] = [ 'name', + 'chartType', 'assetContext', 'lastModified', 'createdAt', @@ -106,8 +109,8 @@ export class ChartOverviewTableComponent implements OnInit { resourceLinkType: 'chart', resourceIdKey: 'elementId', }; - charts: DataExplorerWidgetModel[] = []; - filteredCharts: DataExplorerWidgetModel[] = []; + charts: ChartSummaryDto[] = []; + filteredCharts: ChartSummaryDto[] = []; private dataViewService = inject(ChartService); private dialog = inject(MatDialog); @@ -116,6 +119,7 @@ export class ChartOverviewTableComponent implements OnInit { private dateFormatService = inject(DateFormatService); private routingService = inject(ChartRoutingService); private assetFilterService = inject(SpAssetBrowserService); + private chartRegistryService = inject(ChartRegistry); assetFilter$: Subscription; currentFilterIds = new Set<string>(); @@ -130,95 +134,102 @@ export class ChartOverviewTableComponent implements OnInit { this.dataSource.sortingDataAccessor = (chart, column) => { if (column === 'name') { - return chart.baseAppearanceConfig.widgetTitle; + return chart.name; } else if (column === 'lastModified') { - return chart.metadata.lastModifiedEpochMs; + return chart.lastModifiedEpochMs; } else if (column === 'createdAt') { - return chart.metadata.createdAtEpochMs; + return chart.createdAtEpochMs; + } else if (column === 'chartType') { + return chart.widgetType; } return chart[column]; }; - this.getDataViews(); + this.getCharts(); } - getDataViews(): void { - this.dataViewService.getAllCharts().subscribe(widgets => { - this.charts = widgets.sort((a, b) => - a.baseAppearanceConfig.widgetTitle.localeCompare( - b.baseAppearanceConfig.widgetTitle, - ), + getCharts(): void { + this.dataViewService.getChartSummary().subscribe(chartSummary => { + this.charts = chartSummary.resources.sort((a, b) => + a.name.localeCompare(b.name), ); this.applyChartFilters(this.currentFilterIds); }); } - openDataView(dataView: DataExplorerWidgetModel, editMode: boolean): void { + openChart(dataView: ChartSummaryDto, editMode: boolean): void { this.routingService.navigateToChart( editMode && this.hasDataExplorerWritePrivileges, dataView.elementId, ); } - showManageDialog(chart: DataExplorerWidgetModel) { - const resource: ManageableChart = { - ...chart, - baseAppearanceConfig: { ...chart.baseAppearanceConfig }, - name: chart.baseAppearanceConfig.widgetTitle, - description: '', - }; - const resourceConfig: ObjectManageDialogResourceConfig<ManageableChart> = - { - resourceLabel: 'Chart', - nameLabel: 'Chart title', - descriptionLabel: 'Chart description', - nameProperty: 'name', - assetLinkType: 'chart', - assetLinkCheckboxLabel: - 'Add the current chart to an existing asset', - saveResource: resource => { - resource.baseAppearanceConfig.widgetTitle = resource.name; - const chartResource: Partial<ManageableChart> = { - ...resource, + showManageDialog(chartSummary: ChartSummaryDto) { + this.dataViewService + .getChart(chartSummary.elementId) + .subscribe(chart => { + const resource: ManageableChart = { + ...chart, + baseAppearanceConfig: { ...chart.baseAppearanceConfig }, + name: chart.baseAppearanceConfig.widgetTitle, + description: '', + }; + const resourceConfig: ObjectManageDialogResourceConfig<ManageableChart> = + { + resourceLabel: 'Chart', + nameLabel: 'Chart title', + descriptionLabel: 'Chart description', + nameProperty: 'name', + assetLinkType: 'chart', + assetLinkCheckboxLabel: + 'Add the current chart to an existing asset', + saveResource: resource => { + resource.baseAppearanceConfig.widgetTitle = + resource.name; + const chartResource: Partial<ManageableChart> = { + ...resource, + }; + delete chartResource.name; + delete chartResource.description; + return this.dataViewService.updateChart( + chartResource as DataExplorerWidgetModel, + ); + }, }; - delete chartResource.name; - delete chartResource.description; - return this.dataViewService.updateChart( - chartResource as DataExplorerWidgetModel, - ); - }, - }; - const dialogRef = this.dialogService.open(ObjectManageDialogComponent, { - panelType: PanelType.SLIDE_IN_PANEL, - title: this.translateService.instant('Manage'), - width: '50vw', - data: { - objectInstanceId: chart.elementId, - resource, - saveMode: 'immediate', - resourceConfig, - headerTitle: - this.translateService.instant('Manage Chart ') + - chart.baseAppearanceConfig.widgetTitle, - }, - }); + const dialogRef = this.dialogService.open( + ObjectManageDialogComponent, + { + panelType: PanelType.SLIDE_IN_PANEL, + title: this.translateService.instant('Manage'), + width: '50vw', + data: { + objectInstanceId: chart.elementId, + resource, + saveMode: 'immediate', + resourceConfig, + headerTitle: + this.translateService.instant('Manage Chart ') + + chart.baseAppearanceConfig.widgetTitle, + }, + }, + ); - dialogRef.afterClosed().subscribe(refresh => { - if (refresh) { - this.getDataViews(); - } - }); + dialogRef.afterClosed().subscribe(refresh => { + if (refresh) { + this.getCharts(); + } + }); + }); } - deleteDataView(dataView: DataExplorerWidgetModel) { + deleteChart(chart: ChartSummaryDto) { const dialogRef = this.dialog.open(ConfirmDialogComponent, { width: '600px', data: { title: this.translateService.instant( 'Are you sure you want to delete chart "{{chartTitle}}"?', { - chartTitle: - dataView.baseAppearanceConfig.widgetTitle ?? '', + chartTitle: chart.name ?? '', }, ), subtitle: this.translateService.instant( @@ -231,18 +242,22 @@ export class ChartOverviewTableComponent implements OnInit { dialogRef.afterClosed().subscribe(result => { if (result === 'confirm') { this.dataViewService - .deleteChart(dataView.elementId) + .deleteChart(chart.elementId) .subscribe(() => { - this.getDataViews(); + this.getCharts(); }); } }); } - cloneDataView(dataView: DataExplorerWidgetModel) { - this.dataViewService.cloneChart(dataView).subscribe(() => { - this.getDataViews(); - }); + cloneChart(chartSummary: ChartSummaryDto) { + this.dataViewService + .getChart(chartSummary.elementId) + .subscribe(chart => { + this.dataViewService.cloneChart(chart).subscribe(() => { + this.getCharts(); + }); + }); } applyChartFilters(elementIds: Set<string>): void { @@ -259,12 +274,22 @@ export class ChartOverviewTableComponent implements OnInit { this.dataSource.data = this.filteredCharts; } + getChartTypeIcon(chart: ChartSummaryDto): string { + return this.chartRegistryService.getChartTemplate(chart.widgetType) + .icon; + } + + getChartTypeName(chart: ChartSummaryDto): string { + return this.chartRegistryService.getChartTemplate(chart.widgetType) + .label; + } + formatDate(timestamp?: number): string { return this.dateFormatService.formatDate(timestamp); } - isLegacyMultiSourceChart(chart: DataExplorerWidgetModel): boolean { - return (chart?.dataConfig?.sourceConfigs?.length ?? 0) > 1; + isLegacyMultiSourceChart(chart: ChartSummaryDto): boolean { + return chart.multiSourceChart; } requiresAttention(chart: DataExplorerWidgetModel): boolean {
