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

riemer 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 c7fdf77ce7 feat: Add data preview table (#4207)
c7fdf77ce7 is described below

commit c7fdf77ce7248b9b7a8c94396ae06690fa3957b8
Author: Dominik Riemer <[email protected]>
AuthorDate: Sun Mar 1 09:38:58 2026 +0100

    feat: Add data preview table (#4207)
---
 .../support/utils/dataExplorer/DataExplorerBtns.ts |  24 +
 .../dataExplorer/chartDataPreview.smoke.spec.ts    |  48 ++
 .../chart-container/chart-container.component.ts   |  14 +-
 .../base/base-data-explorer-widget.directive.ts    |   9 +
 .../config/table-widget-config.component.html      |  93 +++-
 .../config/table-widget-config.component.scss      |  35 ++
 .../table/config/table-widget-config.component.ts  | 152 +++++-
 .../charts/table/model/table-widget.model.ts       |   3 +
 .../charts/table/table-widget.component.html       | 223 +++++----
 .../charts/table/table-widget.component.scss       | 109 ++++-
 .../charts/table/table-widget.component.ts         | 527 ++++++++++++++++-----
 .../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 +
 .../data-settings/chart-data-settings.component.ts |   3 +
 .../chart-data-preview.component.html              | 111 +++++
 .../chart-data-preview.component.scss              | 137 ++++++
 .../chart-data-preview.component.ts                | 204 ++++++++
 ui/src/scss/sp/_variables.scss                     |  33 ++
 ui/src/scss/sp/forms.scss                          | 126 +++--
 21 files changed, 1613 insertions(+), 360 deletions(-)

diff --git a/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts 
b/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts
index ee99646fe0..741f17d019 100644
--- a/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts
+++ b/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts
@@ -126,6 +126,30 @@ export class DataExplorerBtns {
         return cy.dataCy('save-data-explorer-go-back-to-overview');
     }
 
+    public static chartDataPreview() {
+        return cy.dataCy('chart-data-preview');
+    }
+
+    public static chartDataPreviewHeader() {
+        return cy.dataCy('chart-data-preview-header');
+    }
+
+    public static chartDataPreviewToggle() {
+        return cy.dataCy('chart-data-preview-toggle');
+    }
+
+    public static chartDataPreviewTable() {
+        return cy.dataCy('chart-data-preview-table');
+    }
+
+    public static chartDataPreviewCell(columnName: string) {
+        return cy.dataCy(`chart-data-preview-cell-${columnName}`);
+    }
+
+    public static chartDataPreviewEmpty() {
+        return cy.dataCy('chart-data-preview-empty');
+    }
+
     public static addNewWidgetBtn() {
         return cy.dataCy('add-new-widget');
     }
diff --git a/ui/cypress/tests/dataExplorer/chartDataPreview.smoke.spec.ts 
b/ui/cypress/tests/dataExplorer/chartDataPreview.smoke.spec.ts
new file mode 100644
index 0000000000..9d95809e77
--- /dev/null
+++ b/ui/cypress/tests/dataExplorer/chartDataPreview.smoke.spec.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { DataExplorerWidget } from '../../support/model/DataExplorerWidget';
+import { PrepareTestDataUtils } from 
'../../support/utils/PrepareTestDataUtils';
+import { DataExplorerBtns } from 
'../../support/utils/dataExplorer/DataExplorerBtns';
+import { DataExplorerUtils } from 
'../../support/utils/dataExplorer/DataExplorerUtils';
+
+describe('Test Chart Data Preview in Data Explorer', () => {
+    beforeEach('Setup Test', () => {
+        DataExplorerUtils.initDataLakeTests();
+    });
+
+    it('Shows and toggles the chart data preview', () => {
+        DataExplorerUtils.addDataViewAndWidget(
+            'preview-view',
+            PrepareTestDataUtils.dataName,
+            DataExplorerWidget.TIME_SERIES,
+        );
+
+        DataExplorerBtns.chartDataPreviewHeader().should('be.visible');
+        DataExplorerBtns.chartDataPreviewTable().should('not.exist');
+
+        DataExplorerBtns.chartDataPreviewToggle().click();
+
+        DataExplorerBtns.chartDataPreviewTable().should('be.visible');
+        DataExplorerBtns.chartDataPreviewCell('time')
+            .should('exist')
+            .and('have.length.at.least', 1);
+
+        DataExplorerBtns.chartDataPreviewToggle().click();
+        DataExplorerBtns.chartDataPreviewTable().should('not.exist');
+    });
+});
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/components/charts/table/config/table-widget-config.component.html
 
b/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.html
index 41fe5fd66d..abed075a07 100644
--- 
a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.html
+++ 
b/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.html
@@ -31,20 +31,83 @@
         "
     >
     </sp-select-properties-config>
-    <sp-split-section
-        [level]="3"
-        [title]="'Search' | translate"
-        marginTop="5px"
-    >
-        <mat-form-field appearance="outline" fxFlex="100" color="accent">
-            <mat-label>{{ 'Filter' | translate }}</mat-label>
-            <input
-                [(ngModel)]="
-                    currentlyConfiguredWidget.visualizationConfig.searchValue
-                "
-                matInput
-                (input)="onFilterChange($event.target.value)"
-            />
-        </mat-form-field>
+    <sp-split-section [level]="3" [title]="'Settings' | translate">
+        <sp-form-field [level]="3" [label]="'Filter' | translate">
+            <mat-form-field fxFlex="100">
+                <input
+                    [(ngModel)]="
+                        currentlyConfiguredWidget.visualizationConfig
+                            .searchValue
+                    "
+                    matInput
+                    (input)="onFilterChange($event.target.value)"
+                />
+            </mat-form-field>
+        </sp-form-field>
+        <sp-form-field [level]="3" [label]="'Page size' | translate">
+            <mat-form-field fxFlex="100">
+                <mat-select
+                    [value]="
+                        currentlyConfiguredWidget.visualizationConfig.pageSize
+                    "
+                    (valueChange)="setPageSize($event)"
+                >
+                    @for (pageSize of pageSizeOptions; track pageSize) {
+                        <mat-option [value]="pageSize">
+                            {{ pageSize }}
+                        </mat-option>
+                    }
+                </mat-select>
+            </mat-form-field>
+        </sp-form-field>
+    </sp-split-section>
+    <sp-split-section [level]="3" [title]="'Value Highlighting' | translate">
+        <div fxLayout="column" class="highlight-color-list">
+            @for (field of highlightableFields; track colorKey(field)) {
+                <div
+                    class="highlight-color-row"
+                    fxLayout="row"
+                    fxLayoutAlign="space-between center"
+                >
+                    <mat-checkbox
+                        [checked]="isHighlighted(field)"
+                        (change)="toggleHighlightedField(field)"
+                    >
+                        <div class="highlight-color-label">
+                            <b>{{ field.runtimeName }}</b>
+                            <span class="highlight-color-measure">
+                                {{
+                                    '(' +
+                                        field.measure +
+                                        ', ' +
+                                        (fieldTypeLabel(field) | translate) +
+                                        ')'
+                                }}
+                            </span>
+                        </div>
+                    </mat-checkbox>
+                    <input
+                        class="highlight-color-picker"
+                        [colorPicker]="getHighlightColor(field)"
+                        [style.background]="getHighlightColor(field)"
+                        [cpPosition]="'auto'"
+                        [cpPresetColors]="presetColors"
+                        (colorPickerChange)="setHighlightColor(field, $event)"
+                        [disabled]="!isHighlighted(field)"
+                        [attr.aria-label]="
+                            'Select highlight color for ' + field.runtimeName
+                                | translate
+                        "
+                    />
+                </div>
+            } @empty {
+                <sp-alert-banner
+                    type="info"
+                    [title]="
+                        'No numeric or boolean fields available.' | translate
+                    "
+                ></sp-alert-banner>
+            }
+        </div>
     </sp-split-section>
 </sp-visualization-config-outer>
diff --git 
a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.scss
 
b/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.scss
index 13cbc4aacb..2bc4d58126 100644
--- 
a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.scss
+++ 
b/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.scss
@@ -15,3 +15,38 @@
  * limitations under the License.
  *
  */
+
+.highlight-color-list {
+    gap: var(--space-sm);
+}
+
+.highlight-color-label {
+    display: flex;
+    flex-direction: column;
+    min-width: 0;
+    font-size: var(--font-size-sm);
+}
+
+.highlight-color-measure {
+    color: var(--color-secondary-text);
+    font-size: var(--font-size-xs);
+}
+
+.highlight-color-picker {
+    margin-left: auto;
+    display: block;
+    width: calc(var(--space-md) * 2.4);
+    height: calc(var(--space-md) * 2.4);
+    padding: 0;
+    border: 1px solid var(--color-tab-border);
+    border-radius: var(--button-border-radius);
+    cursor: pointer;
+    appearance: none;
+    -webkit-appearance: none;
+    background-clip: padding-box;
+}
+
+.highlight-color-picker:disabled {
+    cursor: default;
+    opacity: 0.45;
+}
diff --git 
a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.ts
 
b/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.ts
index 2a44320c29..d374a7fc31 100644
--- 
a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.ts
+++ 
b/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.ts
@@ -24,12 +24,23 @@ import { ChartFieldProviderService } from 
'../../../../services/chart-field-prov
 import { DataExplorerField } from '@streampipes/platform-services';
 import { SpVisualizationConfigOuterComponent } from 
'../../../chart-config/visualization-config-outer/visualization-config-outer.component';
 import { SelectMultiplePropertiesConfigComponent } from 
'../../../chart-config/select-multiple-properties-config/select-multiple-properties-config.component';
-import { SplitSectionComponent } from '@streampipes/shared-ui';
+import {
+    FormFieldComponent,
+    SpAlertBannerComponent,
+    SplitSectionComponent,
+} from '@streampipes/shared-ui';
 import { MatFormField, MatLabel } from '@angular/material/form-field';
-import { FlexDirective } from '@ngbracket/ngx-layout/flex';
+import {
+    FlexDirective,
+    LayoutAlignDirective,
+    LayoutDirective,
+} from '@ngbracket/ngx-layout/flex';
 import { MatInput } from '@angular/material/input';
 import { FormsModule } from '@angular/forms';
 import { TranslatePipe } from '@ngx-translate/core';
+import { MatOption, MatSelect } from '@angular/material/select';
+import { ColorPickerDirective } from 'ngx-color-picker';
+import { MatCheckbox } from '@angular/material/checkbox';
 
 @Component({
     selector: 'sp-data-explorer-table-widget-config',
@@ -44,13 +55,32 @@ import { TranslatePipe } from '@ngx-translate/core';
         MatLabel,
         MatInput,
         FormsModule,
+        MatSelect,
+        MatOption,
+        ColorPickerDirective,
+        LayoutDirective,
+        LayoutAlignDirective,
+        MatCheckbox,
         TranslatePipe,
+        FormFieldComponent,
+        SpAlertBannerComponent,
     ],
 })
 export class TableWidgetConfigComponent extends BaseWidgetConfig<
     TableWidgetModel,
     TableVisConfig
 > {
+    readonly pageSizeOptions = [10, 20, 50, 100, 250, 500];
+    readonly presetColors = [
+        '#39B54A',
+        '#1B1464',
+        '#2563EB',
+        '#F59E0B',
+        '#DC2626',
+        '#14B8A6',
+        '#9333EA',
+    ];
+
     constructor(
         widgetConfigurationService: ChartConfigurationService,
         fieldService: ChartFieldProviderService,
@@ -70,6 +100,79 @@ export class TableWidgetConfigComponent extends 
BaseWidgetConfig<
         this.triggerViewRefresh();
     }
 
+    setHighlightedColumns(highlightedColumns: DataExplorerField[]) {
+        this.currentlyConfiguredWidget.visualizationConfig.highlightedColumns =
+            highlightedColumns;
+        this.syncHighlightColorMap();
+        this.triggerViewRefresh();
+    }
+
+    setPageSize(pageSize: number): void {
+        this.currentlyConfiguredWidget.visualizationConfig.pageSize = pageSize;
+        this.triggerViewRefresh();
+    }
+
+    colorKey(field: DataExplorerField): string {
+        return `${field.fullDbName}:${field.sourceIndex}`;
+    }
+
+    getHighlightColor(field: DataExplorerField): string {
+        return (
+            this.currentlyConfiguredWidget.visualizationConfig
+                .highlightedColumnColors?.[this.colorKey(field)] ??
+            this.defaultHighlightColor(field)
+        );
+    }
+
+    setHighlightColor(field: DataExplorerField, color: string): void {
+        
this.currentlyConfiguredWidget.visualizationConfig.highlightedColumnColors[
+            this.colorKey(field)
+        ] = color;
+        this.triggerViewRefresh();
+    }
+
+    get highlightableFields(): DataExplorerField[] {
+        return this.fieldProvider.allFields.filter(
+            field =>
+                field.fieldCharacteristics.numeric ||
+                field.fieldCharacteristics.binary,
+        );
+    }
+
+    isHighlighted(field: DataExplorerField): boolean {
+        return !!(
+            this.currentlyConfiguredWidget.visualizationConfig
+                .highlightedColumns ?? []
+        ).find(
+            highlightedField =>
+                highlightedField.fullDbName === field.fullDbName &&
+                highlightedField.sourceIndex === field.sourceIndex,
+        );
+    }
+
+    toggleHighlightedField(field: DataExplorerField): void {
+        const highlightedColumns =
+            this.currentlyConfiguredWidget.visualizationConfig
+                .highlightedColumns ?? [];
+
+        if (this.isHighlighted(field)) {
+            
this.currentlyConfiguredWidget.visualizationConfig.highlightedColumns =
+                highlightedColumns.filter(
+                    highlightedField =>
+                        !(
+                            highlightedField.fullDbName === field.fullDbName &&
+                            highlightedField.sourceIndex === field.sourceIndex
+                        ),
+                );
+        } else {
+            
this.currentlyConfiguredWidget.visualizationConfig.highlightedColumns =
+                [...highlightedColumns, field];
+        }
+
+        this.syncHighlightColorMap();
+        this.triggerViewRefresh();
+    }
+
     protected applyWidgetConfig(config: TableVisConfig): void {
         config.selectedColumns = this.fieldService.getSelectedFields(
             config.selectedColumns,
@@ -80,10 +183,55 @@ export class TableWidgetConfigComponent extends 
BaseWidgetConfig<
                     : this.fieldProvider.allFields;
             },
         );
+        config.highlightedColumns = this.fieldService.getSelectedFields(
+            config.highlightedColumns ?? [],
+            this.highlightableFields,
+            () => [],
+        );
+        config.highlightedColumnColors ??= {};
+        this.syncHighlightColorMap();
+        config.pageSize ??= 20;
         config.searchValue ??= '';
     }
 
     protected requiredFieldsForChartPresent(): boolean {
         return true;
     }
+
+    private syncHighlightColorMap(): void {
+        const activeColorKeys = new Set(
+            (
+                this.currentlyConfiguredWidget.visualizationConfig
+                    .highlightedColumns ?? []
+            ).map(field => this.colorKey(field)),
+        );
+        const currentColorMap =
+            this.currentlyConfiguredWidget.visualizationConfig
+                .highlightedColumnColors ?? {};
+
+        const nextColorMap = Object.fromEntries(
+            Object.entries(currentColorMap).filter(([key]) =>
+                activeColorKeys.has(key),
+            ),
+        );
+
+        (
+            this.currentlyConfiguredWidget.visualizationConfig
+                .highlightedColumns ?? []
+        ).forEach(field => {
+            const key = this.colorKey(field);
+            nextColorMap[key] ??= this.defaultHighlightColor(field);
+        });
+
+        
this.currentlyConfiguredWidget.visualizationConfig.highlightedColumnColors =
+            nextColorMap;
+    }
+
+    private defaultHighlightColor(field: DataExplorerField): string {
+        return this.presetColors[field.sourceIndex % this.presetColors.length];
+    }
+
+    fieldTypeLabel(field: DataExplorerField): string {
+        return field.fieldCharacteristics.binary ? 'Boolean' : 'Numeric';
+    }
 }
diff --git 
a/ui/src/app/chart-shared/components/charts/table/model/table-widget.model.ts 
b/ui/src/app/chart-shared/components/charts/table/model/table-widget.model.ts
index 576af48c53..1b7372bc6e 100644
--- 
a/ui/src/app/chart-shared/components/charts/table/model/table-widget.model.ts
+++ 
b/ui/src/app/chart-shared/components/charts/table/model/table-widget.model.ts
@@ -25,6 +25,9 @@ import { DataExplorerVisConfig } from 
'../../../../models/dataview-dashboard.mod
 
 export interface TableVisConfig extends DataExplorerVisConfig {
     selectedColumns: DataExplorerField[];
+    highlightedColumns?: DataExplorerField[];
+    highlightedColumnColors?: Record<string, string>;
+    pageSize?: number;
     searchValue: string;
 }
 
diff --git 
a/ui/src/app/chart-shared/components/charts/table/table-widget.component.html 
b/ui/src/app/chart-shared/components/charts/table/table-widget.component.html
index 94981846a4..0ac5c257ed 100644
--- 
a/ui/src/app/chart-shared/components/charts/table/table-widget.component.html
+++ 
b/ui/src/app/chart-shared/components/charts/table/table-widget.component.html
@@ -22,7 +22,6 @@
     [ngStyle]="{
         background: dataExplorerWidget.baseAppearanceConfig.backgroundColor,
         color: dataExplorerWidget.baseAppearanceConfig.textColor,
-        overflowX: 'auto',
     }"
 >
     @if (showNoDataInDateRange) {
@@ -39,126 +38,120 @@
         </sp-too-much-data>
     }
 
-    <div class="table-container">
-        @if (showData) {
-            <table
-                data-cy="data-explorer-table"
-                mat-table
-                [dataSource]="dataSource"
-                matSort
-                (matSortChange)="sortData($event)"
-                class="table-widget"
-            >
-                <ng-container [matColumnDef]="'time'">
-                    <div>
-                        <th
-                            mat-header-cell
-                            mat-sort-header
-                            *matHeaderCellDef
-                            [ngStyle]="{
-                                background:
-                                    dataExplorerWidget.baseAppearanceConfig
-                                        .backgroundColor,
-                                color: dataExplorerWidget.baseAppearanceConfig
-                                    .textColor,
-                            }"
-                        >
-                            <label class="column-header">{{
-                                'Time' | translate
-                            }}</label>
-                        </th>
-                        <td
-                            mat-cell
-                            data-cy="data-explorer-table-row-timestamp"
-                            *matCellDef="let row"
-                            style="text-align: left"
-                        >
-                            {{ row['time'] | date: 'yyyy-MM-dd HH:mm:ss.SSS' }}
-                        </td>
-                    </div>
-                </ng-container>
-                @for (
-                    element of dataExplorerWidget.visualizationConfig
-                        .selectedColumns;
-                    track element
-                ) {
-                    <ng-container [matColumnDef]="element.fullDbName">
-                        @if (!(element.fullDbName === 'time')) {
-                            <div>
+    @if (showData) {
+        <div class="analytics-table-shell" fxLayout="column" fxFlex>
+            <div class="analytics-table-scroll" fxFlex>
+                <table data-cy="data-explorer-table" class="analytics-table">
+                    <thead>
+                        <tr>
+                            @for (
+                                column of columnNames;
+                                track trackColumn($index, column)
+                            ) {
                                 <th
-                                    mat-header-cell
-                                    mat-sort-header
-                                    *matHeaderCellDef
-                                    [ngStyle]="{
-                                        background:
-                                            dataExplorerWidget
-                                                .baseAppearanceConfig
-                                                .backgroundColor,
-                                        color: dataExplorerWidget
-                                            .baseAppearanceConfig.textColor,
+                                    [ngClass]="{
+                                        'time-column': column === 'time',
+                                        'numeric-cell': 
isNumericColumn(column),
+                                        'highlighted-column':
+                                            isHighlightedColumn(column),
                                     }"
                                 >
-                                    <label class="column-header">{{
-                                        element.fullDbName
-                                    }}</label>
+                                    <button
+                                        type="button"
+                                        class="sort-trigger"
+                                        (click)="sortBy(column)"
+                                    >
+                                        <span>
+                                            @if (column === 'time') {
+                                                {{ 'Time' | translate }}
+                                            } @else {
+                                                {{ headerLabel(column) }}
+                                            }
+                                        </span>
+                                        <mat-icon class="sort-icon">
+                                            {{ sortIcon(column) }}
+                                        </mat-icon>
+                                    </button>
                                 </th>
+                            }
+                        </tr>
+                    </thead>
+                    <tbody>
+                        @if (pagedRows.length > 0) {
+                            @for (
+                                row of pagedRows;
+                                track trackRow($index, row)
+                            ) {
+                                <tr>
+                                    @for (
+                                        column of columnNames;
+                                        track trackColumn($index, column)
+                                    ) {
+                                        <td
+                                            [ngClass]="{
+                                                'time-column':
+                                                    column === 'time',
+                                                'numeric-cell':
+                                                    isNumericColumn(column),
+                                                'highlighted-column':
+                                                    
isHighlightedColumn(column),
+                                            }"
+                                            [ngStyle]="
+                                                getCellStyle(row, column)
+                                            "
+                                            [attr.data-cy]="
+                                                column === 'time'
+                                                    ? 
'data-explorer-table-row-timestamp'
+                                                    : 
'data-explorer-table-row-' +
+                                                      column
+                                            "
+                                        >
+                                            @if (column === 'time') {
+                                                {{
+                                                    row[column]
+                                                        | date
+                                                            : 'yyyy-MM-dd 
HH:mm:ss.SSS'
+                                                }}
+                                            } @else {
+                                                {{
+                                                    formatCellValue(
+                                                        column,
+                                                        row[column]
+                                                    )
+                                                }}
+                                            }
+                                        </td>
+                                    }
+                                </tr>
+                            }
+                        } @else {
+                            <tr>
                                 <td
-                                    mat-cell
-                                    *matCellDef="let row"
-                                    [attr.data-cy]="
-                                        'data-explorer-table-row-' +
-                                        element.fullDbName
-                                    "
-                                    style="text-align: left"
+                                    class="table-empty-row"
+                                    [attr.colspan]="columnNames.length"
                                 >
-                                    {{ row[element.fullDbName] }}
+                                    {{
+                                        'No table rows match the current 
filter.'
+                                            | translate
+                                    }}
                                 </td>
-                            </div>
+                            </tr>
                         }
-                    </ng-container>
-                }
-                @for (element of groupByColumnNames; track element) {
-                    <ng-container [matColumnDef]="element">
-                        <th
-                            mat-header-cell
-                            mat-sort-header
-                            *matHeaderCellDef
-                            [ngStyle]="{
-                                background:
-                                    dataExplorerWidget.baseAppearanceConfig
-                                        .backgroundColor,
-                                color: dataExplorerWidget.baseAppearanceConfig
-                                    .textColor,
-                            }"
-                        >
-                            <label class="column-header">{{ element }}</label>
-                        </th>
-                        <td
-                            mat-cell
-                            *matCellDef="let row"
-                            [attr.data-cy]="
-                                'data-explorer-table-row-' + element
-                            "
-                            style="text-align: left"
-                        >
-                            {{ row[element] }}
-                        </td>
-                    </ng-container>
-                }
-                <tr
-                    mat-header-row
-                    *matHeaderRowDef="columnNames; sticky: true"
-                ></tr>
-                <tr mat-row *matRowDef="let row; columns: columnNames"></tr>
-            </table>
-        }
-        <mat-paginator
-            color="accent"
-            [length]="100"
-            [pageSize]="500"
-            [pageSizeOptions]="[10, 20, 50, 100, 500, 1000]"
-            aria-label="Select page"
-        >
-        </mat-paginator>
-    </div>
+                    </tbody>
+                </table>
+            </div>
+
+            <mat-paginator
+                color="accent"
+                [length]="filteredRows.length"
+                [pageIndex]="pageIndex"
+                [pageSize]="pageSize"
+                [pageSizeOptions]="pageSizeOptions"
+                [showFirstLastButtons]="true"
+                aria-label="Select page"
+                (page)="onPage($event)"
+            >
+            </mat-paginator>
+        </div>
+    }
 </div>
diff --git 
a/ui/src/app/chart-shared/components/charts/table/table-widget.component.scss 
b/ui/src/app/chart-shared/components/charts/table/table-widget.component.scss
index a94e38aa13..3d9e2ee4f0 100644
--- 
a/ui/src/app/chart-shared/components/charts/table/table-widget.component.scss
+++ 
b/ui/src/app/chart-shared/components/charts/table/table-widget.component.scss
@@ -16,31 +16,110 @@
  *
  */
 
-.table-container {
-    margin-left: 10px;
-    margin-right: 10px;
+.analytics-table-shell {
+    min-height: 0;
+    background: inherit;
+}
+
+.analytics-table-scroll {
+    min-height: 0;
+    overflow: auto;
+}
+
+.analytics-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);
+}
+
+.analytics-table th,
+.analytics-table td {
+    padding: var(--space-xs) var(--space-sm);
+    border-right: 1px solid var(--color-border-subtle, var(--color-bg-3));
+    border-bottom: 1px solid var(--color-border-subtle, var(--color-bg-3));
+    white-space: nowrap;
+    max-width: 16rem;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.analytics-table th {
+    position: sticky;
+    top: 0;
+    z-index: 2;
+    padding: 0;
+    background: var(--color-bg-0);
+}
+
+.analytics-table .time-column {
+    position: sticky;
+    left: 0;
+}
+
+.analytics-table th.time-column {
+    z-index: 4;
+    min-width: 11rem;
 }
 
-.table-widget {
+.analytics-table td.time-column {
+    z-index: 1;
+    min-width: 11rem;
+    background: var(--color-bg-0);
+}
+
+.analytics-table tbody tr:nth-child(even) td {
+    background: color-mix(in srgb, var(--color-bg-2) 55%, var(--color-bg-0));
+}
+
+.analytics-table tbody tr:nth-child(even) td.time-column {
+    background: color-mix(in srgb, var(--color-bg-2) 55%, var(--color-bg-0));
+}
+
+.analytics-table tbody tr:hover td {
+    background: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg-0));
+}
+
+.analytics-table tbody tr:hover td.time-column {
+    background: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg-0));
+}
+
+.sort-trigger {
     width: 100%;
-    background: inherit;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: var(--space-sm);
+    padding: var(--space-xs) var(--space-sm);
+    border: 0;
+    background: transparent;
     color: inherit;
+    font: inherit;
+    font-weight: var(--font-weight-semibold);
+    cursor: pointer;
 }
 
-.column-header {
-    font-size: var(--text-sm);
-    font-weight: bold;
+.sort-icon {
+    width: 1rem;
+    height: 1rem;
+    font-size: 1rem;
+    line-height: 1rem;
+    color: var(--color-secondary-text);
 }
 
-tr.mat-row,
-tr.mat-footer-row {
-    height: 24px;
+.numeric-cell {
+    text-align: right;
+    font-variant-numeric: tabular-nums;
 }
 
-.table-widget .mat-cell {
-    color: inherit;
+.highlighted-column .sort-trigger {
+    color: var(--color-primary);
 }
 
-.table-widget .mat-header-cell {
-    color: inherit;
+.table-empty-row {
+    padding: var(--space-lg);
+    text-align: center;
+    color: var(--color-secondary-text);
 }
diff --git 
a/ui/src/app/chart-shared/components/charts/table/table-widget.component.ts 
b/ui/src/app/chart-shared/components/charts/table/table-widget.component.ts
index fc1ff9b967..46ad31dfac 100644
--- a/ui/src/app/chart-shared/components/charts/table/table-widget.component.ts
+++ b/ui/src/app/chart-shared/components/charts/table/table-widget.component.ts
@@ -16,35 +16,33 @@
  *
  */
 
-import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
-import { MatSort, MatSortHeader } from '@angular/material/sort';
-import {
-    MatCell,
-    MatCellDef,
-    MatColumnDef,
-    MatHeaderCell,
-    MatHeaderCellDef,
-    MatHeaderRow,
-    MatHeaderRowDef,
-    MatRow,
-    MatRowDef,
-    MatTable,
-    MatTableDataSource,
-} from '@angular/material/table';
+import { DatePipe, NgClass, NgStyle } from '@angular/common';
+import { Component, ViewChild } from '@angular/core';
+import { MatIcon } from '@angular/material/icon';
+import { MatPaginator, PageEvent } from '@angular/material/paginator';
 import { BaseDataExplorerWidgetDirective } from 
'../base/base-data-explorer-widget.directive';
 import { TableWidgetModel } from './model/table-widget.model';
 import {
     DataExplorerField,
     SpQueryResult,
 } from '@streampipes/platform-services';
-import { MatPaginator } from '@angular/material/paginator';
 import { FlexDirective, LayoutDirective } from '@ngbracket/ngx-layout/flex';
-import { DatePipe, NgStyle } from '@angular/common';
-import { StyleDirective } from '@ngbracket/ngx-layout/extended';
 import { NoDataInDateRangeComponent } from 
'../base/no-data/no-data-in-date-range.component';
 import { TooMuchDataComponent } from 
'../base/too-much-data/too-much-data.component';
 import { TranslatePipe } from '@ngx-translate/core';
 
+type SortDirection = 'asc' | 'desc' | '';
+
+interface TableRow {
+    __rowIndex: number;
+    [key: string]: unknown;
+}
+
+interface NumericColumnStats {
+    min: number;
+    max: number;
+}
+
 @Component({
     selector: 'sp-data-explorer-table-widget',
     templateUrl: './table-widget.component.html',
@@ -53,133 +51,138 @@ import { TranslatePipe } from '@ngx-translate/core';
         LayoutDirective,
         FlexDirective,
         NgStyle,
-        StyleDirective,
+        NgClass,
         NoDataInDateRangeComponent,
         TooMuchDataComponent,
-        MatTable,
-        MatSort,
-        MatColumnDef,
-        MatHeaderCellDef,
-        MatHeaderCell,
-        MatSortHeader,
-        MatCellDef,
-        MatCell,
-        MatHeaderRowDef,
-        MatHeaderRow,
-        MatRowDef,
-        MatRow,
         MatPaginator,
+        MatIcon,
         DatePipe,
         TranslatePipe,
     ],
 })
-export class TableWidgetComponent
-    extends BaseDataExplorerWidgetDirective<TableWidgetModel>
-    implements OnInit, OnDestroy
-{
-    @ViewChild(MatSort, { static: true }) sort: MatSort;
-    @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
-
-    dataSource = new MatTableDataSource();
-    columnNames: string[];
-    groupByColumnNames: string[];
-
-    ngOnInit(): void {
-        super.ngOnInit();
-        this.dataSource.sort = this.sort;
-        this.dataSource.paginator = this.paginator;
-        this.regenerateColumnNames();
-    }
+export class TableWidgetComponent extends 
BaseDataExplorerWidgetDirective<TableWidgetModel> {
+    private static readonly DEFAULT_PAGE_SIZE = 20;
+
+    @ViewChild(MatPaginator) paginator: MatPaginator;
+
+    readonly pageSizeOptions = [10, 20, 50, 100, 250, 500];
+
+    rows: TableRow[] = [];
+    filteredRows: TableRow[] = [];
+    pagedRows: TableRow[] = [];
+    columnNames: string[] = [];
+    groupByColumnNames: string[] = [];
+    pageSize = TableWidgetComponent.DEFAULT_PAGE_SIZE;
+    pageIndex = 0;
+    sortColumn = '';
+    sortDirection: SortDirection = '';
+
+    private numericColumnStats: Record<string, NumericColumnStats> = {};
 
     regenerateColumnNames(): void {
         this.groupByColumnNames = this.makeGroupByColumns(
-            this.dataExplorerWidget.visualizationConfig.selectedColumns,
+            this.dataExplorerWidget.visualizationConfig.selectedColumns ?? [],
         );
-        this.columnNames = ['time'].concat(
-            this.dataExplorerWidget.visualizationConfig.selectedColumns.map(
-                c => c.fullDbName,
-            ),
-            ...this.groupByColumnNames,
+
+        this.columnNames = Array.from(
+            new Set([
+                'time',
+                ...(
+                    this.dataExplorerWidget.visualizationConfig
+                        .selectedColumns ?? []
+                ).map(column => column.fullDbName),
+                ...this.groupByColumnNames,
+            ]),
         );
     }
 
     makeGroupByColumns(selectedColumns: DataExplorerField[]): string[] {
         return this.dataExplorerWidget.dataConfig.sourceConfigs.flatMap(sc => {
-            return sc.queryConfig.groupBy
-                .filter(g => g.selected)
+            return (sc.queryConfig.groupBy ?? [])
+                .filter(groupBy => groupBy.selected)
                 .filter(
-                    g =>
+                    groupBy =>
                         selectedColumns.find(
-                            column => column.runtimeName === g.runtimeName,
+                            column =>
+                                column.runtimeName === groupBy.runtimeName,
                         ) === undefined,
                 )
-                .map(g => g.runtimeName);
+                .map(groupBy => groupBy.runtimeName);
         });
     }
 
-    transformData(spQueryResult: SpQueryResult) {
+    transformData(spQueryResult: SpQueryResult, rowOffset: number): TableRow[] 
{
+        let nextRowIndex = rowOffset;
         return spQueryResult.allDataSeries.flatMap(series =>
             series.rows.map(row =>
-                this.createTableObject(spQueryResult.headers, row, 
series.tags),
+                this.createTableObject(
+                    spQueryResult.headers,
+                    row,
+                    series.tags,
+                    nextRowIndex++,
+                ),
             ),
         );
     }
 
     createTableObject(
         keys: string[],
-        values: any[],
+        values: unknown[],
         tags: Record<string, string>,
-    ) {
-        const row = keys.reduce((object, key, index) => {
-            object[key] = values[index];
-            return object;
-        }, {});
-        if (tags !== null) {
+        rowIndex: number,
+    ): TableRow {
+        const row = keys.reduce(
+            (object, key, index) => {
+                object[key] = values[index];
+                return object;
+            },
+            { __rowIndex: rowIndex } as TableRow,
+        );
+
+        if (tags) {
             Object.keys(tags).forEach(key => {
                 row[key] = tags[key];
             });
         }
+
         return row;
     }
 
-    ngOnDestroy(): void {
-        this.dataSource.data = [];
+    onPage(event: PageEvent): void {
+        this.pageIndex = event.pageIndex;
+        this.pageSize = event.pageSize;
+        this.dataExplorerWidget.visualizationConfig.pageSize = event.pageSize;
+        this.updatePagedRows();
     }
 
-    sortData(event) {
-        if (event.direction === 'asc') {
-            this.dataSource.data = this.dataSource.data.sort((a, b) =>
-                a[event.active] > b[event.active]
-                    ? 1
-                    : b[event.active] > a[event.active]
-                      ? -1
-                      : 0,
-            );
+    sortBy(column: string): void {
+        if (this.sortColumn !== column) {
+            this.sortColumn = column;
+            this.sortDirection = 'asc';
+        } else if (this.sortDirection === 'asc') {
+            this.sortDirection = 'desc';
+        } else if (this.sortDirection === 'desc') {
+            this.sortDirection = '';
+            this.sortColumn = '';
+        } else {
+            this.sortDirection = 'asc';
         }
 
-        if (event.direction === 'desc') {
-            this.dataSource.data = this.dataSource.data.sort((a, b) =>
-                a[event.active] > b[event.active]
-                    ? -1
-                    : b[event.active] > a[event.active]
-                      ? 1
-                      : 0,
-            );
-        }
+        this.applyTableState(false);
+    }
 
-        if (event.direction === '') {
-            this.dataSource.data = this.dataSource.data.sort((a, b) =>
-                a['timestamp'] > b['timestamp']
-                    ? 1
-                    : b['timestamp'] > a['timestamp']
-                      ? -1
-                      : 0,
-            );
+    sortIcon(column: string): string {
+        if (this.sortColumn !== column || this.sortDirection === '') {
+            return 'unfold_more';
         }
+
+        return this.sortDirection === 'asc' ? 'north' : 'south';
     }
 
     public refreshView() {
-        this.refreshColumns();
+        this.ensureDefaults();
+        this.regenerateColumnNames();
+        this.applyTableState(true);
     }
 
     onResize(_width: number, _height: number) {}
@@ -187,11 +190,20 @@ export class TableWidgetComponent
     beforeDataFetched() {}
 
     onDataReceived(spQueryResults: SpQueryResult[]) {
+        this.ensureDefaults();
         this.regenerateColumnNames();
-        const transformedData = spQueryResults
-            .map(spQueryResult => this.transformData(spQueryResult))
-            .flat();
-        this.dataSource.data = [...transformedData];
+
+        let rowOffset = 1;
+        this.rows = spQueryResults.flatMap(spQueryResult => {
+            const transformedRows = this.transformData(
+                spQueryResult,
+                rowOffset,
+            );
+            rowOffset += transformedRows.length;
+            return transformedRows;
+        });
+
+        this.applyTableState(true);
         this.setShownComponents(false, true, false, false);
     }
 
@@ -199,22 +211,313 @@ export class TableWidgetComponent
         addedFields: DataExplorerField[],
         removedFields: DataExplorerField[],
     ) {
+        const fieldUpdateInfo = {
+            addedFields,
+            removedFields,
+            fieldProvider: this.fieldProvider,
+        };
+
         this.dataExplorerWidget.visualizationConfig.selectedColumns =
             this.fieldUpdateService.updateFieldSelection(
-                this.dataExplorerWidget.visualizationConfig.selectedColumns,
-                {
-                    addedFields,
-                    removedFields,
-                    fieldProvider: this.fieldProvider,
-                },
+                this.dataExplorerWidget.visualizationConfig.selectedColumns ??
+                    [],
+                fieldUpdateInfo,
                 () => true,
             );
-        this.refreshColumns();
+
+        this.dataExplorerWidget.visualizationConfig.highlightedColumns = (
+            this.dataExplorerWidget.visualizationConfig.highlightedColumns ?? 
[]
+        ).filter(
+            field =>
+                !removedFields.find(
+                    removedField =>
+                        removedField.fullDbName === field.fullDbName,
+                ),
+        );
+
+        this.refreshView();
     }
 
-    refreshColumns(): void {
-        this.dataSource.filter =
-            this.dataExplorerWidget.visualizationConfig.searchValue;
-        this.regenerateColumnNames();
+    isNumericColumn(column: string): boolean {
+        return !!this.fieldProvider.numericFields.find(
+            field => field.fullDbName === column,
+        );
+    }
+
+    isHighlightedColumn(column: string): boolean {
+        return !!(
+            this.dataExplorerWidget.visualizationConfig.highlightedColumns ?? 
[]
+        ).find(field => field.fullDbName === column);
+    }
+
+    headerLabel(column: string): string {
+        return column === 'time' ? 'Time' : column;
+    }
+
+    formatCellValue(column: string, value: unknown): unknown {
+        if (column === 'time') {
+            return value;
+        }
+
+        if (typeof value === 'object' && value !== null) {
+            try {
+                return JSON.stringify(value);
+            } catch {
+                return String(value);
+            }
+        }
+
+        return value ?? '';
+    }
+
+    getCellStyle(row: TableRow, column: string): Record<string, string> {
+        if (!this.isHighlightedColumn(column)) {
+            return {};
+        }
+
+        const highlightValue = this.getHighlightStrength(row[column], column);
+        if (highlightValue === undefined) {
+            return {};
+        }
+        const intensity = Math.round(8 + highlightValue * 26);
+        const highlightColor = this.getHighlightColor(column);
+
+        return {
+            background: `color-mix(in srgb, ${highlightColor} ${intensity}%, 
var(--color-bg-0))`,
+        };
+    }
+
+    trackColumn(_index: number, column: string): string {
+        return column;
+    }
+
+    trackRow(_index: number, row: TableRow): number {
+        return row.__rowIndex;
+    }
+
+    private ensureDefaults(): void {
+        this.dataExplorerWidget.visualizationConfig.searchValue ??= '';
+        this.dataExplorerWidget.visualizationConfig.highlightedColumns ??= [];
+        this.dataExplorerWidget.visualizationConfig.highlightedColumnColors ??=
+            {};
+        this.dataExplorerWidget.visualizationConfig.pageSize ??=
+            TableWidgetComponent.DEFAULT_PAGE_SIZE;
+
+        this.pageSize = this.pageSizeOptions.includes(
+            this.dataExplorerWidget.visualizationConfig.pageSize,
+        )
+            ? this.dataExplorerWidget.visualizationConfig.pageSize
+            : TableWidgetComponent.DEFAULT_PAGE_SIZE;
+    }
+
+    private applyTableState(resetPageIndex: boolean): void {
+        this.filteredRows = this.filterRows(this.rows);
+        this.numericColumnStats = this.computeNumericStats(this.filteredRows);
+        this.filteredRows = this.sortRows(this.filteredRows);
+
+        if (resetPageIndex) {
+            this.pageIndex = 0;
+            this.paginator?.firstPage();
+        }
+
+        this.ensureValidPageIndex();
+        this.updatePagedRows();
+    }
+
+    private filterRows(rows: TableRow[]): TableRow[] {
+        const searchTerm = (
+            this.dataExplorerWidget.visualizationConfig.searchValue ?? ''
+        )
+            .trim()
+            .toLowerCase();
+
+        if (!searchTerm) {
+            return [...rows];
+        }
+
+        return rows.filter(row =>
+            this.columnNames.some(column =>
+                String(this.formatCellValue(column, row[column]))
+                    .toLowerCase()
+                    .includes(searchTerm),
+            ),
+        );
+    }
+
+    private sortRows(rows: TableRow[]): TableRow[] {
+        if (!this.sortColumn || this.sortDirection === '') {
+            return [...rows];
+        }
+
+        const directionMultiplier = this.sortDirection === 'asc' ? 1 : -1;
+        return [...rows].sort((rowA, rowB) => {
+            const comparison = this.compareValues(
+                rowA[this.sortColumn],
+                rowB[this.sortColumn],
+                this.sortColumn,
+            );
+
+            return comparison * directionMultiplier;
+        });
+    }
+
+    private compareValues(
+        valueA: unknown,
+        valueB: unknown,
+        column: string,
+    ): number {
+        const normalizedA = this.normalizeSortValue(valueA, column);
+        const normalizedB = this.normalizeSortValue(valueB, column);
+
+        if (normalizedA === normalizedB) {
+            return 0;
+        }
+
+        if (normalizedA === null) {
+            return 1;
+        }
+
+        if (normalizedB === null) {
+            return -1;
+        }
+
+        return normalizedA > normalizedB ? 1 : -1;
+    }
+
+    private normalizeSortValue(
+        value: unknown,
+        column: string,
+    ): number | string | null {
+        if (value === null || value === undefined || value === '') {
+            return null;
+        }
+
+        if (column === 'time') {
+            const timestamp = new Date(value as string | number).getTime();
+            return Number.isNaN(timestamp) ? null : timestamp;
+        }
+
+        const numericValue = this.toNumber(value);
+        if (numericValue !== undefined) {
+            return numericValue;
+        }
+
+        if (typeof value === 'boolean') {
+            return value ? 1 : 0;
+        }
+
+        return String(value).toLowerCase();
+    }
+
+    private computeNumericStats(
+        rows: TableRow[],
+    ): Record<string, NumericColumnStats> {
+        return (
+            this.dataExplorerWidget.visualizationConfig.highlightedColumns ?? 
[]
+        )
+            .map(field => field.fullDbName)
+            .reduce(
+                (stats, column) => {
+                    const values = rows
+                        .map(row => this.toNumber(row[column]))
+                        .filter(
+                            (value): value is number => value !== undefined,
+                        );
+
+                    if (values.length > 0) {
+                        stats[column] = {
+                            min: Math.min(...values),
+                            max: Math.max(...values),
+                        };
+                    }
+
+                    return stats;
+                },
+                {} as Record<string, NumericColumnStats>,
+            );
+    }
+
+    private getHighlightColor(column: string): string {
+        const field = (
+            this.dataExplorerWidget.visualizationConfig.highlightedColumns ?? 
[]
+        ).find(highlightedField => highlightedField.fullDbName === column);
+
+        if (!field) {
+            return 'var(--color-primary)';
+        }
+
+        return (
+            this.dataExplorerWidget.visualizationConfig
+                .highlightedColumnColors?.[
+                `${field.fullDbName}:${field.sourceIndex}`
+            ] ?? 'var(--color-primary)'
+        );
+    }
+
+    private getHighlightStrength(
+        value: unknown,
+        column: string,
+    ): number | undefined {
+        const booleanValue = this.toBoolean(value);
+        if (booleanValue !== undefined) {
+            return booleanValue ? 1 : 0;
+        }
+
+        const numericValue = this.toNumber(value);
+        const stats = this.numericColumnStats[column];
+        if (numericValue === undefined || !stats) {
+            return undefined;
+        }
+
+        return stats.max === stats.min
+            ? 0.5
+            : (numericValue - stats.min) / (stats.max - stats.min);
+    }
+
+    private toNumber(value: unknown): number | undefined {
+        if (typeof value === 'number' && Number.isFinite(value)) {
+            return value;
+        }
+
+        if (typeof value === 'string' && value.trim() !== '') {
+            const numericValue = Number(value);
+            return Number.isFinite(numericValue) ? numericValue : undefined;
+        }
+
+        return undefined;
+    }
+
+    private toBoolean(value: unknown): boolean | undefined {
+        if (typeof value === 'boolean') {
+            return value;
+        }
+
+        if (typeof value === 'string') {
+            const normalizedValue = value.trim().toLowerCase();
+            if (normalizedValue === 'true') {
+                return true;
+            }
+            if (normalizedValue === 'false') {
+                return false;
+            }
+        }
+
+        return undefined;
+    }
+
+    private ensureValidPageIndex(): void {
+        const maxPageIndex =
+            this.filteredRows.length > 0
+                ? Math.floor((this.filteredRows.length - 1) / this.pageSize)
+                : 0;
+        this.pageIndex = Math.min(this.pageIndex, maxPageIndex);
+    }
+
+    private updatePagedRows(): void {
+        const startIndex = this.pageIndex * this.pageSize;
+        this.pagedRows = this.filteredRows.slice(
+            startIndex,
+            startIndex + this.pageSize,
+        );
     }
 }
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 10c442c48b..08f897d66b 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
@@ -32,6 +32,7 @@ import {
     FieldConfig,
     LinkageData,
     TimeSelectionConstants,
+    SpQueryResult,
     TimeSettings,
 } from '@streampipes/platform-services';
 import {
@@ -77,6 +78,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',
@@ -94,6 +96,7 @@ import { ChartContainerComponent } from 
'../../../chart-shared/components/chart-
         ChartDesignerPanelComponent,
         MatDrawerContent,
         ChartContainerComponent,
+        ChartDataPreviewComponent,
         TranslatePipe,
     ],
 })
@@ -134,6 +137,7 @@ export class ChartViewComponent
     queryParams$: Subscription;
 
     chartNotFound = false;
+    latestQueryResults: SpQueryResult[] = [];
 
     observableGenerator =
         this.dataExplorerSharedService.defaultObservableGenerator();
@@ -212,6 +216,7 @@ export class ChartViewComponent
 
     loadDataView(dataViewId: string): void {
         this.dataViewLoaded = false;
+        this.latestQueryResults = [];
         this.dataViewService
             .getChart(dataViewId)
             .pipe(
@@ -306,6 +311,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/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 504be6a44e..89e108b7e8 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
@@ -236,6 +236,9 @@ export class ChartDataSettingsComponent implements OnInit {
     makeVisualizationConfig(fields: FieldProvider): TableVisConfig {
         return {
             configurationValid: true,
+            highlightedColumns: [],
+            highlightedColumnColors: {},
+            pageSize: 20,
             searchValue: '',
             selectedColumns: fields.allFields,
         };
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..4c0fe0fa46
--- /dev/null
+++ 
b/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.html
@@ -0,0 +1,111 @@
+<!--
+~ 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" data-cy="chart-data-preview">
+    <div
+        class="preview-header"
+        fxLayout="row"
+        fxLayoutAlign="space-between center"
+        data-cy="chart-data-preview-header"
+    >
+        <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
+                (click)="toggleExpanded()"
+                data-cy="chart-data-preview-toggle"
+                [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
+            data-cy="chart-data-preview-table-scroll"
+        >
+            <table class="preview-table" data-cy="chart-data-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])"
+                                    [attr.data-cy]="
+                                        'chart-data-preview-cell-' + column
+                                    "
+                                >
+                                    {{ formatCellValue(column, row[column]) }}
+                                </td>
+                            }
+                        </tr>
+                    }
+                </tbody>
+            </table>
+        </div>
+    } @else if (expanded) {
+        <div
+            class="preview-empty"
+            fxFlex
+            fxLayout
+            fxLayoutAlign="center center"
+            data-cy="chart-data-preview-empty"
+        >
+            {{ '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..3dcfdbb1dd
--- /dev/null
+++ 
b/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.scss
@@ -0,0 +1,137 @@
+/*
+ * 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-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 {
diff --git a/ui/src/scss/sp/forms.scss b/ui/src/scss/sp/forms.scss
index 335228e26c..256224c3f5 100644
--- a/ui/src/scss/sp/forms.scss
+++ b/ui/src/scss/sp/forms.scss
@@ -16,6 +16,8 @@
  *
  */
 
+@use '@angular/material' as mat;
+
 mat-form-field.mat-mdc-form-field.form-field-size {
     font-size: 12px;
 }
@@ -46,54 +48,96 @@ mat-form-field.mat-mdc-form-field.form-field-size-smaller {
 }
 
 .form-field-small {
-    .mat-mdc-input-element {
-        font-size: 11pt;
-    }
-
-    .mat-mdc-form-field-flex {
-        max-height: 30px;
-    }
-
-    .mat-mdc-text-field-wrapper.mdc-text-field--outlined
-        .mat-mdc-form-field-infix {
-        padding-top: 0;
-        padding-bottom: 0;
-        line-height: 30px;
+    .mat-mdc-input-element,
+    .mat-mdc-select-value-text {
         font-size: 11pt;
-        min-height: 30px;
-    }
-
-    .mdc-text-field__input.smaller-font-size {
-        font-size: 10pt;
     }
 
     .mat-mdc-text-field-wrapper {
-        max-height: 30px;
         background: var(--color-bg-0);
     }
 
-    .mat-mdc-form-field {
-        max-height: 30px;
-        min-height: 0;
-    }
-
-    .mat-mdc-select-value-text {
-        font-size: 11pt;
-    }
-
-    .mat-mdc-text-field-wrapper
-        .mat-mdc-form-field-flex
-        .mat-mdc-floating-label {
-        top: 14px;
-    }
-
-    .mat-mdc-text-field-wrapper
-        .mat-mdc-form-field-flex
-        .mdc-notched-outline--upgraded
-        .mdc-floating-label--float-above {
-        --mat-mdc-form-field-label-transform: translateY(-21.75px)
-            scale(var(--mat-mdc-form-field-floating-label-scale, 0.75));
-        transform: var(--mat-mdc-form-field-label-transform);
+    // ---- Single-line controls: compact height ----
+    // Apply compact sizing only when this is NOT a textarea field.
+    &:not(.form-field-small--textarea) {
+        .mat-mdc-form-field-flex,
+        .mat-mdc-text-field-wrapper {
+            max-height: 30px;
+        }
+
+        .mat-mdc-text-field-wrapper.mdc-text-field--outlined
+            .mat-mdc-form-field-infix {
+            padding-top: 0;
+            padding-bottom: 0;
+            line-height: 30px;
+            min-height: 30px;
+            font-size: 11pt;
+        }
+
+        .mat-mdc-form-field {
+            max-height: 30px;
+            min-height: 0;
+        }
+
+        .mat-mdc-text-field-wrapper
+            .mat-mdc-form-field-flex
+            .mat-mdc-floating-label {
+            top: 14px;
+        }
+
+        .mat-mdc-text-field-wrapper
+            .mat-mdc-form-field-flex
+            .mdc-notched-outline--upgraded
+            .mdc-floating-label--float-above {
+            --mat-mdc-form-field-label-transform: translateY(-21.75px)
+                scale(var(--mat-mdc-form-field-floating-label-scale, 0.75));
+            transform: var(--mat-mdc-form-field-label-transform);
+        }
+    }
+
+    &.form-field-small--textarea,
+    .form-field-small--textarea {
+        // reset compact sizing on the textarea form-field host itself
+        &.mat-mdc-form-field {
+            max-height: none;
+            min-height: 0;
+            height: auto;
+        }
+
+        .mat-mdc-form-field-flex,
+        .mat-mdc-text-field-wrapper,
+        .mat-mdc-form-field {
+            max-height: none;
+            height: auto;
+        }
+
+        // let infix wrap content; give a small but reasonable min height
+        .mat-mdc-text-field-wrapper.mdc-text-field--outlined
+            .mat-mdc-form-field-infix {
+            line-height: normal;
+            min-height: 60px; // tweak to taste
+            height: auto;
+            padding-top: 6px;
+            padding-bottom: 6px;
+        }
+
+        // ensure textarea itself can show multiple lines
+        textarea.mat-mdc-input-element {
+            resize: vertical; // optional
+            line-height: 1.3;
+        }
+
+        // label positioning: let MDC handle it (avoid your hard-coded top)
+        .mat-mdc-text-field-wrapper
+            .mat-mdc-form-field-flex
+            .mat-mdc-floating-label {
+            top: unset;
+        }
+    }
+
+    // Optional: your special smaller font class
+    .mdc-text-field__input.smaller-font-size {
+        font-size: 10pt;
     }
 }
 


Reply via email to