This is an automated email from the ASF dual-hosted git repository.
SvenO3 pushed a commit to branch 4566-add-details-view-for-data-sets
in repository https://gitbox.apache.org/repos/asf/streampipes.git
The following commit(s) were added to
refs/heads/4566-add-details-view-for-data-sets by this push:
new 241b0d29c9 Add details view for datasets
241b0d29c9 is described below
commit 241b0d29c9ab3b7094e8552e18634f98f49128d4
Author: Sven Oehler <[email protected]>
AuthorDate: Thu Jun 18 17:07:13 2026 +0200
Add details view for datasets
---
.../data-settings/chart-data-settings.component.ts | 15 +-
.../datalake-configuration.component.html | 10 +
.../datalake-configuration.component.ts | 6 +
.../abstract-dataset-details.directive.ts | 69 +++++++
.../dataset-details-events.component.html | 213 +++++++++++++++++++++
.../dataset-details-events.component.scss | 149 ++++++++++++++
.../dataset-details-events.component.ts | 198 +++++++++++++++++++
.../dataset-details-schema.component.html | 110 +++++++++++
.../dataset-details-schema.component.scss} | 22 +--
.../dataset-details-schema.component.ts | 124 ++++++++++++
.../dataset-details/dataset-details-tabs.ts} | 26 +--
.../dataset-feature-card.component.ts | 6 +-
ui/src/app/dataset/dataset.routes.ts | 21 ++
13 files changed, 943 insertions(+), 26 deletions(-)
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..a6fce58ee0 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"
>
@@ -274,6 +276,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..ba8b1522e0
--- /dev/null
+++
b/ui/src/app/dataset/components/dataset-details/dataset-details-events/dataset-details-events.component.html
@@ -0,0 +1,213 @@
+<!--
+~ 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>
+ <mat-select
+ [(ngModel)]="eventLimit"
+ (selectionChange)="loadLatestEvents()"
+ data-cy="dataset-details-event-limit"
+ >
+ @for (option of eventLimitOptions; track option) {
+ <mat-option [value]="option">
+ {{ option }}
+ </mat-option>
+ }
+ </mat-select>
+ </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"
+ >
+ <div
+ class="preview-header"
+ fxLayout="row"
+ fxLayoutAlign="space-between center"
+ >
+ <div class="preview-title" fxFlex>
+ {{ 'Latest events' | translate }}
+ </div>
+ <div
+ class="preview-header-actions"
+ fxLayout="row"
+ fxLayoutAlign="end center"
+ >
+ <div class="preview-meta">
+ {{ totalRows }} {{ 'rows' | translate }}
+ </div>
+ </div>
+ </div>
+
+ @if (rows.length > 0 && columns.length > 0) {
+ <div
+ class="preview-table-scroll"
+ fxFlex
+ 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..cd671cc786
--- /dev/null
+++
b/ui/src/app/dataset/components/dataset-details/dataset-details-events/dataset-details-events.component.scss
@@ -0,0 +1,149 @@
+/*!
+ * 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 {
+ flex: 1 1 auto;
+ min-height: 0;
+}
+
+.preview-shell {
+ display: flex;
+ flex: 1 1 auto;
+ min-height: 0;
+ box-sizing: border-box;
+ border-top: 1px solid var(--color-bg-2);
+ border-radius: 0;
+ background: var(--color-bg-0);
+ overflow: hidden;
+}
+
+.create-chart-action {
+ padding-top: var(--space-sm);
+ flex: 0 0 auto;
+}
+
+.preview-header {
+ gap: var(--space-sm);
+ padding: var(--padding-panel-header-y) var(--padding-panel-header-x);
+ border-bottom: 1px solid var(--color-tab-border);
+ background: var(--color-panel-header-accent);
+ min-height: var(--size-panel-header-compact);
+ box-sizing: border-box;
+}
+
+.preview-title {
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-semibold);
+ line-height: var(--line-height-normal);
+}
+
+.preview-header-actions {
+ gap: var(--space-2xs);
+}
+
+.preview-meta {
+ font-size: var(--font-size-xs);
+ color: var(--color-secondary-text);
+ white-space: nowrap;
+}
+
+.preview-table-scroll {
+ flex: 1 1 auto;
+ min-height: 0;
+ 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..04b394315e
--- /dev/null
+++
b/ui/src/app/dataset/components/dataset-details/dataset-details-events/dataset-details-events.component.ts
@@ -0,0 +1,198 @@
+/*
+ * 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 { MatOption, MatSelect } from '@angular/material/select';
+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,
+ MatSelect,
+ MatOption,
+ FormsModule,
+ TranslatePipe,
+ ],
+})
+export class DatasetDetailsEventsComponent
+ extends SpAbstractDatasetDetailsDirective
+ implements OnInit
+{
+ private router = inject(Router);
+ private datePipe = new DatePipe('en-US');
+
+ eventLimit = 10;
+ eventLimitOptions = [10, 25, 50];
+ 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));
+ }
+
+ 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..45905c5ee7
--- /dev/null
+++
b/ui/src/app/dataset/components/dataset-details/dataset-details-schema/dataset-details-schema.component.html
@@ -0,0 +1,110 @@
+<!--
+~ 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) {
+ <table
+ mat-table
+ [dataSource]="schemaRows"
+ class="w-100"
+ data-cy="dataset-details-schema-table"
+ >
+ <ng-container matColumnDef="runtimeName">
+ <th mat-header-cell *matHeaderCellDef>
+ {{ 'Runtime Name' | translate }}
+ </th>
+ <td mat-cell *matCellDef="let row">
+ {{ row.runtimeName }}
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="label">
+ <th mat-header-cell *matHeaderCellDef>
+ {{ 'Field Name' | translate }}
+ </th>
+ <td mat-cell *matCellDef="let row">
+ {{ row.label }}
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="dataType">
+ <th mat-header-cell *matHeaderCellDef>
+ {{ 'Data type' | translate }}
+ </th>
+ <td mat-cell *matCellDef="let row">
+ {{ row.dataType }}
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="propertyScope">
+ <th mat-header-cell *matHeaderCellDef>
+ {{ 'Property scope' | translate }}
+ </th>
+ <td mat-cell *matCellDef="let row">
+ <sp-property-scope-badge
+ [propertyScope]="row.propertyScope"
+ />
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="description">
+ <th mat-header-cell *matHeaderCellDef>
+ {{ 'Description' | translate }}
+ </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>
+ } @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 67%
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..e76c1e7326 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,13 @@
*
*/
-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,
- },
- ],
- },
-];
+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..4e1f0cacca
--- /dev/null
+++
b/ui/src/app/dataset/components/dataset-details/dataset-details-schema/dataset-details-schema.component.ts
@@ -0,0 +1,124 @@
+/*
+ * 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',
+ 'propertyScope',
+ '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[] {
+ return (this.dataset.eventSchema?.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',
+ }),
+ );
+ }
+}
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,
+ },
+ ],
+ },
],
},
];