mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat: auto refresh dashboard (#37459)
Co-authored-by: Richard <richard@ip-192-168-1-32.sa-east-1.compute.internal> Co-authored-by: richard <richard@richards-MacBook-Pro-2.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Evan Rusackas <evan@preset.io>
This commit is contained in:
committed by
GitHub
parent
f60432e34c
commit
fca8a49561
@@ -97,6 +97,7 @@ export type BigNumberVizProps = {
|
||||
trendLineData?: TimeSeriesDatum[];
|
||||
mainColor?: string;
|
||||
echartOptions?: EChartsCoreOption;
|
||||
isRefreshing?: boolean;
|
||||
onContextMenu?: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
|
||||
@@ -16,9 +16,20 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { buildQueryContext, QueryFormData } from '@superset-ui/core';
|
||||
import { buildQueryContext } from '@superset-ui/core';
|
||||
import { EchartsGraphFormData } from './types';
|
||||
import { buildColumnsOrderBy, applyOrderBy } from '../utils/orderby';
|
||||
|
||||
export default function buildQuery(formData: EchartsGraphFormData) {
|
||||
const { source, target, source_category, target_category, row_limit } =
|
||||
formData;
|
||||
const orderby = buildColumnsOrderBy([
|
||||
source,
|
||||
target,
|
||||
source_category,
|
||||
target_category,
|
||||
]);
|
||||
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
return buildQueryContext(formData, {
|
||||
queryFields: {
|
||||
source: 'columns',
|
||||
@@ -26,5 +37,11 @@ export default function buildQuery(formData: QueryFormData) {
|
||||
source_category: 'columns',
|
||||
target_category: 'columns',
|
||||
},
|
||||
buildQuery: baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
...applyOrderBy(orderby, row_limit),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,14 +20,13 @@ import { HeatmapTransformedProps } from './types';
|
||||
import Echart from '../components/Echart';
|
||||
|
||||
export default function Heatmap(props: HeatmapTransformedProps) {
|
||||
const { height, width, echartOptions, refs, formData } = props;
|
||||
const { height, width, echartOptions, refs } = props;
|
||||
return (
|
||||
<Echart
|
||||
refs={refs}
|
||||
height={height}
|
||||
width={width}
|
||||
echartOptions={echartOptions}
|
||||
vizType={formData.vizType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,17 +16,30 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { buildQueryContext } from '@superset-ui/core';
|
||||
import { buildQueryContext, QueryFormOrderBy } from '@superset-ui/core';
|
||||
import { SankeyFormData } from './types';
|
||||
|
||||
export default function buildQuery(formData: SankeyFormData) {
|
||||
const { metric, sort_by_metric, source, target } = formData;
|
||||
const { metric, sort_by_metric, source, target, row_limit } = formData;
|
||||
const groupby = [source, target];
|
||||
const orderby: QueryFormOrderBy[] = [];
|
||||
const shouldApplyOrderBy =
|
||||
row_limit !== undefined && row_limit !== null && row_limit !== 0;
|
||||
|
||||
if (sort_by_metric && metric) {
|
||||
orderby.push([metric, false]);
|
||||
}
|
||||
[source, target].forEach(column => {
|
||||
if (column) {
|
||||
orderby.push([column, true]);
|
||||
}
|
||||
});
|
||||
|
||||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
groupby,
|
||||
...(sort_by_metric && { orderby: [[metric, false]] }),
|
||||
...(shouldApplyOrderBy && orderby.length > 0 && { orderby }),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ export type TimeseriesChartTransformedProps =
|
||||
ContextMenuTransformedProps &
|
||||
CrossFilterTransformedProps & {
|
||||
legendData?: OptionName[];
|
||||
isRefreshing?: boolean;
|
||||
xValueFormatter: TimeFormatter | StringConstructor;
|
||||
xAxis: {
|
||||
label: string;
|
||||
|
||||
@@ -16,14 +16,25 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { buildQueryContext, QueryFormData } from '@superset-ui/core';
|
||||
import { buildQueryContext } from '@superset-ui/core';
|
||||
import { EchartsTreeFormData } from './types';
|
||||
import { buildColumnsOrderBy, applyOrderBy } from '../utils/orderby';
|
||||
|
||||
export default function buildQuery(formData: EchartsTreeFormData) {
|
||||
const { id, parent, name, row_limit } = formData;
|
||||
const orderby = buildColumnsOrderBy([parent, id, name]);
|
||||
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
return buildQueryContext(formData, {
|
||||
queryFields: {
|
||||
id: 'columns',
|
||||
parent: 'columns',
|
||||
name: 'columns',
|
||||
},
|
||||
buildQuery: baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
...applyOrderBy(orderby, row_limit),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,7 +56,8 @@ export function formatTooltip({
|
||||
export default function transformProps(
|
||||
chartProps: EchartsTreeChartProps,
|
||||
): TreeTransformedProps {
|
||||
const { width, height, formData, queriesData, theme } = chartProps;
|
||||
const { width, height, formData, queriesData, theme, isRefreshing } =
|
||||
chartProps;
|
||||
const refs: Refs = {};
|
||||
const data: TreeDataRecord[] = queriesData[0].data || [];
|
||||
|
||||
@@ -182,6 +183,11 @@ export default function transformProps(
|
||||
}
|
||||
});
|
||||
}
|
||||
// Disable animation during refresh to prevent expand/collapse layout animation
|
||||
const seriesAnimation = isRefreshing
|
||||
? false
|
||||
: DEFAULT_TREE_SERIES_OPTION.animation;
|
||||
|
||||
const series: TreeSeriesOption[] = [
|
||||
{
|
||||
type: 'tree',
|
||||
@@ -192,7 +198,7 @@ export default function transformProps(
|
||||
color: theme.colorText,
|
||||
},
|
||||
emphasis: { focus: emphasis },
|
||||
animation: DEFAULT_TREE_SERIES_OPTION.animation,
|
||||
animation: seriesAnimation,
|
||||
layout,
|
||||
orient,
|
||||
symbol,
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function EchartsTreemap({
|
||||
setDataMask(dataMask);
|
||||
}
|
||||
},
|
||||
[emitCrossFilters, getCrossFilterDataMask, setDataMask],
|
||||
[emitCrossFilters, getCrossFilterDataMask, setDataMask, groupby.length],
|
||||
);
|
||||
|
||||
const eventHandlers: EventHandlers = {
|
||||
|
||||
@@ -16,15 +16,25 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { buildQueryContext, QueryFormData } from '@superset-ui/core';
|
||||
import {
|
||||
buildQueryContext,
|
||||
QueryFormData,
|
||||
QueryFormOrderBy,
|
||||
} from '@superset-ui/core';
|
||||
import { buildColumnsOrderBy, applyOrderBy } from '../utils/orderby';
|
||||
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
const { metric, sort_by_metric } = formData;
|
||||
const { metric, sort_by_metric, groupby = [], row_limit } = formData;
|
||||
const orderby: QueryFormOrderBy[] = [];
|
||||
if (sort_by_metric && metric) {
|
||||
orderby.push([metric, false]);
|
||||
}
|
||||
orderby.push(...buildColumnsOrderBy(groupby));
|
||||
|
||||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
...(sort_by_metric && { orderby: [[metric, false]] }),
|
||||
...applyOrderBy(orderby, row_limit),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -70,8 +70,11 @@ import { DEFAULT_LOCALE } from '../constants';
|
||||
// Define this interface here to avoid creating a dependency back to superset-frontend,
|
||||
// TODO: to move the type to @superset-ui/core
|
||||
interface ExplorePageState {
|
||||
common: {
|
||||
locale: string;
|
||||
common?: {
|
||||
locale?: string;
|
||||
};
|
||||
dashboardState?: {
|
||||
isRefreshing?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -156,6 +159,9 @@ function Echart(
|
||||
const locale = useSelector(
|
||||
(state: ExplorePageState) => state?.common?.locale ?? DEFAULT_LOCALE,
|
||||
).toUpperCase();
|
||||
const isDashboardRefreshing = useSelector((state: ExplorePageState) =>
|
||||
Boolean(state?.dashboardState?.isRefreshing),
|
||||
);
|
||||
|
||||
const handleSizeChange = useCallback(
|
||||
({ width, height }: { width: number; height: number }) => {
|
||||
@@ -244,15 +250,33 @@ function Echart(
|
||||
? theme.echartsOptionsOverridesByChartType?.[vizType] || {}
|
||||
: {};
|
||||
|
||||
// Disable animations during auto-refresh to reduce visual noise
|
||||
const animationOverride = isDashboardRefreshing
|
||||
? {
|
||||
animation: false,
|
||||
animationDuration: 0,
|
||||
}
|
||||
: {};
|
||||
|
||||
const themedEchartOptions = mergeReplaceArrays(
|
||||
baseTheme,
|
||||
echartOptions,
|
||||
globalOverrides,
|
||||
chartOverrides,
|
||||
animationOverride,
|
||||
);
|
||||
|
||||
chartRef.current?.setOption(themedEchartOptions, true);
|
||||
const notMerge = !isDashboardRefreshing;
|
||||
if (!notMerge) {
|
||||
chartRef.current?.dispatchAction({ type: 'hideTip' });
|
||||
}
|
||||
chartRef.current?.setOption(themedEchartOptions, {
|
||||
notMerge,
|
||||
replaceMerge: notMerge ? undefined : ['series'],
|
||||
lazyUpdate: isDashboardRefreshing,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- isDashboardRefreshing intentionally excluded to prevent extra setOption calls
|
||||
}, [didMount, echartOptions, eventHandlers, zrEventHandlers, theme, vizType]);
|
||||
|
||||
useEffect(() => () => chartRef.current?.dispose(), []);
|
||||
|
||||
@@ -128,6 +128,7 @@ export interface BaseTransformedProps<F> {
|
||||
echartOptions: EChartsCoreOption;
|
||||
formData: F;
|
||||
height: number;
|
||||
isRefreshing?: boolean;
|
||||
onContextMenu?: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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 { applyOrderBy } from './orderby';
|
||||
|
||||
test('does not apply orderby for numeric zero row limit', () => {
|
||||
expect(applyOrderBy([['col', true]], 0)).toEqual({});
|
||||
});
|
||||
|
||||
test('does not apply orderby for string zero row limit', () => {
|
||||
expect(applyOrderBy([['col', true]], '0')).toEqual({});
|
||||
});
|
||||
|
||||
test('applies orderby for non-zero string row limit', () => {
|
||||
expect(applyOrderBy([['col', true]], '10')).toEqual({
|
||||
orderby: [['col', true]],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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 { QueryFormColumn, QueryFormOrderBy } from '@superset-ui/core';
|
||||
|
||||
/**
|
||||
* Builds orderby clauses from a list of columns, filtering out any non-string
|
||||
* or nullish values. This ensures deterministic row ordering so that chart
|
||||
* elements maintain stable positions across auto-refreshes.
|
||||
*/
|
||||
export function buildColumnsOrderBy(
|
||||
columns: (QueryFormColumn | string | undefined | null)[],
|
||||
ascending: boolean = true,
|
||||
): QueryFormOrderBy[] {
|
||||
return columns
|
||||
.filter((col): col is string => typeof col === 'string' && col !== '')
|
||||
.map(col => [col, ascending]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally applies orderby to a query object spread. Returns the
|
||||
* orderby field only when row_limit is set (non-zero, non-null) and
|
||||
* there are orderby entries to apply.
|
||||
*/
|
||||
export function applyOrderBy(
|
||||
orderby: QueryFormOrderBy[],
|
||||
rowLimit: string | number | undefined | null,
|
||||
): { orderby: QueryFormOrderBy[] } | Record<string, never> {
|
||||
const parsedRowLimit =
|
||||
typeof rowLimit === 'string' ? Number(rowLimit) : rowLimit;
|
||||
const shouldApply =
|
||||
rowLimit !== undefined &&
|
||||
rowLimit !== null &&
|
||||
(Number.isNaN(parsedRowLimit) || parsedRowLimit !== 0);
|
||||
return shouldApply && orderby.length > 0 ? { orderby } : {};
|
||||
}
|
||||
@@ -171,8 +171,8 @@ export function sortAndFilterSeries(
|
||||
|
||||
return orderBy(
|
||||
sortedValues,
|
||||
['value'],
|
||||
[sortSeriesAscending ? 'asc' : 'desc'],
|
||||
['value', 'name'],
|
||||
[sortSeriesAscending ? 'asc' : 'desc', 'asc'],
|
||||
).map(({ name }) => name);
|
||||
}
|
||||
|
||||
@@ -452,7 +452,7 @@ export function getLegendProps(
|
||||
: 'vertical',
|
||||
show,
|
||||
type: effectiveType,
|
||||
selected: legendState,
|
||||
selected: legendState ?? {},
|
||||
selector: ['all', 'inverse'],
|
||||
selectorLabel: {
|
||||
fontFamily: theme.fontFamily,
|
||||
|
||||
Reference in New Issue
Block a user