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

michaelsmolina 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 1e8d648f47 feat: Chart query last run timestamp (#36934)
1e8d648f47 is described below

commit 1e8d648f478f85633d1e1093e6e9c65d7d371972
Author: Luiz Otavio <[email protected]>
AuthorDate: Fri Jan 9 17:02:18 2026 -0300

    feat: Chart query last run timestamp (#36934)
---
 .../src/query/types/QueryResponse.ts               |  6 ++-
 .../plugins/plugin-chart-table/test/testData.ts    |  1 +
 .../src/components/LastQueriedLabel/index.tsx      | 57 ++++++++++++++++++++++
 .../dashboard/components/PropertiesModal/index.tsx | 15 +++++-
 .../sections/StylingSection.test.tsx               | 45 +++++++++++++++++
 .../PropertiesModal/sections/StylingSection.tsx    | 49 ++++++++++++++++++-
 .../src/dashboard/components/SliceHeader/index.tsx |  3 ++
 .../components/SliceHeaderControls/index.tsx       | 23 ++++++---
 .../components/SliceHeaderControls/types.ts        |  1 +
 .../components/gridComponents/Chart/Chart.jsx      | 30 +++++++++++-
 .../explore/components/ExploreChartPanel/index.tsx | 15 ++++++
 superset/charts/schemas.py                         |  7 +++
 superset/common/query_context_processor.py         |  1 +
 superset/common/utils/query_cache_manager.py       | 11 +++++
 superset/dashboards/schemas.py                     |  2 +
 15 files changed, 254 insertions(+), 12 deletions(-)

diff --git 
a/superset-frontend/packages/superset-ui-core/src/query/types/QueryResponse.ts 
b/superset-frontend/packages/superset-ui-core/src/query/types/QueryResponse.ts
index d31f878ff7..71aef8a75c 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/query/types/QueryResponse.ts
+++ 
b/superset-frontend/packages/superset-ui-core/src/query/types/QueryResponse.ts
@@ -16,7 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 import { GenericDataType } from '@apache-superset/core/api/core';
 import { TimeseriesDataRecord } from '../../chart';
 import { AnnotationData } from './AnnotationLayer';
@@ -42,6 +41,11 @@ export interface ChartDataResponseResult {
   cache_key: string | null;
   cache_timeout: number | null;
   cached_dttm: string | null;
+  /**
+   * UTC timestamp when the query was executed (ISO 8601 format).
+   * For cached queries, this is when the original query ran.
+   */
+  queried_dttm: string | null;
   /**
    * Array of data records as dictionary
    */
diff --git a/superset-frontend/plugins/plugin-chart-table/test/testData.ts 
b/superset-frontend/plugins/plugin-chart-table/test/testData.ts
index ca3ed52e33..9b9aa4c852 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/testData.ts
+++ b/superset-frontend/plugins/plugin-chart-table/test/testData.ts
@@ -75,6 +75,7 @@ const basicQueryResult: ChartDataResponseResult = {
   cache_key: null,
   cached_dttm: null,
   cache_timeout: null,
+  queried_dttm: null,
   data: [],
   colnames: [],
   coltypes: [],
diff --git a/superset-frontend/src/components/LastQueriedLabel/index.tsx 
b/superset-frontend/src/components/LastQueriedLabel/index.tsx
new file mode 100644
index 0000000000..f22a3e9afe
--- /dev/null
+++ b/superset-frontend/src/components/LastQueriedLabel/index.tsx
@@ -0,0 +1,57 @@
+/**
+ * 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 { FC } from 'react';
+import { t } from '@superset-ui/core';
+import { css, useTheme } from '@apache-superset/core/ui';
+import { extendedDayjs } from '@superset-ui/core/utils/dates';
+
+interface LastQueriedLabelProps {
+  queriedDttm: string | null;
+}
+
+const LastQueriedLabel: FC<LastQueriedLabelProps> = ({ queriedDttm }) => {
+  const theme = useTheme();
+
+  if (!queriedDttm) {
+    return null;
+  }
+
+  const parsedDate = extendedDayjs.utc(queriedDttm);
+  if (!parsedDate.isValid()) {
+    return null;
+  }
+
+  const formattedTime = parsedDate.local().format('L LTS');
+
+  return (
+    <div
+      css={css`
+        font-size: ${theme.fontSizeSM}px;
+        color: ${theme.colorTextLabel};
+        padding: ${theme.sizeUnit / 2}px ${theme.sizeUnit}px;
+        text-align: right;
+      `}
+      data-test="last-queried-label"
+    >
+      {t('Last queried at')}: {formattedTime}
+    </div>
+  );
+};
+
+export default LastQueriedLabel;
diff --git 
a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx 
b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
index 267c347d1c..e67029e349 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
@@ -128,6 +128,7 @@ const PropertiesModal = ({
   const [customCss, setCustomCss] = useState('');
   const [refreshFrequency, setRefreshFrequency] = useState(0);
   const [selectedThemeId, setSelectedThemeId] = useState<number | null>(null);
+  const [showChartTimestamps, setShowChartTimestamps] = useState(false);
   const [themes, setThemes] = useState<
     Array<{
       id: number;
@@ -140,7 +141,11 @@ const PropertiesModal = ({
   const handleErrorResponse = async (response: Response) => {
     const { error, statusText, message } = await 
getClientErrorObject(response);
     let errorText = error || statusText || t('An error has occurred');
-    if (typeof message === 'object' && 'json_metadata' in message) {
+    if (
+      typeof message === 'object' &&
+      'json_metadata' in message &&
+      typeof (message as { json_metadata: unknown }).json_metadata === 'string'
+    ) {
       errorText = (message as { json_metadata: string }).json_metadata;
     } else if (typeof message === 'string') {
       errorText = message;
@@ -150,7 +155,7 @@ const PropertiesModal = ({
       }
     }
 
-    addDangerToast(errorText);
+    addDangerToast(String(errorText));
   };
 
   const handleDashboardData = useCallback(
@@ -192,10 +197,12 @@ const PropertiesModal = ({
         'shared_label_colors',
         'map_label_colors',
         'color_scheme_domain',
+        'show_chart_timestamps',
       ]);
 
       setJsonMetadata(metaDataCopy ? jsonStringify(metaDataCopy) : '');
       setRefreshFrequency(metadata?.refresh_frequency || 0);
+      setShowChartTimestamps(metadata?.show_chart_timestamps ?? false);
       originalDashboardMetadata.current = metadata;
     },
     [form],
@@ -320,11 +327,13 @@ const PropertiesModal = ({
         : false;
     const jsonMetadataObj = getJsonMetadata();
     jsonMetadataObj.refresh_frequency = refreshFrequency;
+    jsonMetadataObj.show_chart_timestamps = Boolean(showChartTimestamps);
     const customLabelColors = jsonMetadataObj.label_colors || {};
     const updatedDashboardMetadata = {
       ...originalDashboardMetadata.current,
       label_colors: customLabelColors,
       color_scheme: updatedColorScheme,
+      show_chart_timestamps: showChartTimestamps,
     };
 
     originalDashboardMetadata.current = updatedDashboardMetadata;
@@ -711,9 +720,11 @@ const PropertiesModal = ({
                   colorScheme={colorScheme}
                   customCss={customCss}
                   hasCustomLabelsColor={hasCustomLabelsColor}
+                  showChartTimestamps={showChartTimestamps}
                   onThemeChange={handleThemeChange}
                   onColorSchemeChange={onColorSchemeChange}
                   onCustomCssChange={setCustomCss}
+                  onShowChartTimestampsChange={setShowChartTimestamps}
                   addDangerToast={addDangerToast}
                 />
               ),
diff --git 
a/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.test.tsx
 
b/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.test.tsx
index 8b5536093f..fdafd819dc 100644
--- 
a/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.test.tsx
+++ 
b/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.test.tsx
@@ -70,9 +70,11 @@ const defaultProps = {
   colorScheme: 'supersetColors',
   customCss: '',
   hasCustomLabelsColor: false,
+  showChartTimestamps: false,
   onThemeChange: jest.fn(),
   onColorSchemeChange: jest.fn(),
   onCustomCssChange: jest.fn(),
+  onShowChartTimestampsChange: jest.fn(),
   addDangerToast: jest.fn(),
 };
 
@@ -156,6 +158,49 @@ test('displays current color scheme value', () => {
   expect(colorSchemeInput).toHaveValue('testColors');
 });
 
+test('renders chart timestamps field', () => {
+  render(<StylingSection {...defaultProps} />);
+
+  expect(
+    screen.getByTestId('dashboard-show-timestamps-field'),
+  ).toBeInTheDocument();
+  expect(
+    screen.getByTestId('dashboard-show-timestamps-switch'),
+  ).toBeInTheDocument();
+});
+
+test('chart timestamps switch reflects showChartTimestamps prop', () => {
+  const { rerender } = render(
+    <StylingSection {...defaultProps} showChartTimestamps={false} />,
+  );
+
+  let timestampSwitch = screen.getByTestId('dashboard-show-timestamps-switch');
+  expect(timestampSwitch).not.toBeChecked();
+
+  rerender(<StylingSection {...defaultProps} showChartTimestamps />);
+
+  timestampSwitch = screen.getByTestId('dashboard-show-timestamps-switch');
+  expect(timestampSwitch).toBeChecked();
+});
+
+test('calls onShowChartTimestampsChange when switch is toggled', async () => {
+  const onShowChartTimestampsChange = jest.fn();
+  render(
+    <StylingSection
+      {...defaultProps}
+      onShowChartTimestampsChange={onShowChartTimestampsChange}
+    />,
+  );
+
+  const timestampSwitch = screen.getByTestId(
+    'dashboard-show-timestamps-switch',
+  );
+  await userEvent.click(timestampSwitch);
+
+  expect(onShowChartTimestampsChange).toHaveBeenCalled();
+  expect(onShowChartTimestampsChange.mock.calls[0][0]).toBe(true);
+});
+
 // CSS Template Tests
 // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from 
describe blocks
 describe('CSS Template functionality', () => {
diff --git 
a/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.tsx
 
b/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.tsx
index 95e16def76..33a151bb4c 100644
--- 
a/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.tsx
+++ 
b/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.tsx
@@ -24,7 +24,7 @@ import {
   FeatureFlag,
 } from '@superset-ui/core';
 import { styled, Alert } from '@apache-superset/core/ui';
-import { CssEditor, Select } from '@superset-ui/core/components';
+import { CssEditor, Select, Switch } from '@superset-ui/core/components';
 import rison from 'rison';
 import ColorSchemeSelect from 'src/dashboard/components/ColorSchemeSelect';
 import { ModalFormField } from 'src/components/Modal';
@@ -38,6 +38,32 @@ const StyledAlert = styled(Alert)`
   margin-bottom: ${({ theme }) => theme.sizeUnit * 4}px;
 `;
 
+const StyledSwitchContainer = styled.div`
+  ${({ theme }) => `
+    display: flex;
+    flex-direction: column;
+    margin-bottom: ${theme.sizeUnit * 4}px;
+
+    .switch-row {
+      display: flex;
+      align-items: center;
+      gap: ${theme.sizeUnit * 2}px;
+    }
+
+    .switch-label {
+      color: ${theme.colorText};
+      font-size: ${theme.fontSize}px;
+    }
+
+    .switch-helper {
+      display: block;
+      color: ${theme.colorTextTertiary};
+      font-size: ${theme.fontSizeSM}px;
+      margin-top: ${theme.sizeUnit}px;
+    }
+  `}
+`;
+
 interface Theme {
   id: number;
   theme_name: string;
@@ -54,12 +80,14 @@ interface StylingSectionProps {
   colorScheme?: string;
   customCss: string;
   hasCustomLabelsColor: boolean;
+  showChartTimestamps: boolean;
   onThemeChange: (value: any) => void;
   onColorSchemeChange: (
     colorScheme: string,
     options?: { updateMetadata?: boolean },
   ) => void;
   onCustomCssChange: (css: string) => void;
+  onShowChartTimestampsChange: (value: boolean) => void;
   addDangerToast?: (message: string) => void;
 }
 
@@ -69,9 +97,11 @@ const StylingSection = ({
   colorScheme,
   customCss,
   hasCustomLabelsColor,
+  showChartTimestamps,
   onThemeChange,
   onColorSchemeChange,
   onCustomCssChange,
+  onShowChartTimestampsChange,
   addDangerToast,
 }: StylingSectionProps) => {
   const [cssTemplates, setCssTemplates] = useState<CssTemplate[]>([]);
@@ -167,6 +197,23 @@ const StylingSection = ({
           showWarning={hasCustomLabelsColor}
         />
       </ModalFormField>
+      <StyledSwitchContainer data-test="dashboard-show-timestamps-field">
+        <div className="switch-row">
+          <Switch
+            data-test="dashboard-show-timestamps-switch"
+            checked={showChartTimestamps}
+            onChange={onShowChartTimestampsChange}
+          />
+          <span className="switch-label">
+            {t('Show chart query timestamps')}
+          </span>
+        </div>
+        <span className="switch-helper">
+          {t(
+            'Display the last queried timestamp on charts in the dashboard 
view',
+          )}
+        </span>
+      </StyledSwitchContainer>
       {isFeatureEnabled(FeatureFlag.CssTemplates) &&
         cssTemplates.length > 0 && (
           <ModalFormField
diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx 
b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
index b9c0293534..c08b9b3eb6 100644
--- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
@@ -53,6 +53,7 @@ type SliceHeaderProps = SliceHeaderControlsProps & {
   formData: object;
   width: number;
   height: number;
+  queriedDttm?: string | null;
   exportPivotExcel?: (arg0: string) => void;
 };
 
@@ -141,6 +142,7 @@ const SliceHeader = forwardRef<HTMLDivElement, 
SliceHeaderProps>(
       annotationQuery = {},
       annotationError = {},
       cachedDttm = null,
+      queriedDttm = null,
       updatedDttm = null,
       isCached = [],
       isExpanded = false,
@@ -322,6 +324,7 @@ const SliceHeader = forwardRef<HTMLDivElement, 
SliceHeaderProps>(
                   isCached={isCached}
                   isExpanded={isExpanded}
                   cachedDttm={cachedDttm}
+                  queriedDttm={queriedDttm}
                   updatedDttm={updatedDttm}
                   toggleExpandSlice={toggleExpandSlice}
                   forceRefresh={forceRefresh}
diff --git 
a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx 
b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
index 564a4fb745..0af6e6a2d2 100644
--- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
@@ -111,6 +111,7 @@ export interface SliceHeaderControlsProps {
   chartStatus: string;
   isCached: boolean[];
   cachedDttm: string[] | null;
+  queriedDttm?: string | null;
   isExpanded?: boolean;
   updatedDttm: number | null;
   isFullSize?: boolean;
@@ -309,6 +310,7 @@ const SliceHeaderControls = (
     slice,
     isFullSize,
     cachedDttm = [],
+    queriedDttm = null,
     updatedDttm = null,
     addSuccessToast = () => {},
     addDangerToast = () => {},
@@ -341,6 +343,10 @@ const SliceHeaderControls = (
         : item}
     </div>
   ));
+
+  const queriedLabel = queriedDttm
+    ? extendedDayjs.utc(queriedDttm).local().format('L LTS')
+    : null;
   const fullscreenLabel = isFullSize
     ? t('Exit fullscreen')
     : t('Enter fullscreen');
@@ -355,12 +361,17 @@ const SliceHeaderControls = (
     {
       key: MenuKeys.ForceRefresh,
       label: (
-        <>
-          {t('Force refresh')}
-          <RefreshTooltip data-test="dashboard-slice-refresh-tooltip">
-            {refreshTooltip}
-          </RefreshTooltip>
-        </>
+        <Tooltip
+          title={queriedLabel ? `${t('Last queried at')}: ${queriedLabel}` : 
''}
+          overlayStyle={{ maxWidth: 'none' }}
+        >
+          <div>
+            {t('Force refresh')}
+            <RefreshTooltip data-test="dashboard-slice-refresh-tooltip">
+              {refreshTooltip}
+            </RefreshTooltip>
+          </div>
+        </Tooltip>
       ),
       disabled: props.chartStatus === 'loading',
       style: { height: 'auto', lineHeight: 'initial' },
diff --git 
a/superset-frontend/src/dashboard/components/SliceHeaderControls/types.ts 
b/superset-frontend/src/dashboard/components/SliceHeaderControls/types.ts
index f13929b82c..62d4e94e6b 100644
--- a/superset-frontend/src/dashboard/components/SliceHeaderControls/types.ts
+++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/types.ts
@@ -33,6 +33,7 @@ export interface SliceHeaderControlsProps {
   chartStatus: string;
   isCached: boolean[];
   cachedDttm: string[] | null;
+  queriedDttm?: string | null;
   isExpanded?: boolean;
   updatedDttm: number | null;
   isFullSize?: boolean;
diff --git 
a/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx 
b/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx
index 37e24b99f6..d3e1be992e 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx
@@ -28,6 +28,7 @@ import { useDispatch, useSelector } from 'react-redux';
 
 import { exportChart, mountExploreUrl } from 'src/explore/exploreUtils';
 import ChartContainer from 'src/components/Chart/ChartContainer';
+import LastQueriedLabel from 'src/components/LastQueriedLabel';
 import {
   StreamingExportModal,
   useStreamingExport,
@@ -50,6 +51,7 @@ import {
 
 import SliceHeader from '../../SliceHeader';
 import MissingChart from '../../MissingChart';
+
 import {
   addDangerToast,
   addSuccessToast,
@@ -88,6 +90,7 @@ const propTypes = {
 
 const RESIZE_TIMEOUT = 500;
 const DEFAULT_HEADER_HEIGHT = 22;
+const QUERIED_LABEL_HEIGHT = 24;
 
 const ChartWrapper = styled.div`
   overflow: hidden;
@@ -206,6 +209,9 @@ const Chart = props => {
       PLACEHOLDER_DATASOURCE,
   );
   const dashboardInfo = useSelector(state => state.dashboardInfo);
+  const showChartTimestamps = useSelector(
+    state => state.dashboardInfo?.metadata?.show_chart_timestamps ?? false,
+  );
 
   const isCached = useMemo(
     // eslint-disable-next-line camelcase
@@ -310,10 +316,25 @@ const Chart = props => {
     return DEFAULT_HEADER_HEIGHT;
   }, [headerRef]);
 
+  const queriedDttm = Array.isArray(queriesResponse)
+    ? (queriesResponse[queriesResponse.length - 1]?.queried_dttm ?? null)
+    : (queriesResponse?.queried_dttm ?? null);
+
   const getChartHeight = useCallback(() => {
     const headerHeight = getHeaderHeight();
-    return Math.max(height - headerHeight - descriptionHeight, 20);
-  }, [getHeaderHeight, height, descriptionHeight]);
+    const queriedLabelHeight =
+      showChartTimestamps && queriedDttm != null ? QUERIED_LABEL_HEIGHT : 0;
+    return Math.max(
+      height - headerHeight - descriptionHeight - queriedLabelHeight,
+      20,
+    );
+  }, [
+    getHeaderHeight,
+    height,
+    descriptionHeight,
+    queriedDttm,
+    showChartTimestamps,
+  ]);
 
   const handleFilterMenuOpen = useCallback(
     (chartId, column) => {
@@ -615,6 +636,7 @@ const Chart = props => {
         isExpanded={isExpanded}
         isCached={isCached}
         cachedDttm={cachedDttm}
+        queriedDttm={queriedDttm}
         updatedDttm={chartUpdateEndTime}
         toggleExpandSlice={boundActionCreators.toggleExpandSlice}
         forceRefresh={forceRefresh}
@@ -717,6 +739,10 @@ const Chart = props => {
         />
       </ChartWrapper>
 
+      {!isLoading && showChartTimestamps && queriedDttm != null && (
+        <LastQueriedLabel queriedDttm={queriedDttm} />
+      )}
+
       <StreamingExportModal
         visible={isStreamingModalVisible}
         onCancel={() => {
diff --git 
a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx 
b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
index ab11f2a5bf..4287e19a40 100644
--- a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
+++ b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
@@ -43,6 +43,7 @@ import { buildV1ChartDataPayload } from 
'src/explore/exploreUtils';
 import { getChartRequiredFieldsMissingMessage } from 
'src/utils/getChartRequiredFieldsMissingMessage';
 import type { ChartState, Datasource } from 'src/explore/types';
 import type { Slice } from 'src/types/Chart';
+import LastQueriedLabel from 'src/components/LastQueriedLabel';
 import { DataTablesPane } from '../DataTablesPane';
 import { ChartPills } from '../ChartPills';
 import { ExploreAlert } from '../ExploreAlert';
@@ -399,6 +400,19 @@ const ExploreChartPanel = ({
           />
         </ChartHeaderExtension>
         {renderChart()}
+        {!chart.chartStatus || chart.chartStatus !== 'loading' ? (
+          <div
+            css={css`
+              display: flex;
+              justify-content: flex-end;
+              padding-top: ${theme.sizeUnit * 2}px;
+            `}
+          >
+            <LastQueriedLabel
+              queriedDttm={chart.queriesResponse?.[0]?.queried_dttm ?? null}
+            />
+          </div>
+        ) : null}
       </div>
     ),
     [
@@ -415,6 +429,7 @@ const ExploreChartPanel = ({
       formData?.matrixify_enable_vertical_layout,
       formData?.matrixify_enable_horizontal_layout,
       renderChart,
+      theme.sizeUnit,
     ],
   );
 
diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py
index a767be42b0..2ed6446cee 100644
--- a/superset/charts/schemas.py
+++ b/superset/charts/schemas.py
@@ -1464,6 +1464,13 @@ class ChartDataResponseResult(Schema):
         required=True,
         allow_none=True,
     )
+    queried_dttm = fields.String(
+        metadata={
+            "description": "UTC timestamp when the query was executed (ISO 
8601 format)"
+        },
+        required=True,
+        allow_none=True,
+    )
     cache_timeout = fields.Integer(
         metadata={
             "description": "Cache timeout in following order: custom timeout, 
datasource "  # noqa: E501
diff --git a/superset/common/query_context_processor.py 
b/superset/common/query_context_processor.py
index be448873fd..637b2dba1f 100644
--- a/superset/common/query_context_processor.py
+++ b/superset/common/query_context_processor.py
@@ -181,6 +181,7 @@ class QueryContextProcessor:
         return {
             "cache_key": cache_key,
             "cached_dttm": cache.cache_dttm,
+            "queried_dttm": cache.queried_dttm,
             "cache_timeout": self.get_cache_timeout(),
             "df": cache.df,
             "applied_template_filters": cache.applied_template_filters,
diff --git a/superset/common/utils/query_cache_manager.py 
b/superset/common/utils/query_cache_manager.py
index a7c6331930..da2d668e8c 100644
--- a/superset/common/utils/query_cache_manager.py
+++ b/superset/common/utils/query_cache_manager.py
@@ -17,6 +17,7 @@
 from __future__ import annotations
 
 import logging
+from datetime import datetime, timezone
 from typing import Any
 
 from flask import current_app
@@ -67,6 +68,7 @@ class QueryCacheManager:
         cache_dttm: str | None = None,
         cache_value: dict[str, Any] | None = None,
         sql_rowcount: int | None = None,
+        queried_dttm: str | None = None,
     ) -> None:
         self.df = df
         self.query = query
@@ -83,6 +85,7 @@ class QueryCacheManager:
         self.cache_dttm = cache_dttm
         self.cache_value = cache_value
         self.sql_rowcount = sql_rowcount
+        self.queried_dttm = queried_dttm
 
     # pylint: disable=too-many-arguments
     def set_query_result(
@@ -108,6 +111,9 @@ class QueryCacheManager:
             self.df = query_result.df
             self.sql_rowcount = query_result.sql_rowcount
             self.annotation_data = {} if annotation_data is None else 
annotation_data
+            self.queried_dttm = (
+                
datetime.now(tz=timezone.utc).replace(microsecond=0).isoformat()
+            )
 
             if self.status != QueryStatus.FAILED:
                 current_app.config["STATS_LOGGER"].incr("loaded_from_source")
@@ -125,6 +131,8 @@ class QueryCacheManager:
                 "rejected_filter_columns": self.rejected_filter_columns,
                 "annotation_data": self.annotation_data,
                 "sql_rowcount": self.sql_rowcount,
+                "queried_dttm": self.queried_dttm,
+                "dttm": self.queried_dttm,  # Backwards compatibility
             }
             if self.is_loaded and key and self.status != QueryStatus.FAILED:
                 self.set(
@@ -181,6 +189,9 @@ class QueryCacheManager:
                 query_cache.cache_dttm = (
                     cache_value["dttm"] if cache_value is not None else None
                 )
+                query_cache.queried_dttm = cache_value.get(
+                    "queried_dttm", cache_value.get("dttm")
+                )
                 query_cache.cache_value = cache_value
                 current_app.config["STATS_LOGGER"].incr("loaded_from_cache")
             except KeyError as ex:
diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py
index 253bf3dc30..7c9236e99b 100644
--- a/superset/dashboards/schemas.py
+++ b/superset/dashboards/schemas.py
@@ -163,6 +163,8 @@ class DashboardJSONMetadataSchema(Schema):
     map_label_colors = fields.Dict()
     color_scheme_domain = fields.List(fields.Str())
     cross_filters_enabled = fields.Boolean(dump_default=True)
+    # controls visibility of "last queried at" timestamp on charts in 
dashboard view
+    show_chart_timestamps = fields.Boolean(dump_default=False)
     # used for v0 import/export
     import_time = fields.Integer()
     remote_id = fields.Integer()

Reply via email to