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()