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

riemer pushed a commit to branch add-data-preview-table
in repository https://gitbox.apache.org/repos/asf/streampipes.git

commit 603ee55c4b3caea9a79ea3edde9fc97e8a508a8b
Author: Dominik Riemer <[email protected]>
AuthorDate: Fri Feb 27 09:51:00 2026 +0100

    feat: Add data preview
---
 .../chart-container/chart-container.component.ts   |  14 +-
 .../base/base-data-explorer-widget.directive.ts    |   9 +
 .../models/dataview-dashboard.model.ts             |   1 +
 .../chart-view/chart-view.component.html           |  75 +++++---
 .../chart-view/chart-view.component.scss           |  37 ----
 .../components/chart-view/chart-view.component.ts  |   9 +
 .../chart-data-preview.component.html              | 100 ++++++++++
 .../chart-data-preview.component.scss              | 151 +++++++++++++++
 .../chart-data-preview.component.ts                | 204 +++++++++++++++++++++
 ui/src/scss/sp/_variables.scss                     |  33 ++++
 10 files changed, 573 insertions(+), 60 deletions(-)

diff --git 
a/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
 
b/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
index 0c62028bab..a9f4384877 100644
--- 
a/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
+++ 
b/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
@@ -38,6 +38,7 @@ import {
     ExtendedTimeSettings,
     QuickTimeSelection,
     SpLogMessage,
+    SpQueryResult,
     TimeSelectionConstants,
     TimeSettings,
 } from '@streampipes/platform-services';
@@ -152,6 +153,8 @@ export class ChartContainerComponent
     @Output() deleteCallback: EventEmitter<number> = new 
EventEmitter<number>();
     @Output() startEditModeEmitter: EventEmitter<DataExplorerWidgetModel> =
         new EventEmitter<DataExplorerWidgetModel>();
+    @Output() queryResultsEmitter: EventEmitter<SpQueryResult[]> =
+        new EventEmitter<SpQueryResult[]>();
 
     title = '';
     widgetLoaded = false;
@@ -352,7 +355,15 @@ export class ChartContainerComponent
             this.handleTimer(ev),
         );
         const error$ = this.componentRef.instance.errorCallback.subscribe(
-            ev => (this.errorMessage = ev),
+            ev => {
+                this.errorMessage = ev;
+                if (ev) {
+                    this.queryResultsEmitter.emit([]);
+                }
+            },
+        );
+        const data$ = 
this.componentRef.instance.dataReceivedCallback.subscribe(
+            results => this.queryResultsEmitter.emit(results),
         );
 
         this.componentRef.onDestroy(destroy => {
@@ -360,6 +371,7 @@ export class ChartContainerComponent
             remove$?.unsubscribe();
             timer$?.unsubscribe();
             error$?.unsubscribe();
+            data$?.unsubscribe();
         });
     }
 
diff --git 
a/ui/src/app/chart-shared/components/charts/base/base-data-explorer-widget.directive.ts
 
b/ui/src/app/chart-shared/components/charts/base/base-data-explorer-widget.directive.ts
index db21d8b508..a1f862a65b 100644
--- 
a/ui/src/app/chart-shared/components/charts/base/base-data-explorer-widget.directive.ts
+++ 
b/ui/src/app/chart-shared/components/charts/base/base-data-explorer-widget.directive.ts
@@ -70,6 +70,11 @@ export abstract class BaseDataExplorerWidgetDirective<
     errorCallback: EventEmitter<SpLogMessage> =
         new EventEmitter<SpLogMessage>();
 
+    @Output()
+    dataReceivedCallback: EventEmitter<SpQueryResult[]> = new EventEmitter<
+        SpQueryResult[]
+    >();
+
     @Input() editMode: boolean;
     @Input() kioskMode: boolean;
     @Input() dataViewMode: boolean;
@@ -141,6 +146,7 @@ export abstract class BaseDataExplorerWidgetDirective<
                         catchError(err => {
                             this.timerCallback.emit(false);
                             this.errorCallback.emit(err.error);
+                            this.dataReceivedCallback.emit([]);
                             return [];
                         }),
                     );
@@ -271,11 +277,14 @@ export abstract class BaseDataExplorerWidgetDirective<
         const spQueryResult = spQueryResults[0];
 
         if (spQueryResult.total === 0) {
+            this.dataReceivedCallback.emit([]);
             this.setShownComponents(true, false, false, false);
         } else if (spQueryResult['spQueryStatus'] === 'TOO_MUCH_DATA') {
             this.amountOfTooMuchEvents = spQueryResult.total;
+            this.dataReceivedCallback.emit([]);
             this.setShownComponents(false, false, false, true);
         } else {
+            this.dataReceivedCallback.emit(spQueryResults);
             this.onDataReceived(spQueryResults);
         }
     }
diff --git a/ui/src/app/chart-shared/models/dataview-dashboard.model.ts 
b/ui/src/app/chart-shared/models/dataview-dashboard.model.ts
index e792765b6f..3f6e310754 100644
--- a/ui/src/app/chart-shared/models/dataview-dashboard.model.ts
+++ b/ui/src/app/chart-shared/models/dataview-dashboard.model.ts
@@ -36,6 +36,7 @@ export interface BaseWidgetData<T extends 
DataExplorerWidgetModel> {
     removeWidgetCallback: EventEmitter<boolean>;
     timerCallback: EventEmitter<boolean>;
     errorCallback: EventEmitter<SpLogMessage>;
+    dataReceivedCallback: EventEmitter<SpQueryResult[]>;
 
     editMode: boolean;
     kioskMode: boolean;
diff --git a/ui/src/app/chart/components/chart-view/chart-view.component.html 
b/ui/src/app/chart/components/chart-view/chart-view.component.html
index d34c1dab33..cdfb4ae0af 100644
--- a/ui/src/app/chart/components/chart-view/chart-view.component.html
+++ b/ui/src/app/chart/components/chart-view/chart-view.component.html
@@ -45,7 +45,10 @@
                 </div>
             } @else {
                 <mat-drawer-container
-                    class="designer-panel-container h-100 dashboard-grid"
+                    fxFlex="100"
+                    fxLayout="column"
+                    class="h-100"
+                    style="width: 100%; min-height: 0"
                 >
                     <mat-drawer
                         [opened]="editMode"
@@ -70,27 +73,55 @@
                             </div>
                         </sp-sidebar-resize>
                     </mat-drawer>
-                    <mat-drawer-content class="h-100 dashboard-grid">
-                        <div #panel fxFlex="100" fxLayout="column">
-                            @if (
-                                dataView &&
-                                dataView.dataConfig?.sourceConfigs?.length > 0
-                            ) {
-                                <sp-chart-container
-                                    fxFlex
-                                    [dataViewMode]="true"
-                                    [editMode]="editMode"
-                                    [configuredWidget]="dataView"
-                                    [timeSettings]="timeSettings"
-                                    [observableGenerator]="observableGenerator"
-                                    [dataLakeMeasure]="
-                                        dataView.dataConfig.sourceConfigs[0]
-                                            .measure
-                                    "
-                                    (startEditModeEmitter)="editDataView()"
-                                >
-                                </sp-chart-container>
-                            }
+                    <mat-drawer-content
+                        class="h-100"
+                        fxFlex
+                        fxLayout="column"
+                        style="min-height: 0"
+                    >
+                        <div
+                            fxFlex
+                            fxLayout="column"
+                            style="min-height: 0; overflow: hidden"
+                        >
+                            <div
+                                #panel
+                                fxLayout="column"
+                                fxFlex
+                                style="min-height: 0; overflow: hidden"
+                            >
+                                @if (
+                                    dataView &&
+                                    dataView.dataConfig?.sourceConfigs?.length 
>
+                                        0
+                                ) {
+                                    <sp-chart-container
+                                        fxFlex
+                                        style="display: block; min-height: 0"
+                                        [dataViewMode]="true"
+                                        [editMode]="editMode"
+                                        [configuredWidget]="dataView"
+                                        [timeSettings]="timeSettings"
+                                        [observableGenerator]="
+                                            observableGenerator
+                                        "
+                                        [dataLakeMeasure]="
+                                            
dataView.dataConfig.sourceConfigs[0]
+                                                .measure
+                                        "
+                                        (startEditModeEmitter)="editDataView()"
+                                        (queryResultsEmitter)="
+                                            onQueryResultsChanged($event)
+                                        "
+                                    >
+                                    </sp-chart-container>
+                                }
+                            </div>
+
+                            <sp-chart-data-preview
+                                [queryResults]="latestQueryResults"
+                            >
+                            </sp-chart-data-preview>
                         </div>
                     </mat-drawer-content>
                 </mat-drawer-container>
diff --git a/ui/src/app/chart/components/chart-view/chart-view.component.scss 
b/ui/src/app/chart/components/chart-view/chart-view.component.scss
index b286235804..22e68622e7 100644
--- a/ui/src/app/chart/components/chart-view/chart-view.component.scss
+++ b/ui/src/app/chart/components/chart-view/chart-view.component.scss
@@ -16,38 +16,6 @@
  *
  */
 
-.fixed-height {
-    height: 50px;
-}
-
-.data-explorer-options {
-    padding: 0px;
-}
-
-.data-explorer-options-item {
-    display: inline;
-    margin-right: 10px;
-}
-
-.m-20 {
-    margin: 20px;
-}
-
-.h-100 {
-    height: 100%;
-}
-
-.dashboard-grid {
-    display: flex;
-    flex-direction: column;
-    flex: 1 1 100%;
-}
-
-.designer-panel-container {
-    width: 100%;
-    height: 100%;
-}
-
 .designer-panel {
     border: 1px solid var(--color-tab-border);
     width: auto;
@@ -55,8 +23,3 @@
     display: inline-block;
     overflow-x: hidden;
 }
-
-.widget-title-text {
-    white-space: nowrap;
-    margin-right: 12px;
-}
diff --git a/ui/src/app/chart/components/chart-view/chart-view.component.ts 
b/ui/src/app/chart/components/chart-view/chart-view.component.ts
index 43a3461383..26df223397 100644
--- a/ui/src/app/chart/components/chart-view/chart-view.component.ts
+++ b/ui/src/app/chart/components/chart-view/chart-view.component.ts
@@ -31,6 +31,7 @@ import {
     EventPropertyUnion,
     FieldConfig,
     LinkageData,
+    SpQueryResult,
     TimeSettings,
 } from '@streampipes/platform-services';
 import {
@@ -76,6 +77,7 @@ import {
 } from '@angular/material/sidenav';
 import { ChartDesignerPanelComponent } from 
'./designer-panel/chart-designer-panel.component';
 import { ChartContainerComponent } from 
'../../../chart-shared/components/chart-container/chart-container.component';
+import { ChartDataPreviewComponent } from 
'./query-result-preview/chart-data-preview.component';
 
 @Component({
     selector: 'sp-chart-data-view',
@@ -93,6 +95,7 @@ import { ChartContainerComponent } from 
'../../../chart-shared/components/chart-
         ChartDesignerPanelComponent,
         MatDrawerContent,
         ChartContainerComponent,
+        ChartDataPreviewComponent,
         TranslatePipe,
     ],
 })
@@ -132,6 +135,7 @@ export class ChartViewComponent
     currentUser$: Subscription;
 
     chartNotFound = false;
+    latestQueryResults: SpQueryResult[] = [];
 
     observableGenerator =
         this.dataExplorerSharedService.defaultObservableGenerator();
@@ -204,6 +208,7 @@ export class ChartViewComponent
 
     loadDataView(dataViewId: string): void {
         this.dataViewLoaded = false;
+        this.latestQueryResults = [];
         this.dataViewService
             .getChart(dataViewId)
             .pipe(
@@ -242,6 +247,10 @@ export class ChartViewComponent
         this.routingService.navigateToChart(true, this.dataView.elementId);
     }
 
+    onQueryResultsChanged(results: SpQueryResult[]): void {
+        this.latestQueryResults = results ?? [];
+    }
+
     makeDefaultTimeSettings(): TimeSettings {
         return this.timeSelectionService.getDefaultTimeSettings();
     }
diff --git 
a/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.html
 
b/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.html
new file mode 100644
index 0000000000..9d9a0f965a
--- /dev/null
+++ 
b/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.html
@@ -0,0 +1,100 @@
+<!--
+~ 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.
+~
+-->
+
+<div class="preview-shell" fxLayout="column">
+    <div
+        class="preview-header"
+        fxLayout="row"
+        fxLayoutAlign="space-between center"
+    >
+        <div class="preview-title" fxFlex>
+            {{ 'Data preview' | translate }}
+        </div>
+        <div
+            class="preview-header-actions"
+            fxLayout="row"
+            fxLayoutAlign="end center"
+        >
+            <div class="preview-meta">
+                {{ totalRows }} {{ 'rows' | translate }}
+                @if (!allRowsRendered) {
+                    <span class="preview-meta-note">
+                        ({{ 'showing first' | translate }} {{ rows.length }})
+                    </span>
+                }
+            </div>
+            <button
+                mat-icon-button
+                class="preview-toggle"
+                (click)="toggleExpanded()"
+                [attr.aria-label]="
+                    (expanded ? 'Collapse preview' : 'Expand preview')
+                        | translate
+                "
+            >
+                <mat-icon>{{
+                    expanded ? 'expand_more' : 'expand_less'
+                }}</mat-icon>
+            </button>
+        </div>
+    </div>
+
+    @if (expanded && rows.length > 0 && columns.length > 0) {
+        <div class="preview-table-scroll" fxFlex>
+            <table class="preview-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])">
+                                    {{ formatCellValue(column, row[column]) }}
+                                </td>
+                            }
+                        </tr>
+                    }
+                </tbody>
+            </table>
+        </div>
+    } @else if (expanded) {
+        <div
+            class="preview-empty"
+            fxFlex
+            fxLayout
+            fxLayoutAlign="center center"
+        >
+            {{ 'Run or adjust the query to see a row preview.' | translate }}
+        </div>
+    }
+</div>
diff --git 
a/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.scss
 
b/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.scss
new file mode 100644
index 0000000000..a8449a88b3
--- /dev/null
+++ 
b/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.scss
@@ -0,0 +1,151 @@
+/*
+ * 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.
+ *
+ */
+
+:host {
+    display: block;
+    min-height: 0;
+    height: var(--size-data-preview-collapsed);
+    transition: height 160ms ease;
+    color: var(--color-default-text);
+}
+
+.preview-shell {
+    height: 100%;
+    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;
+}
+
+.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-meta-note {
+    margin-left: var(--space-2xs);
+}
+
+.preview-toggle {
+    width: var(--size-icon-button-compact);
+    height: var(--size-icon-button-compact);
+    line-height: var(--size-icon-button-compact);
+    color: var(--color-default-text);
+}
+
+.preview-toggle .mat-icon {
+    font-size: var(--size-icon-compact);
+    width: var(--size-icon-compact);
+    height: var(--size-icon-compact);
+    line-height: var(--size-icon-compact);
+}
+
+.preview-table-scroll {
+    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: 0;
+    padding: var(--space-md);
+    text-align: center;
+    color: var(--color-secondary-text);
+    font-size: var(--font-size-xs);
+}
diff --git 
a/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.ts
 
b/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.ts
new file mode 100644
index 0000000000..68c827acc5
--- /dev/null
+++ 
b/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.ts
@@ -0,0 +1,204 @@
+/*
+ * 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 { DatePipe } from '@angular/common';
+import {
+    ChangeDetectionStrategy,
+    Component,
+    HostBinding,
+    Input,
+    OnChanges,
+    SimpleChanges,
+} from '@angular/core';
+import { MatIcon } from '@angular/material/icon';
+import { MatIconButton } from '@angular/material/button';
+import {
+    FlexDirective,
+    LayoutAlignDirective,
+    LayoutDirective,
+} from '@ngbracket/ngx-layout/flex';
+import { SpQueryResult } from '@streampipes/platform-services';
+import { TranslatePipe } from '@ngx-translate/core';
+
+type PreviewRow = Record<string, unknown>;
+
+@Component({
+    selector: 'sp-chart-data-preview',
+    templateUrl: './chart-data-preview.component.html',
+    styleUrls: ['./chart-data-preview.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+    imports: [
+        TranslatePipe,
+        MatIconButton,
+        MatIcon,
+        LayoutDirective,
+        LayoutAlignDirective,
+        FlexDirective,
+    ],
+})
+export class ChartDataPreviewComponent implements OnChanges {
+    private static readonly MAX_PREVIEW_ROWS = 2000;
+    private readonly datePipe = new DatePipe('en-US');
+
+    @Input() queryResults: SpQueryResult[] = [];
+    @Input() defaultExpanded = false;
+
+    columns: string[] = [];
+    rows: PreviewRow[] = [];
+    totalRows = 0;
+    allRowsRendered = true;
+    expanded = false;
+
+    @HostBinding('style.height')
+    get hostHeight(): string {
+        return this.expanded
+            ? 'var(--size-data-preview-expanded)'
+            : 'var(--size-data-preview-collapsed)';
+    }
+
+    @HostBinding('class.expanded')
+    get hostExpandedClass(): boolean {
+        return this.expanded;
+    }
+
+    ngOnChanges(changes: SimpleChanges): void {
+        if (changes.defaultExpanded) {
+            this.expanded = !!this.defaultExpanded;
+        }
+        if (changes.queryResults) {
+            this.rebuildPreview(this.queryResults ?? []);
+        }
+    }
+
+    private rebuildPreview(queryResults: SpQueryResult[]): void {
+        const headerColumns: string[] = [];
+        const tagColumns: string[] = [];
+        const flattenedRows: PreviewRow[] = [];
+        const showSourceColumn = queryResults.length > 1;
+
+        this.totalRows = 0;
+
+        queryResults.forEach(queryResult => {
+            queryResult.headers?.forEach(header =>
+                this.pushUnique(headerColumns, header),
+            );
+
+            queryResult.allDataSeries?.forEach(series => {
+                const tags = series.tags ?? {};
+                Object.keys(tags).forEach(tagKey =>
+                    this.pushUnique(tagColumns, tagKey),
+                );
+
+                series.rows?.forEach(values => {
+                    this.totalRows += 1;
+                    if (
+                        flattenedRows.length >=
+                        ChartDataPreviewComponent.MAX_PREVIEW_ROWS
+                    ) {
+                        return;
+                    }
+
+                    const row: PreviewRow = {};
+                    queryResult.headers?.forEach((header, index) => {
+                        row[header] = values?.[index];
+                    });
+
+                    Object.entries(tags).forEach(([key, value]) => {
+                        row[key] = value;
+                    });
+
+                    if (showSourceColumn) {
+                        row['_source'] =
+                            queryResult.forId || queryResult.sourceIndex;
+                    }
+
+                    flattenedRows.push(row);
+                });
+            });
+        });
+
+        const orderedHeaderColumns = this.orderHeaderColumns(headerColumns);
+        this.columns = showSourceColumn
+            ? ['_source', ...orderedHeaderColumns, ...tagColumns]
+            : [...orderedHeaderColumns, ...tagColumns];
+        this.rows = flattenedRows;
+        this.allRowsRendered =
+            this.totalRows <= ChartDataPreviewComponent.MAX_PREVIEW_ROWS;
+    }
+
+    private orderHeaderColumns(columns: string[]): string[] {
+        const timeColumns = columns.filter(column => column === 'time');
+        const otherColumns = columns.filter(column => column !== 'time');
+        return [...timeColumns, ...otherColumns];
+    }
+
+    private pushUnique(collection: string[], value: string): void {
+        if (value !== undefined && !collection.includes(value)) {
+            collection.push(value);
+        }
+    }
+
+    trackColumn(_index: number, column: string): string {
+        return column;
+    }
+
+    trackRow(index: number): number {
+        return index;
+    }
+
+    isTimeColumn(column: string): boolean {
+        return column === 'time';
+    }
+
+    displayColumnName(column: string): string {
+        return column === '_source' ? 'Source' : column;
+    }
+
+    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());
+    }
+
+    stringify(value: unknown): string {
+        if (value === null || value === undefined) {
+            return '-';
+        }
+        return String(value);
+    }
+
+    formatCellValue(column: string, value: unknown): string {
+        if (this.isTimeColumn(column) && 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);
+    }
+
+    toggleExpanded(): void {
+        this.expanded = !this.expanded;
+    }
+}
diff --git a/ui/src/scss/sp/_variables.scss b/ui/src/scss/sp/_variables.scss
index d95fc84247..7900e374dd 100644
--- a/ui/src/scss/sp/_variables.scss
+++ b/ui/src/scss/sp/_variables.scss
@@ -100,6 +100,39 @@
     --mat-sidenav-container-divider-color: var(--color-bg-3);
     --mat-button-toggle-height: 30px;
     --mat-table-header-headline-color: var(--mat-sys-on-surface);
+
+    --size-panel-header-compact: calc(var(--space-md) * 3.4);
+    --size-icon-button-compact: calc(var(--space-md) * 2.2);
+    --size-icon-compact: calc(var(--space-md) * 1.4);
+    --size-table-index-column: calc(var(--space-md) * 4);
+    --size-data-preview-collapsed: calc(var(--size-panel-header-compact) + 
2px);
+    --size-data-preview-expanded: 16.25rem;
+
+    --padding-table-cell-compact-y: var(--space-xs);
+    --padding-table-cell-compact-x: var(--space-sm);
+    --padding-panel-header-y: var(--space-sm);
+    --padding-panel-header-x: var(--space-md);
+
+    --color-surface-subtle: color-mix(
+        in srgb,
+        var(--color-bg-2) 55%,
+        var(--color-bg-0)
+    );
+    --color-surface-selected-subtle: color-mix(
+        in srgb,
+        var(--color-primary) 10%,
+        var(--color-bg-0)
+    );
+    --color-border-subtle: color-mix(
+        in srgb,
+        var(--color-bg-3) 72%,
+        transparent
+    );
+    --color-panel-header-accent: color-mix(
+        in srgb,
+        var(--color-primary) 5%,
+        var(--color-bg-0)
+    );
 }
 
 .dark-mode {

Reply via email to