This is an automated email from the ASF dual-hosted git repository.
dominikriemer 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 bd607e3943 feat(#4566): Add details view for data sets (#4592)
bd607e3943 is described below
commit bd607e3943c75dc8904b89cdf47f87457e51c69b
Author: Sven Oehler <[email protected]>
AuthorDate: Mon Jun 22 13:05:48 2026 +0200
feat(#4566): Add details view for data sets (#4592)
Co-authored-by: Dominik Riemer <[email protected]>
---
ui/cypress/support/utils/GeneralUtils.ts | 2 +-
ui/cypress/support/utils/dataset/DatasetBtns.ts | 34 ++++
ui/cypress/support/utils/dataset/DatasetUtils.ts | 61 +++++++
ui/cypress/tests/dataset/datasetDetails.spec.ts | 58 ++++++
ui/deployment/i18n/de.json | 4 +
ui/deployment/i18n/en.json | 4 +
ui/deployment/i18n/pl.json | 4 +
.../data-settings/chart-data-settings.component.ts | 15 +-
.../datalake-configuration.component.html | 26 ++-
.../datalake-configuration.component.ts | 6 +
.../abstract-dataset-details.directive.ts | 69 +++++++
.../dataset-details-events.component.html | 191 ++++++++++++++++++++
.../dataset-details-events.component.scss | 119 ++++++++++++
.../dataset-details-events.component.ts | 201 +++++++++++++++++++++
.../dataset-details-schema.component.html | 122 +++++++++++++
.../dataset-details-schema.component.scss} | 45 +++--
.../dataset-details-schema.component.ts | 134 ++++++++++++++
.../dataset-details/dataset-details-tabs.ts} | 26 +--
.../dataset-feature-card.component.ts | 6 +-
ui/src/app/dataset/dataset.routes.ts | 21 +++
20 files changed, 1117 insertions(+), 31 deletions(-)
diff --git a/ui/cypress/support/utils/GeneralUtils.ts
b/ui/cypress/support/utils/GeneralUtils.ts
index e8ace9468a..88a3476332 100644
--- a/ui/cypress/support/utils/GeneralUtils.ts
+++ b/ui/cypress/support/utils/GeneralUtils.ts
@@ -18,7 +18,7 @@
export class GeneralUtils {
public static tab(identifier: string) {
- return cy.dataCy(`tab-${identifier}`).click();
+ return cy.get(`[data-cy="tab-${identifier}"]`).click();
}
public static openMenuForRow(rowText: string) {
diff --git a/ui/cypress/support/utils/dataset/DatasetBtns.ts
b/ui/cypress/support/utils/dataset/DatasetBtns.ts
index a3ea274575..4cd6a5b4d6 100644
--- a/ui/cypress/support/utils/dataset/DatasetBtns.ts
+++ b/ui/cypress/support/utils/dataset/DatasetBtns.ts
@@ -111,6 +111,40 @@ export class DatasetBtns {
});
}
+ public static datasetDetailsSchemaTable() {
+ return cy.dataCy('dataset-details-schema-table', { timeout: 10000 });
+ }
+
+ public static datasetDetailsSchemaField(runtimeName: string) {
+ return cy.dataCy(`dataset-details-schema-field-${runtimeName}`, {
+ timeout: 10000,
+ });
+ }
+
+ public static datasetDetailsSchemaType(runtimeName: string) {
+ return cy.dataCy(`dataset-details-schema-type-${runtimeName}`, {
+ timeout: 10000,
+ });
+ }
+
+ public static datasetDetailsEventLimit() {
+ return cy.dataCy('dataset-details-event-limit', { timeout: 10000 });
+ }
+
+ public static datasetDetailsEventsTable() {
+ return cy.dataCy('dataset-details-events-table', { timeout: 30000 });
+ }
+
+ public static datasetDetailsEventCell(columnName: string) {
+ return cy.dataCy(`dataset-details-event-cell-${columnName}`, {
+ timeout: 30000,
+ });
+ }
+
+ public static datasetDetailsCreateChart() {
+ return cy.dataCy('dataset-details-create-chart', { timeout: 10000 });
+ }
+
public static datasetTotalCountCell(name: string) {
return this.datasetRow(name).find(
'[data-cy="datalake-number-of-events"]',
diff --git a/ui/cypress/support/utils/dataset/DatasetUtils.ts
b/ui/cypress/support/utils/dataset/DatasetUtils.ts
index 8ff9ce4b71..e5ef33302c 100644
--- a/ui/cypress/support/utils/dataset/DatasetUtils.ts
+++ b/ui/cypress/support/utils/dataset/DatasetUtils.ts
@@ -17,6 +17,7 @@
*/
import { PermissionUtils } from '../user/PermissionUtils';
+import { GeneralUtils } from '../GeneralUtils';
import { DatasetBtns } from './DatasetBtns';
export class DatasetUtils {
@@ -150,6 +151,66 @@ export class DatasetUtils {
.click();
}
+ public static openDatasetDetails(datasetName: string) {
+ DatasetUtils.goToDatasets();
+ DatasetUtils.waitForTotalEvents(datasetName);
+ DatasetBtns.datasetRow(datasetName).click();
+ cy.url().should('include', '#/datasets/');
+ }
+
+ public static waitForTotalEvents(datasetName: string, attempts = 30) {
+ DatasetBtns.datasetTotalCountButton(datasetName).click({
+ force: true,
+ });
+ DatasetBtns.datasetTotalCountCell(datasetName)
+ .find('[data-cy="datalake-number-of-events-spinner"]')
+ .should('not.exist');
+ DatasetBtns.datasetTotalCountCell(datasetName).then($cell => {
+ const eventCount = DatasetUtils.parseEventCount($cell.text());
+
+ if (eventCount > 0) {
+ expect(eventCount).to.be.greaterThan(0);
+ } else if (attempts > 0) {
+ cy.wait(1000);
+ DatasetUtils.waitForTotalEvents(datasetName, attempts - 1);
+ } else {
+ expect(eventCount).to.be.greaterThan(0);
+ }
+ });
+ }
+
+ private static parseEventCount(text: string) {
+ return Number(text.trim().replace(/[^\d.-]/g, ''));
+ }
+
+ public static openLatestEventsTab() {
+ GeneralUtils.tab('Latest events');
+ }
+
+ public static setLatestEventsLimit(limit: number) {
+ DatasetBtns.datasetDetailsEventLimit().clear().type(`${limit}`);
+ DatasetBtns.datasetDetailsEventLimit().blur();
+ }
+
+ public static expectSchemaField(runtimeName: string, expectedType: string)
{
+
DatasetBtns.datasetDetailsSchemaField(runtimeName).should('be.visible');
+ DatasetBtns.datasetDetailsSchemaType(runtimeName).should(
+ 'contain.text',
+ expectedType,
+ );
+ }
+
+ public static expectLatestEventsForColumn(columnName: string) {
+ DatasetBtns.datasetDetailsEventsTable().should('be.visible');
+ DatasetBtns.datasetDetailsEventCell(columnName)
+ .should('exist')
+ .and('have.length.at.least', 1);
+ }
+
+ public static createChartFromDatasetDetails() {
+ DatasetBtns.datasetDetailsCreateChart().click();
+ }
+
public static expectDatasetPreviewDoesNotContainKey(key: string) {
cy.dataCy('dataset-preview-table', { timeout: 10000 }).should(
'be.visible',
diff --git a/ui/cypress/tests/dataset/datasetDetails.spec.ts
b/ui/cypress/tests/dataset/datasetDetails.spec.ts
new file mode 100644
index 0000000000..b17838fcdc
--- /dev/null
+++ b/ui/cypress/tests/dataset/datasetDetails.spec.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 { ConnectUtils } from '../../support/utils/connect/ConnectUtils';
+import { DatasetBtns } from '../../support/utils/dataset/DatasetBtns';
+import { DatasetUtils } from '../../support/utils/dataset/DatasetUtils';
+
+describe('Test Data Set Details', () => {
+ beforeEach('Setup Test', () => {
+ cy.initStreamPipesTest();
+ });
+
+ it('Shows schema and latest events and opens chart creation', () => {
+ const adapterName = 'Machine Data Simulator Dataset Details';
+
+ ConnectUtils.addMachineDataSimulator(adapterName, true, '100');
+ DatasetUtils.openDatasetDetails(adapterName);
+
+ DatasetBtns.datasetDetailsSchemaTable().should('be.visible');
+ DatasetUtils.expectSchemaField('density', 'Number');
+ DatasetUtils.expectSchemaField('mass_flow', 'Number');
+ DatasetUtils.expectSchemaField('sensorId', 'Text');
+ DatasetUtils.expectSchemaField('sensor_fault_flags', 'Boolean');
+
+ DatasetUtils.openLatestEventsTab();
+ DatasetUtils.expectLatestEventsForColumn('density');
+ DatasetUtils.expectLatestEventsForColumn('mass_flow');
+ DatasetUtils.setLatestEventsLimit(5);
+ DatasetUtils.expectLatestEventsForColumn('density');
+ DatasetUtils.expectLatestEventsForColumn('mass_flow');
+
+ DatasetUtils.createChartFromDatasetDetails();
+
+ cy.url().should('include', '#/chart/create');
+ cy.url().should(
+ 'include',
+ `measureName=${encodeURIComponent(adapterName)}`,
+ );
+ cy.dataCy('data-explorer-select-data-set').should(
+ 'have.value',
+ adapterName,
+ );
+ });
+});
diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index a5d72cf6a9..b8f936636e 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -247,6 +247,7 @@
"Create adapter": "Adapter erstellen",
"Create asset": "Asset erstellen",
"Create asset links": "Asset-Links erstellen",
+ "Create chart": "Diagramm erstellen",
"Create chart in new tab": "Diagramm in neuem Tab erstellen",
"Create link": "Link erstellen",
"Create new API key": "Neuen API-Schlüssel erstellen",
@@ -425,6 +426,7 @@
"Error: {{message}} with cause {{cause}}": "Fehler: {{message}} mit Ursache
{{cause}}",
"Event Aggregation": "Aggregation",
"Event Transformation Configuration has changed": "Die Transformation von
Events wurde geändert",
+ "Events": "Ereignisse",
"Everything older than": "Älter als",
"Exact location of the site": "Genaue Lage des Standorts",
"Excel template": "Excel Vorlage",
@@ -729,6 +731,7 @@
"No datasets available": "Keine Datensätze verfügbar",
"No datasets match your search.": "Keine Datensätze entsprechen deiner
Suche.",
"No entries available.": "Keine Einträge vorhanden.",
+ "No event schema available.": "Kein Ereignisschema verfügbar.",
"No export providers found": "Keine Exportanbieter gefunden",
"No groups configured": "Keine Gruppen konfiguriert",
"No labels available - Click 'Manage Labels'": "Keine Labels verfügbar -
Klicken Sie auf 'Labels verwalten'",
@@ -1102,6 +1105,7 @@
"The desired adapter was not found!": "Der gewünschte Adapter wurde nicht
gefunden!",
"The desired chart was not found!": "Die gewünschte Karte wurde nicht
gefunden!",
"The desired dashboard was not found!": "Das gewünschte Dashboard wurde
nicht gefunden!",
+ "The desired dataset was not found!": "Das gewünschte Dataset wurde nicht
gefunden!",
"The desired pipeline was not found!": "Die gewünschte Pipeline wurde nicht
gefunden",
"The following fields used by this chart no longer exist in the dataset:":
"Die folgenden von diesem Diagramm verwendeten Felder existieren nicht mehr im
Dataset:",
"The following files already exist. Please rename them.": "Die folgenden
Dateien existieren bereits. Bitte umbenennen.",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index f4b2ed0f2d..afd3952a86 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -247,6 +247,7 @@
"Create adapter": null,
"Create asset": null,
"Create asset links": null,
+ "Create chart": null,
"Create chart in new tab": null,
"Create link": null,
"Create new API key": null,
@@ -425,6 +426,7 @@
"Error: {{message}} with cause {{cause}}": "Error: {{message}} with cause
{{cause}}",
"Event Aggregation": null,
"Event Transformation Configuration has changed": null,
+ "Events": null,
"Everything older than": null,
"Exact location of the site": null,
"Excel template": null,
@@ -729,6 +731,7 @@
"No datasets available": null,
"No datasets match your search.": null,
"No entries available.": null,
+ "No event schema available.": null,
"No export providers found": null,
"No groups configured": null,
"No labels available - Click 'Manage Labels'": null,
@@ -1102,6 +1105,7 @@
"The desired adapter was not found!": null,
"The desired chart was not found!": null,
"The desired dashboard was not found!": null,
+ "The desired dataset was not found!": null,
"The desired pipeline was not found!": null,
"The following fields used by this chart no longer exist in the dataset:":
null,
"The following files already exist. Please rename them.": null,
diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json
index 4ff88dd0b2..4a2e1b3513 100644
--- a/ui/deployment/i18n/pl.json
+++ b/ui/deployment/i18n/pl.json
@@ -247,6 +247,7 @@
"Create adapter": "Utwórz adapter",
"Create asset": "Utwórz zasób",
"Create asset links": "Utwórz linki zasobu",
+ "Create chart": "Utwórz wykres",
"Create chart in new tab": "Utwórz wykres w nowej karcie",
"Create link": "Utwórz link",
"Create new API key": "Utwórz nowy klucz API",
@@ -425,6 +426,7 @@
"Error: {{message}} with cause {{cause}}": "Błąd: {{message}} z powodu
{{cause}}",
"Event Aggregation": "Agregacja",
"Event Transformation Configuration has changed": "Konfiguracja
transformacji zdarzeń została zmieniona",
+ "Events": "Zdarzenia",
"Everything older than": "Wszystko starsze niż",
"Exact location of the site": "Dokładne położenie lokalizacji",
"Excel template": "Szablon Excela",
@@ -729,6 +731,7 @@
"No datasets available": "Brak dostępnych zbiorów danych",
"No datasets match your search.": "Żadne zbiory danych nie pasują do
wyszukiwania.",
"No entries available.": "Brak dostępnych wpisów.",
+ "No event schema available.": "Brak dostępnego schematu zdarzeń.",
"No export providers found": "Nie znaleziono exportera",
"No groups configured": "Nie skonfigurowano grup",
"No labels available - Click 'Manage Labels'": "Brak dostępnych etykiet -
kliknij 'Zarządzaj etykietami'",
@@ -1102,6 +1105,7 @@
"The desired adapter was not found!": "Nie znaleziono żądanego adaptera!",
"The desired chart was not found!": "Nie znaleziono żądanego wykresu!",
"The desired dashboard was not found!": "Nie znaleziono żądanego pulpitu!",
+ "The desired dataset was not found!": "Nie znaleziono żądanego zbioru
danych!",
"The desired pipeline was not found!": "Nie znaleziono żądanego strumienia!",
"The following fields used by this chart no longer exist in the dataset:":
"Następujące pola używane przez ten wykres nie istnieją już w zbiorze danych:",
"The following files already exist. Please rename them.": "Następujące pliki
już istnieją. Zmień ich nazwy.",
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
index 2875af0354..a441cd8fb4 100644
---
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
@@ -34,7 +34,7 @@ import {
SourceConfig,
} from '@streampipes/platform-services';
import { Tuple2 } from '../../../../../core-model/base/Tuple2';
-import { Router } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
import { ChartConfigurationService } from
'../../../../../chart-shared/services/chart-configuration.service';
import { FieldSelectionPanelComponent } from
'./field-selection-panel/field-selection-panel.component';
import { GroupSelectionPanelComponent } from
'./group-selection-panel/group-selection-panel.component';
@@ -133,6 +133,7 @@ export class ChartDataSettingsComponent implements OnInit {
private fieldProviderService = inject(ChartFieldProviderService);
private widgetTypeService = inject(ChartTypeService);
private router = inject(Router);
+ private route = inject(ActivatedRoute);
@Input() dataConfig: DataExplorerDataConfig;
@Input() dataLakeMeasure: DataLakeMeasure;
@@ -205,6 +206,18 @@ export class ChartDataSettingsComponent implements OnInit {
findDefaultConfig(): {
measureName: string | undefined;
} {
+ const measureNameFromQueryParams =
+ this.route.snapshot.queryParams.measureName;
+ const matchingMeasurement = this.availableMeasurements.find(
+ measurement =>
+ measurement.measureName === measureNameFromQueryParams,
+ );
+ if (matchingMeasurement) {
+ return {
+ measureName: matchingMeasurement.measureName,
+ };
+ }
+
if (this.availableMeasurements.length > 0) {
return {
measureName: this.availableMeasurements[0].measureName,
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 d8792a53a4..72c6816298 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
@@ -67,6 +67,8 @@
}"
[assetContextConfig]="assetContextConfig"
[showActionsMenu]="true"
+ [rowsClickable]="true"
+ (rowClicked)="openDatasetDetails($event.elementId)"
matSort
data-cy="datalake-settings"
>
@@ -92,7 +94,11 @@
<th mat-header-cell mat-sort-header *matHeaderCellDef>
{{ '# Events (7d)' | translate }}
</th>
- <td mat-cell *matCellDef="let configurationEntry">
+ <td
+ mat-cell
+ data-cy="datalake-number-of-events-latest"
+ *matCellDef="let configurationEntry"
+ >
@if (configurationEntry.eventsLatestLoading) {
<mat-spinner
[diameter]="20"
@@ -137,6 +143,7 @@
<sp-label
tone="neutral"
class="cursor-pointer"
+ data-cy="datalake-total-count-button"
minWidth="100px"
[labelText]="
configurationEntry.eventsTotal
@@ -146,7 +153,8 @@
(click)="
receiveTotalMeasurementSize(
configurationEntry
- )
+ );
+ $event.stopPropagation()
"
>
</sp-label>
@@ -163,7 +171,8 @@
(click)="
receiveTotalMeasurementSize(
configurationEntry
- )
+ );
+ $event.stopPropagation()
"
>
</sp-label>
@@ -197,7 +206,8 @@
(click)="
openRetentionDialog(
configurationEntry.elementId
- )
+ );
+ $event.stopPropagation()
"
>
<i
@@ -274,6 +284,14 @@
</ng-container>
<ng-template spTableActions let-element>
+ <button
+ mat-menu-item
+ data-cy="dataset-details-menu-link"
+ (click)="openDatasetDetails(element.elementId)"
+ >
+ <mat-icon>visibility</mat-icon>
+ <span>{{ 'Show' | translate }}</span>
+ </button>
<button
mat-menu-item
data-cy="datalake-download-btn"
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 fcfc0472aa..cf53cd6441 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
@@ -24,6 +24,7 @@ import {
OnInit,
ViewChild,
} from '@angular/core';
+import { Router } from '@angular/router';
import {
MatCell,
MatCellDef,
@@ -148,6 +149,7 @@ export class DatalakeConfigurationComponent
private translateService = inject(TranslateService);
private currentUserService = inject(CurrentUserService);
private assetFilterService = inject(SpAssetBrowserService);
+ private router = inject(Router);
dataSource: MatTableDataSource<DataLakeConfigurationEntry> =
new MatTableDataSource([]);
@@ -392,6 +394,10 @@ export class DatalakeConfigurationComponent
});
}
+ openDatasetDetails(elementId: string): void {
+ this.router.navigate(['datasets', elementId]);
+ }
+
openRetentionDialog(measurementId: string): void {
const dialogRef: DialogRef<DataRetentionDialogComponent> =
this.dialogService.open(DataRetentionDialogComponent, {
diff --git
a/ui/src/app/dataset/components/dataset-details/abstract-dataset-details.directive.ts
b/ui/src/app/dataset/components/dataset-details/abstract-dataset-details.directive.ts
new file mode 100644
index 0000000000..3bea3e3b53
--- /dev/null
+++
b/ui/src/app/dataset/components/dataset-details/abstract-dataset-details.directive.ts
@@ -0,0 +1,69 @@
+/*
+ * 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 { Directive, inject } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import {
+ DataLakeMeasure,
+ DatalakeRestService,
+} from '@streampipes/platform-services';
+import { SpBreadcrumbService, SpNavigationItem } from '@streampipes/shared-ui';
+import { catchError, of } from 'rxjs';
+import { SpDatasetDetailsTabs } from './dataset-details-tabs';
+
+@Directive()
+export abstract class SpAbstractDatasetDetailsDirective {
+ protected activatedRoute = inject(ActivatedRoute);
+ protected datalakeRestService = inject(DatalakeRestService);
+ protected breadcrumbService = inject(SpBreadcrumbService);
+
+ currentDatasetId: string;
+ tabs: SpNavigationItem[] = [];
+ dataset: DataLakeMeasure;
+ datasetNotFound = false;
+
+ onInit(): void {
+ const elementId = this.activatedRoute.snapshot.params.elementId;
+ if (elementId) {
+ this.currentDatasetId = elementId;
+ this.tabs = new SpDatasetDetailsTabs().getTabs(elementId);
+ this.loadDataset();
+ }
+ }
+
+ loadDataset(): void {
+ this.datalakeRestService
+ .getMeasurement(this.currentDatasetId)
+ .pipe(
+ catchError(() => {
+ this.datasetNotFound = true;
+ return of(null);
+ }),
+ )
+ .subscribe(dataset => {
+ if (!dataset) {
+ return;
+ }
+
+ this.dataset = DataLakeMeasure.fromData(dataset);
+ this.onDatasetLoaded();
+ });
+ }
+
+ abstract onDatasetLoaded(): void;
+}
diff --git
a/ui/src/app/dataset/components/dataset-details/dataset-details-events/dataset-details-events.component.html
b/ui/src/app/dataset/components/dataset-details/dataset-details-events/dataset-details-events.component.html
new file mode 100644
index 0000000000..d647754863
--- /dev/null
+++
b/ui/src/app/dataset/components/dataset-details/dataset-details-events/dataset-details-events.component.html
@@ -0,0 +1,191 @@
+<!--
+~ 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.
+~
+-->
+
+<sp-basic-nav-tabs
+ [spNavigationItems]="tabs"
+ [activeLink]="'events'"
+ [showBackLink]="true"
+ [backLinkTarget]="['datasets']"
+>
+ @if (datasetNotFound) {
+ <div class="text-xl" fxFlex="100" fxLayoutAlign="center center">
+ {{ 'The desired dataset was not found!' | translate }}
+ </div>
+ } @else if (dataset) {
+ <div
+ fxLayout="column"
+ fxFlex="100"
+ fxLayoutGap="1rem"
+ class="events-tab-content"
+ >
+ <div fxLayout="row" fxLayoutAlign="start center">
+ <sp-basic-header-title-component
+ [title]="dataset.measureName + ' - Latest events'"
+ ></sp-basic-header-title-component>
+ <span fxFlex></span>
+ <sp-element-id
+ fxLayoutAlign="end start"
+ [value]="dataset.elementId"
+ >
+ </sp-element-id>
+ </div>
+
+ <div fxLayout="row" fxLayoutAlign="end center">
+ <mat-form-field
+ appearance="outline"
+ subscriptSizing="dynamic"
+ class="event-limit-select"
+ >
+ <mat-label>{{ 'Events' | translate }}</mat-label>
+ <input
+ matInput
+ type="number"
+ min="1"
+ max="1000"
+ [(ngModel)]="eventLimit"
+ (change)="onEventLimitChange(eventLimit)"
+ data-cy="dataset-details-event-limit"
+ />
+ </mat-form-field>
+ </div>
+
+ <div fxLayout="column" class="events-preview-section">
+ @if (loadingEvents) {
+ <div
+ fxLayout="row"
+ fxLayoutAlign="center center"
+ class="loading-state"
+ >
+ <mat-spinner [diameter]="32" color="accent">
+ {{ 'Loading' | translate }}
+ </mat-spinner>
+ </div>
+ } @else {
+ <div
+ class="preview-shell"
+ fxLayout="column"
+ data-cy="dataset-details-events-preview"
+ >
+ @if (rows.length > 0 && columns.length > 0) {
+ <div
+ class="preview-table-scroll"
+ data-cy="dataset-details-events-table-scroll"
+ >
+ <table
+ class="preview-table"
+ data-cy="dataset-details-events-table"
+ >
+ <thead>
+ <tr>
+ <th class="index-col">#</th>
+ @for (
+ column of columns;
+ track trackColumn(
+ $index,
+ column
+ )
+ ) {
+ <th
+ [title]="
+ displayColumnName(
+ column
+ )
+ "
+ >
+ {{
+ displayColumnName(
+ column
+ )
+ }}
+ </th>
+ }
+ </tr>
+ </thead>
+ <tbody>
+ @for (
+ row of rows;
+ track trackRow($index)
+ ) {
+ <tr>
+ <td class="index-col">
+ {{ $index + 1 }}
+ </td>
+ @for (
+ column of columns;
+ track trackColumn(
+ $index,
+ column
+ )
+ ) {
+ <td
+ [title]="
+ stringify(
+ row[column]
+ )
+ "
+ [attr.data-cy]="
+
'dataset-details-event-cell-' +
+ column
+ "
+ >
+ {{
+ formatPreviewValue(
+ column,
+ row[column]
+ )
+ }}
+ </td>
+ }
+ </tr>
+ }
+ </tbody>
+ </table>
+ </div>
+ } @else {
+ <div
+ class="preview-empty"
+ fxFlex
+ fxLayout
+ fxLayoutAlign="center center"
+ data-cy="dataset-details-events-empty"
+ >
+ {{ 'No data available.' | translate }}
+ </div>
+ }
+ </div>
+ }
+
+ <div
+ fxLayout="row"
+ fxLayoutAlign="center center"
+ class="create-chart-action"
+ >
+ <button
+ mat-flat-button
+ color="accent"
+ data-cy="dataset-details-create-chart"
+ (click)="navigateToCreateChart()"
+ >
+ <mat-icon>insert_chart</mat-icon>
+ <span>{{ 'Create chart' | translate }}</span>
+ </button>
+ </div>
+ </div>
+ </div>
+ }
+</sp-basic-nav-tabs>
diff --git
a/ui/src/app/dataset/components/dataset-details/dataset-details-events/dataset-details-events.component.scss
b/ui/src/app/dataset/components/dataset-details/dataset-details-events/dataset-details-events.component.scss
new file mode 100644
index 0000000000..16526330a0
--- /dev/null
+++
b/ui/src/app/dataset/components/dataset-details/dataset-details-events/dataset-details-events.component.scss
@@ -0,0 +1,119 @@
+/*!
+ * 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.
+ *
+ */
+
+.event-limit-select {
+ width: 8rem;
+}
+
+.loading-state {
+ min-height: 12rem;
+}
+
+.events-tab-content {
+ min-height: 0;
+}
+
+.events-preview-section {
+ align-items: stretch;
+}
+
+.preview-shell {
+ display: flex;
+ box-sizing: border-box;
+ border-radius: 0;
+ background: var(--color-bg-0);
+ overflow: hidden;
+}
+
+.create-chart-action {
+ padding-top: var(--space-sm);
+ flex: 0 0 auto;
+}
+
+.preview-table-scroll {
+ max-height: calc(100vh - 300px);
+ overflow: auto;
+}
+
+.preview-table {
+ width: max-content;
+ min-width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+ font-size: var(--font-size-xs);
+ line-height: var(--line-height-tight);
+ background: var(--color-bg-0);
+}
+
+.preview-table th,
+.preview-table td {
+ padding: var(--padding-table-cell-compact-y)
+ var(--padding-table-cell-compact-x);
+ text-align: left;
+ border-bottom: 1px solid var(--color-border-subtle);
+ border-right: 1px solid var(--color-border-subtle);
+ white-space: nowrap;
+ max-width: 16rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.preview-table th {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ background: var(--color-bg-0);
+ color: var(--color-default-text);
+ font-weight: var(--font-weight-semibold);
+}
+
+.preview-table tbody tr:nth-child(even) td {
+ background: var(--color-surface-subtle);
+}
+
+.preview-table tbody tr:hover td {
+ background: var(--color-surface-selected-subtle);
+}
+
+.preview-table th.index-col,
+.preview-table td.index-col {
+ position: sticky;
+ left: 0;
+ z-index: 2;
+ min-width: var(--size-table-index-column);
+ max-width: var(--size-table-index-column);
+ text-align: right;
+ color: var(--color-secondary-text);
+ background: var(--color-bg-0);
+}
+
+.preview-table tbody tr:nth-child(even) td.index-col {
+ background: var(--color-surface-subtle);
+}
+
+.preview-table tbody tr:hover td.index-col {
+ background: var(--color-surface-selected-subtle);
+}
+
+.preview-empty {
+ min-height: 12rem;
+ padding: var(--space-md);
+ text-align: center;
+ color: var(--color-secondary-text);
+ font-size: var(--font-size-xs);
+}
diff --git
a/ui/src/app/dataset/components/dataset-details/dataset-details-events/dataset-details-events.component.ts
b/ui/src/app/dataset/components/dataset-details/dataset-details-events/dataset-details-events.component.ts
new file mode 100644
index 0000000000..7c19543d72
--- /dev/null
+++
b/ui/src/app/dataset/components/dataset-details/dataset-details-events/dataset-details-events.component.ts
@@ -0,0 +1,201 @@
+/*
+ * 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 { Component, OnInit, inject } from '@angular/core';
+import { Router } from '@angular/router';
+import { SpQueryResult } from '@streampipes/platform-services';
+import {
+ SpBasicHeaderTitleComponent,
+ SpBasicNavTabsComponent,
+ SpElementIdComponent,
+} from '@streampipes/shared-ui';
+import { SpAbstractDatasetDetailsDirective } from
'../abstract-dataset-details.directive';
+import {
+ FlexDirective,
+ LayoutAlignDirective,
+ LayoutDirective,
+ LayoutGapDirective,
+} from '@ngbracket/ngx-layout/flex';
+import { MatButton } from '@angular/material/button';
+import { MatIcon } from '@angular/material/icon';
+import { MatProgressSpinner } from '@angular/material/progress-spinner';
+import { MatFormField, MatLabel } from '@angular/material/form-field';
+import { MatInput } from '@angular/material/input';
+import { FormsModule } from '@angular/forms';
+import { TranslatePipe } from '@ngx-translate/core';
+import { catchError, finalize, of } from 'rxjs';
+import { SpConfigurationRoutes } from
'../../../../configuration/configuration.breadcrumb';
+import { DatePipe } from '@angular/common';
+
+type PreviewRow = Record<string, unknown>;
+
+@Component({
+ selector: 'sp-dataset-details-events',
+ templateUrl: './dataset-details-events.component.html',
+ styleUrls: ['./dataset-details-events.component.scss'],
+ imports: [
+ SpBasicNavTabsComponent,
+ SpBasicHeaderTitleComponent,
+ SpElementIdComponent,
+ LayoutDirective,
+ LayoutAlignDirective,
+ LayoutGapDirective,
+ FlexDirective,
+ MatButton,
+ MatIcon,
+ MatProgressSpinner,
+ MatFormField,
+ MatLabel,
+ MatInput,
+ FormsModule,
+ TranslatePipe,
+ ],
+})
+export class DatasetDetailsEventsComponent
+ extends SpAbstractDatasetDetailsDirective
+ implements OnInit
+{
+ private router = inject(Router);
+ private datePipe = new DatePipe('en-US');
+
+ eventLimit = 10;
+ columns: string[] = [];
+ rows: PreviewRow[] = [];
+ totalRows = 0;
+ loadingEvents = false;
+
+ ngOnInit(): void {
+ super.onInit();
+ }
+
+ onDatasetLoaded(): void {
+ this.breadcrumbService.updateBreadcrumb([
+ SpConfigurationRoutes.BASE,
+ { label: 'Datasets', link: ['datasets'] },
+ { label: this.dataset.measureName },
+ { label: 'Latest events' },
+ ]);
+ this.loadLatestEvents();
+ }
+
+ loadLatestEvents(): void {
+ if (!this.dataset) {
+ return;
+ }
+
+ this.loadingEvents = true;
+ this.datalakeRestService
+ .getData(this.dataset.measureName, {
+ endDate: new Date().getTime(),
+ startDate: 0,
+ limit: this.eventLimit,
+ order: 'DESC',
+ missingValueBehaviour: 'empty',
+ columns: this.getRuntimeNames().toString(),
+ })
+ .pipe(
+ catchError(() => {
+ return of(new SpQueryResult());
+ }),
+ finalize(() => {
+ this.loadingEvents = false;
+ }),
+ )
+ .subscribe(result => this.applyPreviewResult(result));
+ }
+
+ onEventLimitChange(value: number): void {
+ this.eventLimit = Math.min(Math.max(Number(value) || 1, 1), 1000);
+ this.loadLatestEvents();
+ }
+
+ navigateToCreateChart(): void {
+ this.router.navigate(['chart', 'create'], {
+ queryParams: {
+ editMode: true,
+ measureName: this.dataset.measureName,
+ },
+ });
+ }
+
+ formatPreviewValue(name: string, value: unknown): string {
+ if (name === 'time' && this.isValidDateValue(value)) {
+ return (
+ this.datePipe.transform(
+ value as string | number | Date,
+ 'yyyy-MM-dd HH:mm:ss.SSS',
+ ) ?? this.stringify(value)
+ );
+ }
+
+ return this.stringify(value);
+ }
+
+ stringify(value: unknown): string {
+ if (value === null || value === undefined) {
+ return '-';
+ }
+
+ return String(value);
+ }
+
+ isValidDateValue(value: unknown): boolean {
+ if (value === null || value === undefined || value === '') {
+ return false;
+ }
+
+ const date = new Date(value as string | number);
+ return !Number.isNaN(date.getTime());
+ }
+
+ displayColumnName(column: string): string {
+ return column;
+ }
+
+ trackColumn(_index: number, column: string): string {
+ return column;
+ }
+
+ trackRow(index: number): number {
+ return index;
+ }
+
+ private getRuntimeNames(): string[] {
+ return (this.dataset.eventSchema?.eventProperties ?? []).map(
+ property => property.runtimeName,
+ );
+ }
+
+ private applyPreviewResult(result: SpQueryResult): void {
+ this.columns = this.orderHeaderColumns(result?.headers ?? []);
+ const rows = result?.allDataSeries?.[0]?.rows ?? [];
+ this.totalRows = rows.length;
+ this.rows = rows.map(row =>
+ this.columns.reduce<PreviewRow>((previewRow, column) => {
+ previewRow[column] = row[result.headers.indexOf(column)];
+ return previewRow;
+ }, {}),
+ );
+ }
+
+ private orderHeaderColumns(columns: string[]): string[] {
+ const timeColumns = columns.filter(column => column === 'time');
+ const otherColumns = columns.filter(column => column !== 'time');
+ return [...timeColumns, ...otherColumns];
+ }
+}
diff --git
a/ui/src/app/dataset/components/dataset-details/dataset-details-schema/dataset-details-schema.component.html
b/ui/src/app/dataset/components/dataset-details/dataset-details-schema/dataset-details-schema.component.html
new file mode 100644
index 0000000000..fc1ddca160
--- /dev/null
+++
b/ui/src/app/dataset/components/dataset-details/dataset-details-schema/dataset-details-schema.component.html
@@ -0,0 +1,122 @@
+<!--
+~ 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.
+~
+-->
+
+<sp-basic-nav-tabs
+ [spNavigationItems]="tabs"
+ [activeLink]="'schema'"
+ [showBackLink]="true"
+ [backLinkTarget]="['datasets']"
+>
+ @if (datasetNotFound) {
+ <div class="text-xl" fxFlex="100" fxLayoutAlign="center center">
+ {{ 'The desired dataset was not found!' | translate }}
+ </div>
+ } @else if (dataset) {
+ <div fxLayout="column" fxFlex="100">
+ <div fxLayout="row" fxLayoutAlign="start center">
+ <sp-basic-header-title-component
+ [title]="dataset.measureName + ' - Event schema'"
+ ></sp-basic-header-title-component>
+ <span fxFlex></span>
+ <sp-element-id
+ fxLayoutAlign="end start"
+ [value]="dataset.elementId"
+ >
+ </sp-element-id>
+ </div>
+
+ @if (schemaRows.length > 0) {
+ <div data-cy="dataset-details-schema-table">
+ <table mat-table [dataSource]="schemaRows" class="w-100">
+ <ng-container matColumnDef="runtimeName">
+ <th mat-header-cell *matHeaderCellDef>
+ <strong>{{
+ 'Runtime Name' | translate
+ }}</strong>
+ </th>
+ <td mat-cell *matCellDef="let row">
+ <div class="runtime-name-row">
+ <div
+ class="runtime-name"
+ [attr.data-cy]="
+ 'dataset-details-schema-field-' +
+ row.runtimeName
+ "
+ >
+ {{ row.runtimeName }}
+ </div>
+ <sp-property-scope-badge
+ [propertyScope]="row.propertyScope"
+ />
+ </div>
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="label">
+ <th mat-header-cell *matHeaderCellDef>
+ <strong>{{ 'Field Name' | translate }}</strong>
+ </th>
+ <td mat-cell *matCellDef="let row">
+ {{ row.label }}
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="dataType">
+ <th mat-header-cell *matHeaderCellDef>
+ <strong>{{ 'Data type' | translate }}</strong>
+ </th>
+ <td mat-cell *matCellDef="let row">
+ <div
+ class="value"
+ [attr.data-cy]="
+ 'dataset-details-schema-type-' +
+ row.runtimeName
+ "
+ >
+ {{ row.dataType }}
+ </div>
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="description">
+ <th mat-header-cell *matHeaderCellDef>
+ <strong>{{ 'Description' | translate
}}</strong>
+ </th>
+ <td mat-cell *matCellDef="let row">
+ {{ row.description }}
+ </td>
+ </ng-container>
+
+ <tr
+ mat-header-row
+ *matHeaderRowDef="schemaColumns"
+ ></tr>
+ <tr
+ mat-row
+ *matRowDef="let row; columns: schemaColumns"
+ ></tr>
+ </table>
+ </div>
+ } @else {
+ <div class="empty-state">
+ {{ 'No event schema available.' | translate }}
+ </div>
+ }
+ </div>
+ }
+</sp-basic-nav-tabs>
diff --git a/ui/src/app/dataset/dataset.routes.ts
b/ui/src/app/dataset/components/dataset-details/dataset-details-schema/dataset-details-schema.component.scss
similarity index 60%
copy from ui/src/app/dataset/dataset.routes.ts
copy to
ui/src/app/dataset/components/dataset-details/dataset-details-schema/dataset-details-schema.component.scss
index 1bd06b8b40..32e33a0f81 100644
--- a/ui/src/app/dataset/dataset.routes.ts
+++
b/ui/src/app/dataset/components/dataset-details/dataset-details-schema/dataset-details-schema.component.scss
@@ -16,17 +16,36 @@
*
*/
-import { Routes } from '@angular/router';
-import { DatalakeConfigurationComponent } from
'./components/datalake-configuration/datalake-configuration.component';
+.empty-state {
+ padding: var(--space-md);
+ border: 1px solid var(--color-bg-3);
+ background: var(--color-bg-1);
+}
-export const DATASET_ROUTES: Routes = [
- {
- path: '',
- children: [
- {
- path: '',
- component: DatalakeConfigurationComponent,
- },
- ],
- },
-];
+.runtime-name-row {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-xs);
+}
+
+.runtime-name {
+ font-weight: bold;
+ border-radius: 3px;
+ padding: 3px 5px;
+ background: var(--color-bg-3);
+ display: inline-flex;
+}
+
+.mat-mdc-row:nth-child(even) {
+ background: var(--color-bg-1);
+}
+
+.value {
+ border-radius: 3px;
+ padding: 3px 5px;
+}
+
+td,
+th {
+ white-space: nowrap;
+}
diff --git
a/ui/src/app/dataset/components/dataset-details/dataset-details-schema/dataset-details-schema.component.ts
b/ui/src/app/dataset/components/dataset-details/dataset-details-schema/dataset-details-schema.component.ts
new file mode 100644
index 0000000000..a5df2a76de
--- /dev/null
+++
b/ui/src/app/dataset/components/dataset-details/dataset-details-schema/dataset-details-schema.component.ts
@@ -0,0 +1,134 @@
+/*
+ * 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 { Component, OnInit, inject } from '@angular/core';
+import {
+ PipelineElementSchemaService,
+ PropertyScopeBadgeComponent,
+ SpBasicHeaderTitleComponent,
+ SpBasicNavTabsComponent,
+ SpElementIdComponent,
+} from '@streampipes/shared-ui';
+import { SpAbstractDatasetDetailsDirective } from
'../abstract-dataset-details.directive';
+import {
+ FlexDirective,
+ LayoutAlignDirective,
+ LayoutDirective,
+} from '@ngbracket/ngx-layout/flex';
+import {
+ MatCell,
+ MatCellDef,
+ MatColumnDef,
+ MatHeaderCell,
+ MatHeaderCellDef,
+ MatHeaderRow,
+ MatHeaderRowDef,
+ MatRow,
+ MatRowDef,
+ MatTable,
+} from '@angular/material/table';
+import { TranslatePipe } from '@ngx-translate/core';
+import { SpConfigurationRoutes } from
'../../../../configuration/configuration.breadcrumb';
+
+interface SchemaRow {
+ runtimeName: string;
+ label: string;
+ dataType: string;
+ propertyScope: string;
+ description: string;
+}
+
+@Component({
+ selector: 'sp-dataset-details-schema',
+ templateUrl: './dataset-details-schema.component.html',
+ styleUrls: ['./dataset-details-schema.component.scss'],
+ imports: [
+ SpBasicNavTabsComponent,
+ SpBasicHeaderTitleComponent,
+ SpElementIdComponent,
+ PropertyScopeBadgeComponent,
+ LayoutDirective,
+ LayoutAlignDirective,
+ FlexDirective,
+ MatTable,
+ MatColumnDef,
+ MatHeaderCellDef,
+ MatHeaderCell,
+ MatCellDef,
+ MatCell,
+ MatHeaderRowDef,
+ MatHeaderRow,
+ MatRowDef,
+ MatRow,
+ TranslatePipe,
+ ],
+})
+export class DatasetDetailsSchemaComponent
+ extends SpAbstractDatasetDetailsDirective
+ implements OnInit
+{
+ private pipelineElementSchemaService =
inject(PipelineElementSchemaService);
+
+ schemaRows: SchemaRow[] = [];
+ schemaColumns = ['runtimeName', 'label', 'dataType', 'description'];
+
+ ngOnInit(): void {
+ super.onInit();
+ }
+
+ onDatasetLoaded(): void {
+ this.schemaRows = this.makeSchemaRows();
+ this.breadcrumbService.updateBreadcrumb([
+ SpConfigurationRoutes.BASE,
+ { label: 'Datasets', link: ['datasets'] },
+ { label: this.dataset.measureName },
+ { label: 'Event schema' },
+ ]);
+ }
+
+ private makeSchemaRows(): SchemaRow[] {
+ const eventProperties = this.dataset.eventSchema?.eventProperties ??
[];
+ const schemaRows = eventProperties.map(property => ({
+ runtimeName: property.runtimeName || 'n/a',
+ label: property.label || 'n/a',
+ dataType:
+ this.pipelineElementSchemaService.getFriendlyRuntimeType(
+ property,
+ ),
+ propertyScope: property.propertyScope,
+ description: property.description || 'n/a',
+ }));
+
+ return [this.makeTimestampRow(), ...schemaRows];
+ }
+
+ private makeTimestampRow(): SchemaRow {
+ return {
+ runtimeName: this.getTimestampFieldName(),
+ label: 'n/a',
+ dataType: 'Timestamp',
+ propertyScope: 'HEADER_PROPERTY',
+ description:
+ 'Timestamp field used for storing and querying this dataset',
+ };
+ }
+
+ private getTimestampFieldName(): string {
+ return this.dataset.timestampField?.split('::').pop() || 'time';
+ }
+}
diff --git a/ui/src/app/dataset/dataset.routes.ts
b/ui/src/app/dataset/components/dataset-details/dataset-details-tabs.ts
similarity index 62%
copy from ui/src/app/dataset/dataset.routes.ts
copy to ui/src/app/dataset/components/dataset-details/dataset-details-tabs.ts
index 1bd06b8b40..db4637ac90 100644
--- a/ui/src/app/dataset/dataset.routes.ts
+++ b/ui/src/app/dataset/components/dataset-details/dataset-details-tabs.ts
@@ -16,17 +16,21 @@
*
*/
-import { Routes } from '@angular/router';
-import { DatalakeConfigurationComponent } from
'./components/datalake-configuration/datalake-configuration.component';
+import { SpNavigationItem } from '@streampipes/shared-ui';
-export const DATASET_ROUTES: Routes = [
- {
- path: '',
- children: [
+export class SpDatasetDetailsTabs {
+ public getTabs(elementId: string): SpNavigationItem[] {
+ return [
{
- path: '',
- component: DatalakeConfigurationComponent,
+ itemId: 'schema',
+ itemTitle: 'Event schema',
+ itemLink: ['datasets', elementId, 'schema'],
},
- ],
- },
-];
+ {
+ itemId: 'events',
+ itemTitle: 'Latest events',
+ itemLink: ['datasets', elementId, 'events'],
+ },
+ ];
+ }
+}
diff --git
a/ui/src/app/dataset/components/dataset-feature-card/dataset-feature-card.component.ts
b/ui/src/app/dataset/components/dataset-feature-card/dataset-feature-card.component.ts
index 99faeba446..f14d2f9332 100644
---
a/ui/src/app/dataset/components/dataset-feature-card/dataset-feature-card.component.ts
+++
b/ui/src/app/dataset/components/dataset-feature-card/dataset-feature-card.component.ts
@@ -17,6 +17,7 @@
*/
import { Component, inject, Input, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
import { FlexFillDirective } from '@ngbracket/ngx-layout';
import {
DateFormatService,
@@ -76,6 +77,7 @@ export class DatasetFeatureCardComponent implements OnInit {
private datalakeRestService = inject(DatalakeRestService);
private genericStorageService = inject(GenericStorageService);
private dateFormatService = inject(DateFormatService);
+ private router = inject(Router);
ngOnInit() {
forkJoin([
@@ -162,7 +164,9 @@ export class DatasetFeatureCardComponent implements OnInit {
);
}
- navigateToChartView(): void {}
+ navigateToChartView(): void {
+ this.router.navigate(['datasets', this.resourceId, 'schema']);
+ }
}
interface PreviewRow {
diff --git a/ui/src/app/dataset/dataset.routes.ts
b/ui/src/app/dataset/dataset.routes.ts
index 1bd06b8b40..b1d8417bb0 100644
--- a/ui/src/app/dataset/dataset.routes.ts
+++ b/ui/src/app/dataset/dataset.routes.ts
@@ -18,6 +18,8 @@
import { Routes } from '@angular/router';
import { DatalakeConfigurationComponent } from
'./components/datalake-configuration/datalake-configuration.component';
+import { DatasetDetailsSchemaComponent } from
'./components/dataset-details/dataset-details-schema/dataset-details-schema.component';
+import { DatasetDetailsEventsComponent } from
'./components/dataset-details/dataset-details-events/dataset-details-events.component';
export const DATASET_ROUTES: Routes = [
{
@@ -25,8 +27,27 @@ export const DATASET_ROUTES: Routes = [
children: [
{
path: '',
+ pathMatch: 'full',
component: DatalakeConfigurationComponent,
},
+ {
+ path: ':elementId',
+ children: [
+ {
+ path: '',
+ pathMatch: 'full',
+ redirectTo: 'schema',
+ },
+ {
+ path: 'schema',
+ component: DatasetDetailsSchemaComponent,
+ },
+ {
+ path: 'events',
+ component: DatasetDetailsEventsComponent,
+ },
+ ],
+ },
],
},
];