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

enzomartellucci pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new c175346808a fix(table-charts): Prevent time grain from altering Raw 
Records in Tables + Interactive Tables (#37561)
c175346808a is described below

commit c175346808ab2b1634c8d9057dfbdf7efbed66f8
Author: Levis Mbote <[email protected]>
AuthorDate: Thu Feb 19 12:24:09 2026 +0300

    fix(table-charts): Prevent time grain from altering Raw Records in Tables + 
Interactive Tables (#37561)
---
 .../src/AgGridTableChart.tsx                       |  18 +-
 .../src/transformProps.ts                          |   3 +-
 .../test/AgGridTableChart.test.tsx                 | 359 +++++++++++++++++++++
 .../plugins/plugin-chart-table/src/TableChart.tsx  |   9 +-
 .../plugin-chart-table/src/transformProps.ts       |   3 +-
 .../plugin-chart-table/test/TableChart.test.tsx    |  56 +++-
 6 files changed, 439 insertions(+), 9 deletions(-)

diff --git 
a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTableChart.tsx 
b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTableChart.tsx
index 2944477696e..79eee9a91ff 100644
--- 
a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTableChart.tsx
+++ 
b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTableChart.tsx
@@ -251,8 +251,13 @@ export default function TableChart<D extends DataRecord = 
DataRecord>(
   );
 
   const timestampFormatter = useCallback(
-    value => getTimeFormatterForGranularity(timeGrain)(value),
-    [timeGrain],
+    (value: DataRecordValue) =>
+      isRawRecords
+        ? String(value ?? '')
+        : getTimeFormatterForGranularity(timeGrain)(
+            value as number | Date | null | undefined,
+          ),
+    [timeGrain, isRawRecords],
   );
 
   const toggleFilter = useCallback(
@@ -276,7 +281,14 @@ export default function TableChart<D extends DataRecord = 
DataRecord>(
         setDataMask(getCrossFilterDataMask(crossFilterProps).dataMask);
       }
     },
-    [emitCrossFilters, setDataMask, filters, timeGrain],
+    [
+      emitCrossFilters,
+      setDataMask,
+      filters,
+      timeGrain,
+      isActiveFilterValue,
+      timestampFormatter,
+    ],
   );
 
   const handleServerPaginationChange = useCallback(
diff --git 
a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts 
b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts
index 2925632468e..d793f2c27a6 100644
--- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts
@@ -343,6 +343,7 @@ const processColumns = memoizeOne(function processColumns(
       metrics: metrics_,
       percent_metrics: percentMetrics_,
       column_config: columnConfig = {},
+      query_mode: queryMode,
     },
     queriesData,
   } = props;
@@ -393,7 +394,7 @@ const processColumns = memoizeOne(function processColumns(
         const timeFormat = customFormat || tableTimestampFormat;
         // When format is "Adaptive Formatting" (smart_date)
         if (timeFormat === SMART_DATE_ID) {
-          if (granularity) {
+          if (granularity && queryMode !== QueryMode.Raw) {
             // time column use formats based on granularity
             formatter = getTimeFormatterForGranularity(granularity);
           } else if (customFormat) {
diff --git 
a/superset-frontend/plugins/plugin-chart-ag-grid-table/test/AgGridTableChart.test.tsx
 
b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/AgGridTableChart.test.tsx
new file mode 100644
index 00000000000..875f49331a3
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/AgGridTableChart.test.tsx
@@ -0,0 +1,359 @@
+/**
+ * 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 '@testing-library/jest-dom';
+import { render, screen, waitFor } from '@superset-ui/core/spec';
+import { QueryMode, TimeGranularity, SMART_DATE_ID } from '@superset-ui/core';
+import { setupAGGridModules } from 
'@superset-ui/core/components/ThemedAgGridReact';
+import AgGridTableChart from '../src/AgGridTableChart';
+import transformProps from '../src/transformProps';
+import { ProviderWrapper } from '../../plugin-chart-table/test/testHelpers';
+import testData from '../../plugin-chart-table/test/testData';
+
+const mockSetDataMask = jest.fn();
+
+beforeAll(() => {
+  setupAGGridModules();
+});
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+test('transformProps parses pageLength to pageSize', () => {
+  expect(transformProps(testData.basic).pageSize).toBe(20);
+  expect(
+    transformProps({
+      ...testData.basic,
+      rawFormData: { ...testData.basic.rawFormData, page_length: '20' },
+    }).pageSize,
+  ).toBe(20);
+  expect(
+    transformProps({
+      ...testData.basic,
+      rawFormData: { ...testData.basic.rawFormData, page_length: '' },
+    }).pageSize,
+  ).toBe(0);
+});
+
+test('transformProps does not apply time grain formatting in Raw Records 
mode', () => {
+  const rawRecordsProps = {
+    ...testData.basic,
+    rawFormData: {
+      ...testData.basic.rawFormData,
+      query_mode: QueryMode.Raw,
+      time_grain_sqla: TimeGranularity.MONTH,
+      table_timestamp_format: SMART_DATE_ID,
+    },
+  };
+
+  const transformedProps = transformProps(rawRecordsProps);
+  expect(transformedProps.isRawRecords).toBe(true);
+  expect(transformedProps.timeGrain).toBe(TimeGranularity.MONTH);
+});
+
+test('transformProps handles null/undefined timestamp values correctly', () => 
{
+  const rawRecordsProps = {
+    ...testData.basic,
+    rawFormData: {
+      ...testData.basic.rawFormData,
+      query_mode: QueryMode.Raw,
+    },
+  };
+
+  const transformedProps = transformProps(rawRecordsProps);
+  expect(transformedProps.isRawRecords).toBe(true);
+});
+
+test('AgGridTableChart renders basic data', async () => {
+  const props = transformProps(testData.basic);
+  render(
+    ProviderWrapper({
+      children: (
+        <AgGridTableChart
+          {...props}
+          setDataMask={mockSetDataMask}
+          slice_id={1}
+        />
+      ),
+    }),
+  );
+
+  await waitFor(() => {
+    const grid = document.querySelector('.ag-container');
+    expect(grid).toBeInTheDocument();
+  });
+
+  const headerCells = document.querySelectorAll('.ag-header-cell-text');
+  const headerTexts = Array.from(headerCells).map(el => el.textContent);
+  expect(headerTexts).toContain('name');
+  expect(headerTexts).toContain('sum__num');
+
+  const dataRows = document.querySelectorAll('.ag-row:not(.ag-row-pinned)');
+  expect(dataRows.length).toBe(3);
+
+  expect(screen.getByText('Michael')).toBeInTheDocument();
+  expect(screen.getByText('Joe')).toBeInTheDocument();
+  expect(screen.getByText('Maria')).toBeInTheDocument();
+});
+
+test('AgGridTableChart renders with server pagination', async () => {
+  const props = transformProps({
+    ...testData.basic,
+    rawFormData: {
+      ...testData.basic.rawFormData,
+      server_pagination: true,
+    },
+  });
+  props.serverPagination = true;
+  props.rowCount = 100;
+  props.serverPaginationData = {
+    currentPage: 0,
+    pageSize: 20,
+  };
+
+  render(
+    ProviderWrapper({
+      children: (
+        <AgGridTableChart
+          {...props}
+          setDataMask={mockSetDataMask}
+          slice_id={1}
+        />
+      ),
+    }),
+  );
+
+  await waitFor(() => {
+    const grid = document.querySelector('.ag-container');
+    expect(grid).toBeInTheDocument();
+  });
+
+  expect(screen.getByText('Page Size:')).toBeInTheDocument();
+  expect(screen.getByText('Page')).toBeInTheDocument();
+
+  const paginationEl = screen.getByText('Page Size:').closest('div')!;
+  const paginationText = paginationEl.textContent;
+  expect(paginationText).toContain('1');
+  expect(paginationText).toContain('20');
+  expect(paginationText).toContain('100');
+  expect(paginationText).toContain('5');
+});
+
+test('AgGridTableChart renders with search enabled', async () => {
+  const props = transformProps({
+    ...testData.basic,
+    rawFormData: {
+      ...testData.basic.rawFormData,
+      include_search: true,
+    },
+  });
+  props.includeSearch = true;
+
+  render(
+    ProviderWrapper({
+      children: (
+        <AgGridTableChart
+          {...props}
+          setDataMask={mockSetDataMask}
+          slice_id={1}
+        />
+      ),
+    }),
+  );
+
+  await waitFor(() => {
+    const grid = document.querySelector('.ag-container');
+    expect(grid).toBeInTheDocument();
+  });
+
+  const searchContainer = document.querySelector('.search-container');
+  expect(searchContainer).toBeInTheDocument();
+
+  const searchInput = screen.getByPlaceholderText('Search');
+  expect(searchInput).toBeInTheDocument();
+  expect(searchInput).toHaveAttribute('type', 'text');
+  expect(searchInput).toHaveAttribute('id', 'filter-text-box');
+});
+
+test('AgGridTableChart renders with totals', async () => {
+  const props = transformProps({
+    ...testData.basic,
+    rawFormData: {
+      ...testData.basic.rawFormData,
+      show_totals: true,
+    },
+  });
+  props.showTotals = true;
+  props.totals = { sum__num: 1000 };
+
+  render(
+    ProviderWrapper({
+      children: (
+        <AgGridTableChart
+          {...props}
+          setDataMask={mockSetDataMask}
+          slice_id={1}
+        />
+      ),
+    }),
+  );
+
+  await waitFor(() => {
+    const grid = document.querySelector('.ag-container');
+    expect(grid).toBeInTheDocument();
+  });
+
+  const pinnedRows = document.querySelectorAll('.ag-floating-bottom .ag-row');
+  expect(pinnedRows.length).toBeGreaterThan(0);
+
+  const dataRows = document.querySelectorAll(
+    '.ag-body-viewport .ag-row:not(.ag-row-pinned)',
+  );
+  expect(dataRows.length).toBe(3);
+});
+
+test('AgGridTableChart handles empty data', async () => {
+  const props = transformProps(testData.empty);
+
+  render(
+    ProviderWrapper({
+      children: (
+        <AgGridTableChart
+          {...props}
+          setDataMask={mockSetDataMask}
+          slice_id={1}
+        />
+      ),
+    }),
+  );
+
+  await waitFor(() => {
+    const grid = document.querySelector('.ag-container');
+    expect(grid).toBeInTheDocument();
+  });
+
+  const dataRows = document.querySelectorAll(
+    '.ag-center-cols-container .ag-row',
+  );
+  expect(dataRows.length).toBe(0);
+
+  const headerCells = document.querySelectorAll('.ag-header-cell');
+  expect(headerCells.length).toBeGreaterThan(0);
+});
+
+test('AgGridTableChart renders with time comparison', async () => {
+  const props = transformProps(testData.comparison);
+  props.isUsingTimeComparison = true;
+
+  render(
+    ProviderWrapper({
+      children: (
+        <AgGridTableChart
+          {...props}
+          setDataMask={mockSetDataMask}
+          slice_id={1}
+        />
+      ),
+    }),
+  );
+
+  await waitFor(() => {
+    const grid = document.querySelector('.ag-container');
+    expect(grid).toBeInTheDocument();
+  });
+
+  const comparisonDropdown = document.querySelector(
+    '.time-comparison-dropdown',
+  );
+  expect(comparisonDropdown).toBeInTheDocument();
+
+  const headerCells = document.querySelectorAll('.ag-header-cell-text');
+  const headerTexts = Array.from(headerCells).map(el => el.textContent);
+  expect(headerTexts).toContain('#');
+  expect(headerTexts).toContain('△');
+  expect(headerTexts).toContain('%');
+});
+
+test('AgGridTableChart handles raw records mode', async () => {
+  const rawRecordsProps = {
+    ...testData.basic,
+    rawFormData: {
+      ...testData.basic.rawFormData,
+      query_mode: QueryMode.Raw,
+    },
+  };
+  const props = transformProps(rawRecordsProps);
+
+  expect(props.isRawRecords).toBe(true);
+
+  render(
+    ProviderWrapper({
+      children: (
+        <AgGridTableChart
+          {...props}
+          setDataMask={mockSetDataMask}
+          slice_id={1}
+        />
+      ),
+    }),
+  );
+
+  await waitFor(() => {
+    const grid = document.querySelector('.ag-container');
+    expect(grid).toBeInTheDocument();
+  });
+
+  const dataRows = document.querySelectorAll('.ag-row:not(.ag-row-pinned)');
+  expect(dataRows.length).toBe(3);
+
+  const headerCells = document.querySelectorAll('.ag-header-cell');
+  expect(headerCells.length).toBeGreaterThan(0);
+});
+
+test('AgGridTableChart corrects invalid page number when currentPage >= 
totalPages', async () => {
+  const props = transformProps({
+    ...testData.basic,
+    rawFormData: {
+      ...testData.basic.rawFormData,
+      server_pagination: true,
+    },
+  });
+  props.serverPagination = true;
+  props.rowCount = 50;
+  props.serverPaginationData = {
+    currentPage: 5,
+    pageSize: 20,
+  };
+
+  render(
+    ProviderWrapper({
+      children: (
+        <AgGridTableChart
+          {...props}
+          setDataMask={mockSetDataMask}
+          slice_id={1}
+        />
+      ),
+    }),
+  );
+
+  await waitFor(() => {
+    expect(mockSetDataMask).toHaveBeenCalled();
+  });
+});
diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx 
b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
index 499ae52e952..e95387049d9 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
@@ -356,8 +356,13 @@ export default function TableChart<D extends DataRecord = 
DataRecord>(
   );
 
   const timestampFormatter = useCallback(
-    value => getTimeFormatterForGranularity(timeGrain)(value),
-    [timeGrain],
+    (value: DataRecordValue) =>
+      isRawRecords
+        ? String(value ?? '')
+        : getTimeFormatterForGranularity(timeGrain)(
+            value as number | Date | null | undefined,
+          ),
+    [timeGrain, isRawRecords],
   );
   const [tableSize, setTableSize] = useState<TableSize>({
     width: 0,
diff --git a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts 
b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
index 48849e3dd20..358181e46de 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
@@ -212,6 +212,7 @@ const processColumns = memoizeOne(function processColumns(
       metrics: metrics_,
       percent_metrics: percentMetrics_,
       column_config: columnConfig = {},
+      query_mode: queryMode,
     },
     rawDatasource,
     queriesData,
@@ -274,7 +275,7 @@ const processColumns = memoizeOne(function processColumns(
         const timeFormat = customFormat || tableTimestampFormat;
         // When format is "Adaptive Formatting" (smart_date)
         if (timeFormat === SMART_DATE_ID) {
-          if (granularity) {
+          if (granularity && queryMode !== QueryMode.Raw) {
             // time column use formats based on granularity
             formatter = getTimeFormatterForGranularity(granularity);
           } else if (customFormat) {
diff --git 
a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx 
b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
index 5183e5ab543..e6a955b511b 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
@@ -26,6 +26,12 @@ import {
   within,
 } from '@superset-ui/core/spec';
 import { cloneDeep } from 'lodash';
+import {
+  QueryMode,
+  TimeGranularity,
+  SMART_DATE_ID,
+  getTimeFormatterForGranularity,
+} from '@superset-ui/core';
 import TableChart, { sanitizeHeaderId } from '../src/TableChart';
 import { GenericDataType } from '@apache-superset/core/api/core';
 import transformProps from '../src/transformProps';
@@ -377,6 +383,52 @@ describe('plugin-chart-table', () => {
       expect(percentMetric2?.originalLabel).toBe('metric_2');
     });
 
+    test('should not apply time grain formatting in Raw Records mode', () => {
+      const rawRecordsProps = {
+        ...testData.basic,
+        rawFormData: {
+          ...testData.basic.rawFormData,
+          query_mode: QueryMode.Raw,
+          time_grain_sqla: TimeGranularity.MONTH,
+          table_timestamp_format: SMART_DATE_ID,
+        },
+      };
+
+      const transformedProps = transformProps(rawRecordsProps);
+      const timestampColumn = transformedProps.columns.find(
+        col => col.key === '__timestamp',
+      );
+
+      expect(timestampColumn).toBeDefined();
+      const testValue = new Date('2023-01-15T10:30:45');
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const formatted = (timestampColumn?.formatter as any)?.(testValue);
+      const granularityFormatted = getTimeFormatterForGranularity(
+        TimeGranularity.MONTH,
+      )(testValue as number | Date | null);
+      expect(formatted).not.toBe(granularityFormatted);
+      expect(typeof formatted).toBe('string');
+      expect(formatted).toContain('2023');
+    });
+
+    test('should handle null/undefined timestamp values correctly', () => {
+      const rawRecordsProps = {
+        ...testData.basic,
+        rawFormData: {
+          ...testData.basic.rawFormData,
+          query_mode: QueryMode.Raw,
+        },
+      };
+
+      const transformedProps = transformProps(rawRecordsProps);
+      expect(transformedProps.isRawRecords).toBe(true);
+
+      const timestampColumn = transformedProps.columns.find(
+        col => col.key === '__timestamp',
+      );
+      expect(timestampColumn).toBeDefined();
+    });
+
     describe('TableChart', () => {
       test('render basic data', () => {
         render(
@@ -386,7 +438,8 @@ describe('plugin-chart-table', () => {
         const firstDataRow = screen.getAllByRole('rowgroup')[1];
         const cells = firstDataRow.querySelectorAll('td');
         expect(cells).toHaveLength(12);
-        expect(cells[0]).toHaveTextContent('2020-01-01 12:34:56');
+        // Date is rendered as ISO string format
+        expect(cells[0]).toHaveTextContent('2020-01-01T12:34:56');
         expect(cells[1]).toHaveTextContent('Michael');
         // number is not in `metrics` list, so it should output raw value
         // (in real world Superset, this would mean the column is used in 
GROUP BY)
@@ -1422,7 +1475,6 @@ describe('plugin-chart-table', () => {
                         column: 'sum__num',
                         operator: '>',
                         targetValue: 2467,
-                        // useGradient is undefined
                       },
                     ],
                   },

Reply via email to