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

SvenO3 pushed a commit to branch 
4633-move-dataset-count-display-into-dataset-overview
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to 
refs/heads/4633-move-dataset-count-display-into-dataset-overview by this push:
     new a0b5de13b5 Add last event column to dataset view
a0b5de13b5 is described below

commit a0b5de13b5b1da969a824c91d88e84d2b47b25ec
Author: Sven Oehler <[email protected]>
AuthorDate: Thu Jun 25 10:41:30 2026 +0200

    Add last event column to dataset view
---
 .../last-updated-formatter.service.ts              | 101 +++++++++++++++++++
 .../components/kiosk/dashboard-kiosk.component.ts  |  54 ++---------
 .../datalake-configuration-entry.ts                |   6 +-
 .../datalake-configuration.component.html          |  88 +----------------
 .../datalake-configuration.component.ts            | 107 ++++++++++++++-------
 5 files changed, 184 insertions(+), 172 deletions(-)

diff --git 
a/ui/src/app/core-services/time-formatting/last-updated-formatter.service.ts 
b/ui/src/app/core-services/time-formatting/last-updated-formatter.service.ts
new file mode 100644
index 0000000000..4f906933b7
--- /dev/null
+++ b/ui/src/app/core-services/time-formatting/last-updated-formatter.service.ts
@@ -0,0 +1,101 @@
+/*
+ * 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 { TranslateService } from '@ngx-translate/core';
+
+@Injectable({ providedIn: 'root' })
+export class LastUpdatedFormatterService {
+    private translateService = inject(TranslateService);
+
+    private exactTimeFormatter: Intl.DateTimeFormat;
+    private relativeTimeFormatter: Intl.RelativeTimeFormat;
+    private formatterLocale: string;
+
+    public formatLastUpdatedAt(
+        updatedAt: number | null | undefined,
+        currentTime = Date.now(),
+        emptyLabel = 'n/a',
+    ): string {
+        if (!updatedAt) {
+            return emptyLabel;
+        }
+
+        const exactTime = this.formatExactTime(updatedAt);
+        const relativeTime = this.formatRelativeTime(updatedAt, currentTime);
+        return relativeTime ? `${relativeTime} (${exactTime})` : exactTime;
+    }
+
+    private formatExactTime(updatedAt: number): string {
+        this.updateFormatters();
+        return this.exactTimeFormatter.format(new Date(updatedAt));
+    }
+
+    private formatRelativeTime(
+        updatedAt: number,
+        currentTime: number,
+    ): string | undefined {
+        const ageInSeconds = Math.max(
+            0,
+            Math.round((currentTime - updatedAt) / 1000),
+        );
+
+        if (ageInSeconds < 60) {
+            this.updateFormatters();
+            return this.relativeTimeFormatter.format(-ageInSeconds, 'second');
+        } else if (ageInSeconds < 3600) {
+            this.updateFormatters();
+            return this.relativeTimeFormatter.format(
+                -Math.floor(ageInSeconds / 60),
+                'minute',
+            );
+        } else if (ageInSeconds < 86400) {
+            this.updateFormatters();
+            return this.relativeTimeFormatter.format(
+                -Math.floor(ageInSeconds / 3600),
+                'hour',
+            );
+        }
+
+        return undefined;
+    }
+
+    private updateFormatters(): void {
+        const locale = this.currentLocale;
+        if (this.formatterLocale === locale) {
+            return;
+        }
+
+        this.exactTimeFormatter = new Intl.DateTimeFormat(locale, {
+            dateStyle: 'medium',
+            timeStyle: 'medium',
+        });
+        this.relativeTimeFormatter = new Intl.RelativeTimeFormat(locale, {
+            numeric: 'auto',
+        });
+        this.formatterLocale = locale;
+    }
+
+    private get currentLocale(): string {
+        return (
+            this.translateService.getCurrentLang() ||
+            this.translateService.getFallbackLang() ||
+            'en'
+        );
+    }
+}
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 8686067d34..e8b62b8e3c 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
@@ -45,7 +45,8 @@ import {
     LayoutAlignDirective,
     LayoutDirective,
 } from '@ngbracket/ngx-layout/flex';
-import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { TranslatePipe } from '@ngx-translate/core';
+import { LastUpdatedFormatterService } from 
'../../../core-services/time-formatting/last-updated-formatter.service';
 
 @Component({
     selector: 'sp-dashboard-kiosk',
@@ -66,9 +67,9 @@ export class DashboardKioskComponent implements OnInit, 
OnDestroy {
     private destroyRef = inject(DestroyRef);
     private dashboardService = inject(DashboardService);
     private timeSelectionService = inject(TimeSelectionService);
-    private translateService = inject(TranslateService);
     private dataExplorerDashboardService = 
inject(DataExplorerDashboardService);
     private dataExplorerSharedService = inject(ChartSharedService);
+    private lastUpdatedFormatterService = inject(LastUpdatedFormatterService);
 
     observableGenerator: ObservableGenerator;
     dashboard: Dashboard;
@@ -188,55 +189,12 @@ export class DashboardKioskComponent implements OnInit, 
OnDestroy {
     }
 
     formatLastUpdatedAt(): string {
-        const exactTime = this.formatExactLastUpdatedAt();
-        const relativeTime = this.formatRelativeLastUpdatedAt();
-        return relativeTime ? `${relativeTime} (${exactTime})` : exactTime;
-    }
-
-    private formatExactLastUpdatedAt(): string {
-        return new Intl.DateTimeFormat(this.currentLocale, {
-            dateStyle: 'medium',
-            timeStyle: 'medium',
-        }).format(new Date(this.lastUpdatedAt));
-    }
-
-    private formatRelativeLastUpdatedAt(): string | undefined {
-        const ageInSeconds = Math.max(
-            0,
-            Math.round((this.currentTime - this.lastUpdatedAt) / 1000),
-        );
-
-        if (ageInSeconds < 60) {
-            return this.relativeTimeFormatter.format(-ageInSeconds, 'second');
-        } else if (ageInSeconds < 3600) {
-            return this.relativeTimeFormatter.format(
-                -Math.floor(ageInSeconds / 60),
-                'minute',
-            );
-        } else if (ageInSeconds < 86400) {
-            return this.relativeTimeFormatter.format(
-                -Math.floor(ageInSeconds / 3600),
-                'hour',
-            );
-        }
-
-        return undefined;
-    }
-
-    private get currentLocale(): string {
-        return (
-            this.translateService.currentLang ||
-            this.translateService.defaultLang ||
-            'en'
+        return this.lastUpdatedFormatterService.formatLastUpdatedAt(
+            this.lastUpdatedAt,
+            this.currentTime,
         );
     }
 
-    private get relativeTimeFormatter(): Intl.RelativeTimeFormat {
-        return new Intl.RelativeTimeFormat(this.currentLocale, {
-            numeric: 'auto',
-        });
-    }
-
     ngOnDestroy() {
         this.refresh$?.unsubscribe();
         this.dashboardRefresh$?.unsubscribe();
diff --git 
a/ui/src/app/dataset/components/datalake-configuration/datalake-configuration-entry.ts
 
b/ui/src/app/dataset/components/datalake-configuration/datalake-configuration-entry.ts
index a6b3aadef5..c6f9a7f1e9 100644
--- 
a/ui/src/app/dataset/components/datalake-configuration/datalake-configuration-entry.ts
+++ 
b/ui/src/app/dataset/components/datalake-configuration/datalake-configuration-entry.ts
@@ -20,10 +20,8 @@ export class DataLakeConfigurationEntry {
     public name: string;
     public measureName: string;
     public pipelines: string[] = [];
-    public eventsTotal = 0;
-    public eventsLatest = 0;
-    public eventsTotalLoading = false;
-    public eventsLatestLoading = false;
+    public lastEvent: number | null = null;
+    public lastEventLoading = false;
     public remove = true;
     public elementId: string;
     public retentionConfigured = false;
diff --git 
a/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.html
 
b/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.html
index 3b3e882896..0c00c5679f 100644
--- 
a/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.html
+++ 
b/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.html
@@ -49,14 +49,6 @@
             </button>
         </div>
         <div fxLayout="column" class="w-100" fxLayoutGap="10px">
-            <sp-alert-banner
-                type="info"
-                [title]="'Caution when loading total count' | translate"
-                [description]="
-                    'For large datasets, computing the total number of events 
can take a long time.'
-                "
-            >
-            </sp-alert-banner>
             <sp-table
                 featureCardId="measurement"
                 [dataSource]="dataSource"
@@ -90,16 +82,16 @@
                     </td>
                 </ng-container>
 
-                <ng-container matColumnDef="eventsLatest">
+                <ng-container matColumnDef="lastEvent">
                     <th mat-header-cell mat-sort-header *matHeaderCellDef>
-                        {{ '# Events (7d)' | translate }}
+                        {{ 'Last event' | translate }}
                     </th>
                     <td
                         mat-cell
-                        data-cy="datalake-number-of-events-latest"
+                        data-cy="datalake-last-event"
                         *matCellDef="let configurationEntry"
                     >
-                        @if (configurationEntry.eventsLatestLoading) {
+                        @if (configurationEntry.lastEventLoading) {
                             <mat-spinner
                                 [diameter]="20"
                                 fxLayoutAlign="center"
@@ -107,81 +99,11 @@
                                 >{{ 'Loading' | translate }}
                             </mat-spinner>
                         } @else {
-                            <sp-label
-                                tone="neutral"
-                                minWidth="100px"
-                                [labelText]="
-                                    configurationEntry.eventsLatest | number
-                                "
-                                size="small"
-                            >
-                            </sp-label>
+                            {{ formatLastEvent(configurationEntry.lastEvent) }}
                         }
                     </td>
                 </ng-container>
 
-                <ng-container matColumnDef="eventsTotal">
-                    <th mat-header-cell mat-sort-header *matHeaderCellDef>
-                        {{ '# Events (total)' | translate }}
-                    </th>
-                    <td
-                        mat-cell
-                        data-cy="datalake-number-of-events"
-                        *matCellDef="let configurationEntry"
-                    >
-                        <div fxLayout="row" fxLayoutAlign="start center">
-                            @if (configurationEntry.eventsTotalLoading) {
-                                <mat-spinner
-                                    [diameter]="20"
-                                    fxLayoutAlign="center"
-                                    color="accent"
-                                    data-cy="datalake-number-of-events-spinner"
-                                    >{{ 'Loading' | translate }}
-                                </mat-spinner>
-                            } @else {
-                                @if (configurationEntry.eventsTotal > -1) {
-                                    <sp-label
-                                        tone="neutral"
-                                        class="cursor-pointer"
-                                        data-cy="datalake-total-count-value"
-                                        minWidth="100px"
-                                        [labelText]="
-                                            configurationEntry.eventsTotal
-                                                | number
-                                        "
-                                        size="small"
-                                        (click)="
-                                            receiveTotalMeasurementSize(
-                                                configurationEntry
-                                            );
-                                            $event.stopPropagation()
-                                        "
-                                    >
-                                    </sp-label>
-                                } @else {
-                                    <sp-label
-                                        tone="neutral"
-                                        class="cursor-pointer"
-                                        data-cy="datalake-total-count-button"
-                                        minWidth="100px"
-                                        [labelText]="
-                                            'Click to load' | translate
-                                        "
-                                        size="small"
-                                        (click)="
-                                            receiveTotalMeasurementSize(
-                                                configurationEntry
-                                            );
-                                            $event.stopPropagation()
-                                        "
-                                    >
-                                    </sp-label>
-                                }
-                            }
-                        </div>
-                    </td>
-                </ng-container>
-
                 <ng-container matColumnDef="retention">
                     <th mat-header-cell *matHeaderCellDef>
                         {{ 'Retention' | translate }}
diff --git 
a/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.ts
 
b/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.ts
index b98ac2278a..c8977701f6 100644
--- 
a/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.ts
+++ 
b/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.ts
@@ -45,6 +45,7 @@ import {
     DatasetSummaryDto,
     ExportProviderService,
     ExportProviderSettings,
+    SpQueryResult,
 } from '@streampipes/platform-services';
 import { MatPaginator } from '@angular/material/paginator';
 import { MatSort, MatSortHeader } from '@angular/material/sort';
@@ -57,11 +58,9 @@ import {
     ObjectPermissionDialogComponent,
     PanelType,
     SpAssetBrowserService,
-    SpAlertBannerComponent,
     SpBasicHeaderTitleComponent,
     SpBasicViewComponent,
     SpBreadcrumbService,
-    SpLabelComponent,
     SpTableAssetContextConfig,
     SpTableActionsDirective,
     SpTableComponent,
@@ -88,10 +87,11 @@ import { MatButton, MatIconButton } from 
'@angular/material/button';
 import { MatTooltip } from '@angular/material/tooltip';
 import { MatIcon } from '@angular/material/icon';
 import { MatProgressSpinner } from '@angular/material/progress-spinner';
-import { DatePipe, DecimalPipe, NgStyle } from '@angular/common';
+import { DatePipe, NgStyle } from '@angular/common';
 import { StyleDirective } from '@ngbracket/ngx-layout/extended';
 import { MatMenuItem } from '@angular/material/menu';
-import { forkJoin, Subscription } from 'rxjs';
+import { catchError, forkJoin, map, of, Subscription } from 'rxjs';
+import { LastUpdatedFormatterService } from 
'../../../core-services/time-formatting/last-updated-formatter.service';
 
 @Component({
     selector: 'sp-datalake-configuration',
@@ -123,14 +123,11 @@ import { forkJoin, Subscription } from 'rxjs';
         MatHeaderRow,
         MatRowDef,
         MatRow,
-        DecimalPipe,
         DatePipe,
         TranslatePipe,
-        SpLabelComponent,
         SpTableComponent,
         SpBasicHeaderTitleComponent,
         SpBasicViewComponent,
-        SpAlertBannerComponent,
         SpTableActionsDirective,
     ],
 })
@@ -150,6 +147,7 @@ export class DatalakeConfigurationComponent
     private currentUserService = inject(CurrentUserService);
     private assetFilterService = inject(SpAssetBrowserService);
     private router = inject(Router);
+    private lastUpdatedFormatterService = inject(LastUpdatedFormatterService);
 
     dataSource: MatTableDataSource<DataLakeConfigurationEntry> =
         new MatTableDataSource([]);
@@ -168,8 +166,7 @@ export class DatalakeConfigurationComponent
         'name',
         'assetContext',
         'pipeline',
-        'eventsLatest',
-        'eventsTotal',
+        'lastEvent',
         'retention',
         'actions',
     ];
@@ -190,6 +187,7 @@ export class DatalakeConfigurationComponent
     isAdmin = false;
     writeAccess = false;
     assetFilter$: Subscription;
+    currentTime = Date.now();
     currentFilterIds: Set<string> = new Set<string>();
 
     ngOnInit(): void {
@@ -219,7 +217,7 @@ export class DatalakeConfigurationComponent
         this.spTable.paginator.page.subscribe(event => {
             this.pageIndex = event.pageIndex;
             this.pageSize = event.pageSize;
-            this.receiveMeasurementSizes(this.pageIndex);
+            this.receiveLastEventTimes(this.pageIndex);
         });
     }
 
@@ -267,7 +265,7 @@ export class DatalakeConfigurationComponent
 
         this.dataSource.data = this.filteredMeasurements;
         this.updatePaginatorAfterFiltering();
-        this.receiveMeasurementSizes(this.pageIndex);
+        this.receiveLastEventTimes(this.pageIndex);
 
         setTimeout(() => {
             this.dataSource.paginator = this.paginator;
@@ -432,21 +430,16 @@ export class DatalakeConfigurationComponent
     onPageChange(event: any): void {
         this.pageIndex = event.pageIndex;
         this.pageSize = event.pageSize;
-        //this.receiveMeasurementSizes(this.pageIndex);
     }
 
-    receiveTotalMeasurementSize(entry: DataLakeConfigurationEntry): void {
-        this.queryEntryCounts([entry], 'eventsTotal');
-    }
-
-    receiveMeasurementSizes(pageIndex: number): void {
+    receiveLastEventTimes(pageIndex: number): void {
         const start = pageIndex * this.pageSize;
         const end = start + this.pageSize;
         const measurements = this.filteredMeasurements
             .slice(start, end)
-            .filter(m => m.eventsLatest === -1);
+            .filter(m => m.lastEvent === null);
         if (measurements.length > 0) {
-            this.queryEntryCounts(measurements, 'eventsLatest', 7);
+            this.queryLastEventTimes(measurements);
         }
     }
     showPermissionsDialog(element: DataLakeConfigurationEntry): void {
@@ -487,38 +480,51 @@ export class DatalakeConfigurationComponent
         });
     }
 
-    queryEntryCounts(
-        measurements: DataLakeConfigurationEntry[],
-        targetField: string,
-        daysBack = -1,
-    ): void {
-        this.applyLoadingStatus(measurements, targetField, true);
+    queryLastEventTimes(measurements: DataLakeConfigurationEntry[]): void {
+        this.applyLastEventLoadingStatus(measurements, true);
         forkJoin(
             measurements.map(measurement =>
-                this.datalakeRestService.getMeasurementEntryCount(
-                    measurement.elementId,
-                    daysBack,
-                ),
+                this.datalakeRestService
+                    .getData(
+                        measurement.name,
+                        {
+                            endDate: Date.now(),
+                            startDate: 0,
+                            limit: 1,
+                            order: 'DESC',
+                            missingValueBehaviour: 'empty',
+                        },
+                        true,
+                    )
+                    .pipe(
+                        map(result => this.extractLastTimestamp(result)),
+                        catchError(() => of(0)),
+                    ),
             ),
         ).subscribe(res => {
-            this.applyLoadingStatus(measurements, targetField, false);
+            this.applyLastEventLoadingStatus(measurements, false);
             measurements.forEach((measurement, index) => {
-                measurement[targetField] = res[index];
+                measurement.lastEvent = res[index];
             });
         });
     }
 
-    applyLoadingStatus(
+    applyLastEventLoadingStatus(
         measurements: DataLakeConfigurationEntry[],
-        targetField: string,
         status: boolean,
     ): void {
-        const loadingField = targetField + 'Loading';
         measurements.forEach(measurement => {
-            measurement[loadingField] = status;
+            measurement.lastEventLoading = status;
         });
     }
 
+    formatLastEvent(lastEventAt: number | null): string {
+        return this.lastUpdatedFormatterService.formatLastUpdatedAt(
+            lastEventAt,
+            this.currentTime,
+        );
+    }
+
     private toConfigurationEntry(
         measurement: DatasetSummaryDto,
     ): DataLakeConfigurationEntry {
@@ -531,11 +537,38 @@ export class DatalakeConfigurationComponent
         entry.lastExport = measurement.lastExport;
         entry.lastRetentionStatus = measurement.lastRetentionStatus;
         entry.remove = measurement.removable;
-        entry.eventsLatest = -1;
-        entry.eventsTotal = -1;
+        entry.lastEvent = null;
         return entry;
     }
 
+    private extractLastTimestamp(result: SpQueryResult): number {
+        if (result?.lastTimestamp) {
+            return result.lastTimestamp;
+        }
+
+        const series = result?.allDataSeries?.find(
+            dataSeries => dataSeries.rows?.length > 0,
+        );
+        const row = series?.rows?.[0];
+        if (!row) {
+            return 0;
+        }
+
+        const headers = result.headers?.length
+            ? result.headers
+            : series.headers;
+        const timestampIndex = headers?.indexOf('time') ?? 0;
+        return this.toTimestamp(row[timestampIndex >= 0 ? timestampIndex : 0]);
+    }
+
+    private toTimestamp(value: unknown): number {
+        const timestamp =
+            typeof value === 'number'
+                ? value
+                : new Date(String(value)).getTime();
+        return Number.isNaN(timestamp) ? 0 : timestamp;
+    }
+
     private openRetentionLogDialog(measurement: DataLakeMeasure): void {
         const dialogRef: DialogRef<DataRetentionLogDialogComponent> =
             this.dialogService.open(DataRetentionLogDialogComponent, {

Reply via email to