sadpandajoe commented on code in PR #45541:
URL: https://github.com/apache/superset/pull/45541#discussion_r4377256861
##########
superset-frontend/src/components/Chart/Chart.test.tsx:
##########
@@ -224,3 +224,78 @@ test('shows the semantic-layer disabled response while
dashboard metadata is una
screen.getByText('Semantic layers are not enabled.'),
).toBeInTheDocument();
});
+
+const previousResponse = [{ data: [{ value: 1 }] }] as Parameters<
+ typeof Chart
+>[0]['queriesResponse'];
+
+test('keeps a dashboard chart on screen under the spinner while it refreshes',
() => {
+ const { rerender } = render(
+ <Chart
+ {...baseProps}
+ dashboardId={17}
+ chartStatus="rendered"
+ queriesResponse={previousResponse}
+ />,
+ );
+ rerender(
+ <Chart
+ {...baseProps}
+ dashboardId={17}
+ chartStatus="loading"
+ queriesResponse={previousResponse}
+ />,
+ );
+ expect(screen.getByTestId('slice-container')).toBeInTheDocument();
+ expect(screen.getByRole('status')).toBeInTheDocument();
+ expect(screen.queryByText('Waiting on database...')).not.toBeInTheDocument();
+});
+
+test('shows no spinner over a dashboard chart during an auto-refresh', () => {
Review Comment:
This test mounts straight in `loading`, so the response's form data is still
`undefined` and `keepChartOnScreen` is false, which means the overlay at the
end of `Chart.tsx` is never reached. Changing that line to `{keepChartOnScreen
&& <Loading size="s" muted />}` (dropping `showSpinner`) would put a spinner
over every auto-refresh and this test would still pass. Could it render as
`rendered` with the response first, rerender as `loading` with
`suppressLoadingSpinner`, and assert `queryByRole('status')` is null, like the
Explore test below does?
##########
superset-frontend/src/components/Chart/Chart.tsx:
##########
@@ -435,6 +474,25 @@ function Chart({
const isLoading = chartStatus === 'loading';
// Suppress spinner during auto-refresh to avoid visual flicker
const showSpinner = isLoading && !suppressLoadingSpinner;
+ // A dashboard chart with a previous response stays on screen while it
+ // refreshes, and the spinner is drawn over it instead of replacing it.
+ // A chart that fetches its own data (deck.gl Multiple Layers) only starts
+ // reloading it once its own query lands and the spinner is gone, so kept on
+ // screen it would show the previous data with no sign of the reload. It gets
+ // the regular spinner and loads its data again when it mounts.
+ const chartFetchesOwnData =
+ getChartMetadataRegistry().get(formData.viz_type || vizType)
+ ?.enableNoResults === false;
+ const keepChartOnScreen =
+ isLoading &&
+ !!dashboardId &&
+ !chartFetchesOwnData &&
+ responseFormData !== undefined &&
+ hasValidPreviousData(
Review Comment:
Nothing drives `Chart` with a response that `hasValidPreviousData` rejects.
The Matrixify test renders `ChartRenderer` alone, and the other tests mount in
`loading` or pass through `failed`. Deleting this call leaves every test green,
but a Matrixify dashboard chart that goes `rendered` -> `loading` would then
keep the container while `ChartRenderer` returns null, leaving an empty box
with a small spinner instead of "Waiting on database...". Could a `Chart` test
render a Matrixify response as `rendered`, rerender as `loading`, and assert
the waiting message shows and the chart is gone?
##########
superset-frontend/src/components/Chart/ChartRenderer.tsx:
##########
@@ -282,25 +328,53 @@ function ChartRendererComponent({
const chartEnableNoResults =
getChartMetadataRegistry().get(vizType)?.enableNoResults ?? true;
- // A failed load leaves its errored response in the store, and the next
- // CHART_UPDATE_STARTED clears chartAlert but not queriesResponse. Checking
- // the response itself keeps that stale error from being drawn as chart data
- // while the retry is in flight.
- const hasAnyErrors = queriesResponse?.some(item => item?.error);
- // The response of a chart that fetches its own data stays empty once it has
- // loaded, so for those any response that landed counts as previous data.
- const hasPreviousResponse = chartEnableNoResults
- ? (queriesResponse?.length ?? 0) > 0
- : Array.isArray(queriesResponse);
- const hasValidPreviousData = hasPreviousResponse && !hasAnyErrors;
-
- // An auto-refresh keeps the chart on screen while its query is in flight.
- // The store holds the previous response until the new one lands, so that
- // response is what the chart renders in the meantime.
- const isRefreshing =
+ // A dashboard keeps the chart on screen while its query is in flight, be it
+ // an auto-refresh, a manual refresh or a filter change. The store holds the
+ // previous response until the new one lands, so that response is what the
+ // chart renders in the meantime.
+ const isAutoRefreshing =
Boolean(restProps.suppressLoadingSpinner) && chartStatus === 'loading';
+ const isRefreshing =
+ chartStatus === 'loading' &&
+ (isAutoRefreshing || source === ChartSource.Dashboard);
const hasRenderableResults =
- resultsReady || (isRefreshing && hasValidPreviousData);
+ resultsReady ||
+ (isRefreshing &&
+ responseFormData !== undefined &&
+ hasValidPreviousData(queriesResponse, responseFormData, vizType));
+
+ // A filter change updates formData as soon as the new query starts, so the
+ // previous response is drawn with the form data it was queried with until
+ // the new one lands. What changes without a new query always comes from the
+ // current form data: the dashboard's colours, and the controls a chart sets
+ // on itself, such as an Area chart's stack toggle or a map's viewport.
+ //
+ // A plugin transforms and redraws the chart whenever it gets a new form data
+ // object, and an ECharts chart replays its entrance animation when it does.
+ // When the chart is already drawn with the same values, as it is when a
+ // refresh starts with nothing changed, it keeps the object it has.
+ const drawnFormDataRef = useRef(currentFormData);
+ const renderedFormData = useMemo(() => {
+ let nextFormData = currentFormData;
+ if (isRefreshing && responseFormData) {
+ nextFormData = { ...responseFormData };
+ [...DASHBOARD_COLOR_KEYS, ...getDisplayControlNames(vizType)].forEach(
+ key => {
+ if (key in currentFormData) {
+ nextFormData[key] = currentFormData[key];
+ } else {
+ delete nextFormData[key];
+ }
+ },
+ );
+ }
+ return isEqual(nextFormData, drawnFormDataRef.current)
Review Comment:
Reusing the form data object keeps the plugin from re-transforming, but
`SuperChart` also re-transforms when `hooks` changes (it is an input of the
`ChartProps` selector). A native-filter change replaces `formData` and
`latestQueryFormData`, which recreates `setDataMaskCallback` and then `hooks`,
so the old response is transformed and redrawn under the spinner, and an
ECharts chart replays its entrance animation before the new data arrives. Only
the whole-dashboard refresh passes `isRefreshing`, so that path is not covered.
Is that redraw intended for filter changes, or should the callback be kept
stable while the old response is shown?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]