Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0232349
feat: query execution timestamp is set when query just ran
luizotavio32 Jan 6, 2026
55d34c5
fix: change deprecated lib
luizotavio32 Jan 6, 2026
676212a
feat: add query dttm field to response model
luizotavio32 Jan 6, 2026
1cb861a
test: add query dttm field to response model
luizotavio32 Jan 6, 2026
1314089
feat: add query timestamp model
luizotavio32 Jan 6, 2026
9e3d27c
feat: add query timestamp to chart and chart on dashboard
luizotavio32 Jan 6, 2026
38fad61
chore: pre-commit fixes
luizotavio32 Jan 6, 2026
2592b1c
refactor: timestamp will use browser standard time format
luizotavio32 Jan 6, 2026
e7e4181
fix: query will show the query run timestamp instead of cache timestamp
luizotavio32 Jan 7, 2026
c82e0bb
pre-commit fixes
luizotavio32 Jan 7, 2026
002e6d8
linting fixes
luizotavio32 Jan 7, 2026
d6c0008
refactor: rollback ChartPills as timestamp wont be here anymore
luizotavio32 Jan 7, 2026
4a215b5
refactor: on chart explore, timestamp will appear on bottom right of …
luizotavio32 Jan 8, 2026
2640b55
style: relative import
luizotavio32 Jan 8, 2026
77fc5dd
refactor: CodeAnt AI suggestions
luizotavio32 Jan 8, 2026
8e09e63
feat: add tooltip with last queried at [timestamp] when hovering over…
luizotavio32 Jan 8, 2026
274c3e2
feat: add on/off config for the chart timestamp under dashboard prope…
luizotavio32 Jan 9, 2026
3b0af22
test: update Styling section tests
luizotavio32 Jan 9, 2026
8cd7963
chore: trigger CI rebuild
luizotavio32 Jan 9, 2026
7678e77
refactor: change switch to appear left of the label
luizotavio32 Jan 9, 2026
63d6c23
chore: trigger CI rebuild
luizotavio32 Jan 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const basicQueryResult: ChartDataResponseResult = {
cache_key: null,
cached_dttm: null,
cache_timeout: null,
queried_dttm: null,
data: [],
colnames: [],
coltypes: [],
Expand Down
57 changes: 57 additions & 0 deletions superset-frontend/src/components/LastQueriedLabel/index.tsx
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -150,7 +155,7 @@ const PropertiesModal = ({
}
}

addDangerToast(errorText);
addDangerToast(String(errorText));
};

const handleDashboardData = useCallback(
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -711,9 +720,11 @@ const PropertiesModal = ({
colorScheme={colorScheme}
customCss={customCss}
hasCustomLabelsColor={hasCustomLabelsColor}
showChartTimestamps={showChartTimestamps}
onThemeChange={handleThemeChange}
onColorSchemeChange={onColorSchemeChange}
onCustomCssChange={setCustomCss}
onShowChartTimestampsChange={setShowChartTimestamps}
addDangerToast={addDangerToast}
/>
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};

Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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;
}

Expand All @@ -69,9 +97,11 @@ const StylingSection = ({
colorScheme,
customCss,
hasCustomLabelsColor,
showChartTimestamps,
onThemeChange,
onColorSchemeChange,
onCustomCssChange,
onShowChartTimestampsChange,
addDangerToast,
}: StylingSectionProps) => {
const [cssTemplates, setCssTemplates] = useState<CssTemplate[]>([]);
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type SliceHeaderProps = SliceHeaderControlsProps & {
formData: object;
width: number;
height: number;
queriedDttm?: string | null;
exportPivotExcel?: (arg0: string) => void;
};

Expand Down Expand Up @@ -141,6 +142,7 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
annotationQuery = {},
annotationError = {},
cachedDttm = null,
queriedDttm = null,
updatedDttm = null,
isCached = [],
isExpanded = false,
Expand Down Expand Up @@ -322,6 +324,7 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
isCached={isCached}
isExpanded={isExpanded}
cachedDttm={cachedDttm}
queriedDttm={queriedDttm}
updatedDttm={updatedDttm}
toggleExpandSlice={toggleExpandSlice}
forceRefresh={forceRefresh}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export interface SliceHeaderControlsProps {
chartStatus: string;
isCached: boolean[];
cachedDttm: string[] | null;
queriedDttm?: string | null;
isExpanded?: boolean;
updatedDttm: number | null;
isFullSize?: boolean;
Expand Down Expand Up @@ -309,6 +310,7 @@ const SliceHeaderControls = (
slice,
isFullSize,
cachedDttm = [],
queriedDttm = null,
updatedDttm = null,
addSuccessToast = () => {},
addDangerToast = () => {},
Expand Down Expand Up @@ -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');
Expand All @@ -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' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface SliceHeaderControlsProps {
chartStatus: string;
isCached: boolean[];
cachedDttm: string[] | null;
queriedDttm?: string | null;
isExpanded?: boolean;
updatedDttm: number | null;
isFullSize?: boolean;
Expand Down
Loading
Loading