mirror of
https://github.com/apache/superset.git
synced 2026-05-07 00:44:26 +00:00
feat: Chart query last run timestamp (#36934)
(cherry picked from commit 1e8d648f47)
This commit is contained in:
committed by
Michael S. Molina
parent
edbeadd249
commit
d29ddd1def
@@ -16,7 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { TimeseriesDataRecord } from '../../chart';
|
||||
import { AnnotationData } from './AnnotationLayer';
|
||||
|
||||
@@ -51,6 +50,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
|
||||
*/
|
||||
|
||||
@@ -75,6 +75,7 @@ const basicQueryResult: ChartDataResponseResult = {
|
||||
cache_key: null,
|
||||
cached_dttm: null,
|
||||
cache_timeout: null,
|
||||
queried_dttm: null,
|
||||
data: [],
|
||||
colnames: [],
|
||||
coltypes: [],
|
||||
|
||||
56
superset-frontend/src/components/LastQueriedLabel/index.tsx
Normal file
56
superset-frontend/src/components/LastQueriedLabel/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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, css, useTheme } from '@superset-ui/core';
|
||||
import { extendedDayjs } from 'src/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.typography.sizes.s}px;
|
||||
color: ${theme.colors.text.label};
|
||||
padding: ${theme.gridUnit / 2}px ${theme.gridUnit}px;
|
||||
text-align: right;
|
||||
`}
|
||||
data-test="last-queried-label"
|
||||
>
|
||||
{t('Last queried at')}: {formattedTime}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LastQueriedLabel;
|
||||
@@ -177,7 +177,7 @@ test('should render - FeatureFlag disabled', async () => {
|
||||
screen.getByRole('heading', { name: 'Basic information' }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Access' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Colors' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Style' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Advanced' })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'Certification' }),
|
||||
@@ -223,7 +223,7 @@ test('should render - FeatureFlag enabled', async () => {
|
||||
).toBeInTheDocument();
|
||||
// Tags will be included since isFeatureFlag always returns true in this test
|
||||
expect(screen.getByRole('heading', { name: 'Tags' })).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('heading')).toHaveLength(5);
|
||||
expect(screen.getAllByRole('heading')).toHaveLength(6);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Advanced' })).toBeInTheDocument();
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Input } from 'src/components/Input';
|
||||
import { FormItem } from 'src/components/Form';
|
||||
import jsonStringify from 'json-stringify-pretty-compact';
|
||||
import Button from 'src/components/Button';
|
||||
import { Switch } from 'src/components/Switch';
|
||||
import { AntdForm, AsyncSelect, Col, Row } from 'src/components';
|
||||
import rison from 'rison';
|
||||
import {
|
||||
@@ -67,6 +68,32 @@ const StyledJsonEditor = styled(JsonEditor)`
|
||||
border: 1px solid ${({ theme }) => theme.colors.secondary.light2};
|
||||
`;
|
||||
|
||||
const StyledSwitchContainer = styled.div`
|
||||
${({ theme }) => `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: ${theme.gridUnit * 2}px;
|
||||
|
||||
.switch-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${theme.gridUnit * 2}px;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
color: ${theme.colors.text.label};
|
||||
font-size: ${theme.typography.sizes.m}px;
|
||||
}
|
||||
|
||||
.switch-helper {
|
||||
display: block;
|
||||
color: ${theme.colors.text.help};
|
||||
font-size: ${theme.typography.sizes.m}px;
|
||||
margin-top: ${theme.gridUnit}px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
type PropertiesModalProps = {
|
||||
dashboardId: number;
|
||||
dashboardTitle?: string;
|
||||
@@ -120,6 +147,7 @@ const PropertiesModal = ({
|
||||
const [roles, setRoles] = useState<Roles>([]);
|
||||
const saveLabel = onlyApply ? t('Apply') : t('Save');
|
||||
const [tags, setTags] = useState<TagType[]>([]);
|
||||
const [showChartTimestamps, setShowChartTimestamps] = useState(false);
|
||||
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
|
||||
const originalDashboardMetadata = useRef<Record<string, any>>({});
|
||||
|
||||
@@ -134,7 +162,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;
|
||||
@@ -146,7 +178,7 @@ const PropertiesModal = ({
|
||||
|
||||
Modal.error({
|
||||
title: t('Error'),
|
||||
content: errorText,
|
||||
content: String(errorText),
|
||||
okButtonProps: { danger: true, className: 'btn-danger' },
|
||||
});
|
||||
};
|
||||
@@ -209,9 +241,11 @@ const PropertiesModal = ({
|
||||
'shared_label_colors',
|
||||
'map_label_colors',
|
||||
'color_scheme_domain',
|
||||
'show_chart_timestamps',
|
||||
]);
|
||||
|
||||
setJsonMetadata(metaDataCopy ? jsonStringify(metaDataCopy) : '');
|
||||
setShowChartTimestamps(metadata?.show_chart_timestamps ?? false);
|
||||
originalDashboardMetadata.current = metadata;
|
||||
},
|
||||
[form],
|
||||
@@ -358,11 +392,13 @@ const PropertiesModal = ({
|
||||
? resettableCustomLabels
|
||||
: false;
|
||||
const jsonMetadataObj = getJsonMetadata();
|
||||
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;
|
||||
@@ -378,6 +414,8 @@ const PropertiesModal = ({
|
||||
updateMetadata: false,
|
||||
});
|
||||
|
||||
// Add show_chart_timestamps back to metadata since it was omitted from jsonMetadata state
|
||||
metadata.show_chart_timestamps = showChartTimestamps;
|
||||
currentJsonMetadata = jsonStringify(metadata);
|
||||
|
||||
const moreOnSubmitProps: { roles?: Roles } = {};
|
||||
@@ -428,19 +466,45 @@ const PropertiesModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getRowsWithoutRoles = () => {
|
||||
const jsonMetadataObj = getJsonMetadata();
|
||||
const hasCustomLabelsColor = !!Object.keys(
|
||||
jsonMetadataObj?.label_colors || {},
|
||||
).length;
|
||||
const getRowsWithoutRoles = () => (
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<h3 style={{ marginTop: '1em' }}>{t('Access')}</h3>
|
||||
<StyledFormItem label={t('Owners')}>
|
||||
<AsyncSelect
|
||||
allowClear
|
||||
ariaLabel={t('Owners')}
|
||||
disabled={isLoading}
|
||||
mode="multiple"
|
||||
onChange={handleOnChangeOwners}
|
||||
options={(input, page, pageSize) =>
|
||||
loadAccessOptions('owners', input, page, pageSize)
|
||||
}
|
||||
value={handleOwnersSelectValue()}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
<p className="help-block">
|
||||
{t(
|
||||
'Owners is a list of users who can alter the dashboard. Searchable by name or username.',
|
||||
)}
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
|
||||
return (
|
||||
const getRowsWithRoles = () => (
|
||||
<>
|
||||
<Row>
|
||||
<Col xs={24} md={24}>
|
||||
<h3 style={{ marginTop: '1em' }}>{t('Access')}</h3>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<h3 style={{ marginTop: '1em' }}>{t('Access')}</h3>
|
||||
<StyledFormItem label={t('Owners')}>
|
||||
<AsyncSelect
|
||||
allowClear
|
||||
allowNewOptions
|
||||
ariaLabel={t('Owners')}
|
||||
disabled={isLoading}
|
||||
mode="multiple"
|
||||
@@ -458,85 +522,28 @@ const PropertiesModal = ({
|
||||
</p>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<h3 style={{ marginTop: '1em' }}>{t('Colors')}</h3>
|
||||
<ColorSchemeControlWrapper
|
||||
hasCustomLabelsColor={hasCustomLabelsColor}
|
||||
onChange={onColorSchemeChange}
|
||||
colorScheme={colorScheme}
|
||||
/>
|
||||
<StyledFormItem label={t('Roles')}>
|
||||
<AsyncSelect
|
||||
allowClear
|
||||
ariaLabel={t('Roles')}
|
||||
disabled={isLoading}
|
||||
mode="multiple"
|
||||
onChange={handleOnChangeRoles}
|
||||
options={(input, page, pageSize) =>
|
||||
loadAccessOptions('roles', input, page, pageSize)
|
||||
}
|
||||
value={handleRolesSelectValue()}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
<p className="help-block">
|
||||
{t(
|
||||
'Roles is a list which defines access to the dashboard. Granting a role access to a dashboard will bypass dataset level checks. If no roles are defined, regular access permissions apply.',
|
||||
)}
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
const getRowsWithRoles = () => {
|
||||
const jsonMetadataObj = getJsonMetadata();
|
||||
const hasCustomLabelsColor = !!Object.keys(
|
||||
jsonMetadataObj?.label_colors || {},
|
||||
).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Col xs={24} md={24}>
|
||||
<h3 style={{ marginTop: '1em' }}>{t('Access')}</h3>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<StyledFormItem label={t('Owners')}>
|
||||
<AsyncSelect
|
||||
allowClear
|
||||
allowNewOptions
|
||||
ariaLabel={t('Owners')}
|
||||
disabled={isLoading}
|
||||
mode="multiple"
|
||||
onChange={handleOnChangeOwners}
|
||||
options={(input, page, pageSize) =>
|
||||
loadAccessOptions('owners', input, page, pageSize)
|
||||
}
|
||||
value={handleOwnersSelectValue()}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
<p className="help-block">
|
||||
{t(
|
||||
'Owners is a list of users who can alter the dashboard. Searchable by name or username.',
|
||||
)}
|
||||
</p>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<StyledFormItem label={t('Roles')}>
|
||||
<AsyncSelect
|
||||
allowClear
|
||||
ariaLabel={t('Roles')}
|
||||
disabled={isLoading}
|
||||
mode="multiple"
|
||||
onChange={handleOnChangeRoles}
|
||||
options={(input, page, pageSize) =>
|
||||
loadAccessOptions('roles', input, page, pageSize)
|
||||
}
|
||||
value={handleRolesSelectValue()}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
<p className="help-block">
|
||||
{t(
|
||||
'Roles is a list which defines access to the dashboard. Granting a role access to a dashboard will bypass dataset level checks. If no roles are defined, regular access permissions apply.',
|
||||
)}
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={24} md={12}>
|
||||
<ColorSchemeControlWrapper
|
||||
hasCustomLabelsColor={hasCustomLabelsColor}
|
||||
onChange={onColorSchemeChange}
|
||||
colorScheme={colorScheme}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
||||
</>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
@@ -591,6 +598,11 @@ const PropertiesModal = ({
|
||||
setTags(parsedTags);
|
||||
};
|
||||
|
||||
const jsonMetadataObj = getJsonMetadata();
|
||||
const hasCustomLabelsColor = !!Object.keys(
|
||||
jsonMetadataObj?.label_colors || {},
|
||||
).length;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={show}
|
||||
@@ -663,6 +675,41 @@ const PropertiesModal = ({
|
||||
{isFeatureEnabled(FeatureFlag.DashboardRbac)
|
||||
? getRowsWithRoles()
|
||||
: getRowsWithoutRoles()}
|
||||
<Row>
|
||||
<Col xs={24} md={24}>
|
||||
<h3>{t('Style')}</h3>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={24} md={12}>
|
||||
<div css={{ paddingRight: '8px' }}>
|
||||
<ColorSchemeControlWrapper
|
||||
hasCustomLabelsColor={hasCustomLabelsColor}
|
||||
onChange={onColorSchemeChange}
|
||||
colorScheme={colorScheme}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<StyledSwitchContainer data-test="dashboard-show-timestamps-field">
|
||||
<div className="switch-row">
|
||||
<Switch
|
||||
data-test="dashboard-show-timestamps-switch"
|
||||
checked={showChartTimestamps}
|
||||
onChange={setShowChartTimestamps}
|
||||
/>
|
||||
<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>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={24} md={24}>
|
||||
<h3>{t('Certification')}</h3>
|
||||
|
||||
@@ -50,6 +50,7 @@ type SliceHeaderProps = SliceHeaderControlsProps & {
|
||||
formData: object;
|
||||
width: number;
|
||||
height: number;
|
||||
queriedDttm?: string | null;
|
||||
};
|
||||
|
||||
const annotationsLoading = t('Annotation layers are still loading.');
|
||||
@@ -141,6 +142,7 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
|
||||
annotationQuery = {},
|
||||
annotationError = {},
|
||||
cachedDttm = null,
|
||||
queriedDttm = null,
|
||||
updatedDttm = null,
|
||||
isCached = [],
|
||||
isExpanded = false,
|
||||
@@ -271,6 +273,7 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
|
||||
isCached={isCached}
|
||||
isExpanded={isExpanded}
|
||||
cachedDttm={cachedDttm}
|
||||
queriedDttm={queriedDttm}
|
||||
updatedDttm={updatedDttm}
|
||||
toggleExpandSlice={toggleExpandSlice}
|
||||
forceRefresh={forceRefresh}
|
||||
|
||||
@@ -117,6 +117,7 @@ export interface SliceHeaderControlsProps {
|
||||
chartStatus: string;
|
||||
isCached: boolean[];
|
||||
cachedDttm: string[] | null;
|
||||
queriedDttm?: string | null;
|
||||
isExpanded?: boolean;
|
||||
updatedDttm: number | null;
|
||||
isFullSize?: boolean;
|
||||
@@ -292,6 +293,7 @@ const SliceHeaderControls = (
|
||||
slice,
|
||||
isFullSize,
|
||||
cachedDttm = [],
|
||||
queriedDttm = null,
|
||||
updatedDttm = null,
|
||||
addSuccessToast = () => {},
|
||||
addDangerToast = () => {},
|
||||
@@ -324,6 +326,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');
|
||||
@@ -359,10 +365,17 @@ const SliceHeaderControls = (
|
||||
style={{ height: 'auto', lineHeight: 'initial' }}
|
||||
data-test="refresh-chart-menu-item"
|
||||
>
|
||||
{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>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item key={MenuKeys.Fullscreen}>{fullscreenLabel}</Menu.Item>
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface SliceHeaderControlsProps {
|
||||
chartStatus: string;
|
||||
isCached: boolean[];
|
||||
cachedDttm: string[] | null;
|
||||
queriedDttm?: string | null;
|
||||
isExpanded?: boolean;
|
||||
updatedDttm: number | null;
|
||||
isFullSize?: boolean;
|
||||
|
||||
@@ -27,6 +27,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 {
|
||||
LOG_ACTIONS_CHANGE_DASHBOARD_FILTER,
|
||||
LOG_ACTIONS_EXPLORE_DASHBOARD_CHART,
|
||||
@@ -79,6 +80,7 @@ const propTypes = {
|
||||
// resizing across all slices on a dashboard on every update
|
||||
const RESIZE_TIMEOUT = 500;
|
||||
const DEFAULT_HEADER_HEIGHT = 22;
|
||||
const QUERIED_LABEL_HEIGHT = 24;
|
||||
|
||||
const ChartWrapper = styled.div`
|
||||
overflow: hidden;
|
||||
@@ -161,6 +163,21 @@ const Chart = props => {
|
||||
PLACEHOLDER_DATASOURCE,
|
||||
);
|
||||
const dashboardInfo = useSelector(state => state.dashboardInfo);
|
||||
const showChartTimestamps = useSelector(
|
||||
state => state.dashboardInfo?.metadata?.show_chart_timestamps ?? false,
|
||||
);
|
||||
|
||||
const { queriesResponse, chartUpdateEndTime, chartStatus, annotationQuery } =
|
||||
chart;
|
||||
const isLoading = chartStatus === 'loading';
|
||||
// eslint-disable-next-line camelcase
|
||||
const isCached = queriesResponse?.map(({ is_cached }) => is_cached) || [];
|
||||
const cachedDttm =
|
||||
// eslint-disable-next-line camelcase
|
||||
queriesResponse?.map(({ cached_dttm }) => cached_dttm) || [];
|
||||
const queriedDttm = Array.isArray(queriesResponse)
|
||||
? (queriesResponse[queriesResponse.length - 1]?.queried_dttm ?? null)
|
||||
: (queriesResponse?.queried_dttm ?? null);
|
||||
|
||||
const [descriptionHeight, setDescriptionHeight] = useState(0);
|
||||
const [height, setHeight] = useState(props.height);
|
||||
@@ -224,8 +241,19 @@ const Chart = props => {
|
||||
|
||||
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) => {
|
||||
@@ -419,15 +447,6 @@ const Chart = props => {
|
||||
return <MissingChart height={getChartHeight()} />;
|
||||
}
|
||||
|
||||
const { queriesResponse, chartUpdateEndTime, chartStatus, annotationQuery } =
|
||||
chart;
|
||||
const isLoading = chartStatus === 'loading';
|
||||
// eslint-disable-next-line camelcase
|
||||
const isCached = queriesResponse?.map(({ is_cached }) => is_cached) || [];
|
||||
const cachedDttm =
|
||||
// eslint-disable-next-line camelcase
|
||||
queriesResponse?.map(({ cached_dttm }) => cached_dttm) || [];
|
||||
|
||||
return (
|
||||
<SliceContainer
|
||||
className="chart-slice"
|
||||
@@ -442,6 +461,7 @@ const Chart = props => {
|
||||
isExpanded={isExpanded}
|
||||
isCached={isCached}
|
||||
cachedDttm={cachedDttm}
|
||||
queriedDttm={queriedDttm}
|
||||
updatedDttm={chartUpdateEndTime}
|
||||
toggleExpandSlice={boundActionCreators.toggleExpandSlice}
|
||||
forceRefresh={forceRefresh}
|
||||
@@ -531,6 +551,10 @@ const Chart = props => {
|
||||
emitCrossFilters={emitCrossFilters}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
|
||||
{!isLoading && showChartTimestamps && queriedDttm != null && (
|
||||
<LastQueriedLabel queriedDttm={queriedDttm} />
|
||||
)}
|
||||
</SliceContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,6 +43,7 @@ import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
|
||||
import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
|
||||
import { buildV1ChartDataPayload } from 'src/explore/exploreUtils';
|
||||
import { getChartRequiredFieldsMissingMessage } from 'src/utils/getChartRequiredFieldsMissingMessage';
|
||||
import LastQueriedLabel from 'src/components/LastQueriedLabel';
|
||||
import { DataTablesPane } from '../DataTablesPane';
|
||||
import { ChartPills } from '../ChartPills';
|
||||
import { ExploreAlert } from '../ExploreAlert';
|
||||
@@ -372,6 +373,19 @@ const ExploreChartPanel = ({
|
||||
rowLimit={formData?.row_limit}
|
||||
/>
|
||||
{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>
|
||||
),
|
||||
[
|
||||
@@ -386,6 +400,7 @@ const ExploreChartPanel = ({
|
||||
refreshCachedQuery,
|
||||
formData?.row_limit,
|
||||
renderChart,
|
||||
theme.sizeUnit,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1433,6 +1433,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
|
||||
|
||||
@@ -224,6 +224,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,
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from flask_caching import Cache
|
||||
@@ -65,6 +66,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
|
||||
@@ -81,6 +83,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(
|
||||
@@ -106,6 +109,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:
|
||||
stats_logger.incr("loaded_from_source")
|
||||
@@ -121,6 +127,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(
|
||||
@@ -175,6 +183,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
|
||||
stats_logger.incr("loaded_from_cache")
|
||||
except KeyError as ex:
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user