This is an automated email from the ASF dual-hosted git repository.

enzomartellucci pushed a commit to branch 
enxdev/fix/dashboard-charts-infinite-loading
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 4d64caa072da2ba1d9758c03e6ac9c04f683380a
Author: Enzo Martellucci <[email protected]>
AuthorDate: Thu Feb 12 18:03:22 2026 +0100

    fix(charts): improve error display for failed charts in dashboards
---
 .../src/components/Chart/Chart.test.tsx            | 90 ++++++++++++++++++++++
 superset-frontend/src/components/Chart/Chart.tsx   | 16 +++-
 superset/db_engine_specs/gsheets.py                |  7 +-
 tests/unit_tests/db_engine_specs/test_gsheets.py   | 20 +++++
 4 files changed, 130 insertions(+), 3 deletions(-)

diff --git a/superset-frontend/src/components/Chart/Chart.test.tsx 
b/superset-frontend/src/components/Chart/Chart.test.tsx
new file mode 100644
index 00000000000..63fd3ac7642
--- /dev/null
+++ b/superset-frontend/src/components/Chart/Chart.test.tsx
@@ -0,0 +1,90 @@
+/**
+ * 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 { render, screen } from 'spec/helpers/testing-library';
+import '@testing-library/jest-dom';
+import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants';
+import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
+import Chart from './Chart';
+import type { Actions } from './Chart';
+
+const mockActions: Actions = {
+  logEvent: jest.fn() as unknown as Actions['logEvent'],
+  chartRenderingFailed: jest.fn() as unknown as 
Actions['chartRenderingFailed'],
+  chartRenderingSucceeded:
+    jest.fn() as unknown as Actions['chartRenderingSucceeded'],
+  postChartFormData: jest.fn() as unknown as Actions['postChartFormData'],
+};
+
+const baseProps = {
+  chartId: 1,
+  width: 800,
+  height: 600,
+  actions: mockActions,
+  formData: { datasource: '1__table', viz_type: 'table' },
+  vizType: 'table',
+  setControlValue: jest.fn(),
+};
+
+test('shows backend error instead of loading spinner when datasource is still 
a placeholder', () => {
+  render(
+    <Chart
+      {...baseProps}
+      chartStatus="failed"
+      chartAlert="Your default credentials were not found."
+      datasource={PLACEHOLDER_DATASOURCE}
+      datasetsStatus={ResourceStatus.Loading}
+      queriesResponse={[
+        {
+          errors: [
+            {
+              error_type: 'GENERIC_BACKEND_ERROR',
+              message: 'Your default credentials were not found.',
+              extra: {
+                issue_codes: [{ code: 1011, message: 'Issue 1011' }],
+              },
+              level: 'error',
+            },
+          ],
+        },
+      ]}
+    />,
+  );
+
+  expect(
+    screen.getByText(/Your default credentials were not found/),
+  ).toBeInTheDocument();
+});
+
+test('shows loading spinner for client-side errors without errors array when 
datasource is still a placeholder', () => {
+  render(
+    <Chart
+      {...baseProps}
+      chartStatus="failed"
+      chartAlert="Some client-side error"
+      datasource={PLACEHOLDER_DATASOURCE}
+      datasetsStatus={ResourceStatus.Loading}
+      queriesResponse={[{}]}
+    />,
+  );
+
+  expect(screen.getByRole('status')).toBeInTheDocument();
+  expect(
+    screen.queryByText(/Some client-side error/),
+  ).not.toBeInTheDocument();
+});
diff --git a/superset-frontend/src/components/Chart/Chart.tsx 
b/superset-frontend/src/components/Chart/Chart.tsx
index 9a05bb270fb..7ab23a2260d 100644
--- a/superset-frontend/src/components/Chart/Chart.tsx
+++ b/superset-frontend/src/components/Chart/Chart.tsx
@@ -168,6 +168,11 @@ const LoadingDiv = styled.div`
   transform: translate(-50%, -50%);
 `;
 
+const ErrorContainer = styled.div<{ height: number }>`
+  height: ${p => p.height}px;
+  overflow: auto;
+`;
+
 const MessageSpan = styled.span`
   display: block;
   text-align: center;
@@ -261,7 +266,10 @@ class Chart extends PureComponent<ChartProps, {}> {
     const message = chartAlert || queryResponse?.message;
 
     // if datasource is still loading, don't render JS errors
+    // but always show backend API errors (which have an errors array)
+    // so users can see real issues like auth failures
     if (
+      !error &&
       chartAlert !== undefined &&
       chartAlert !== NONEXISTENT_DATASET &&
       datasource === PLACEHOLDER_DATASOURCE &&
@@ -353,8 +361,12 @@ class Chart extends PureComponent<ChartProps, {}> {
     const isLoading = chartStatus === 'loading';
 
     if (chartStatus === 'failed') {
-      return queriesResponse?.map(item =>
-        this.renderErrorMessage(item as ChartErrorType),
+      return (
+        <ErrorContainer height={height}>
+          {queriesResponse?.map(item =>
+            this.renderErrorMessage(item as ChartErrorType),
+          )}
+        </ErrorContainer>
       );
     }
 
diff --git a/superset/db_engine_specs/gsheets.py 
b/superset/db_engine_specs/gsheets.py
index 780f92cc750..3b718b6335e 100644
--- a/superset/db_engine_specs/gsheets.py
+++ b/superset/db_engine_specs/gsheets.py
@@ -203,13 +203,18 @@ class GSheetsEngineSpec(ShillelaghEngineSpec):
         In case the token was manually revoked on Google side, `google-auth` 
will
         try to automatically refresh credentials, but it fails since it only 
has the
         access token. This override catches this scenario as well.
+
+        Also catches the case where no credentials are configured at all
+        (missing Application Default Credentials).
         """
+        error_message = str(ex)
         return (
             g
             and hasattr(g, "user")
             and (
                 isinstance(ex, cls.oauth2_exception)
-                or "credentials do not contain the necessary fields" in str(ex)
+                or "credentials do not contain the necessary fields" in 
error_message
+                or "default credentials were not found" in 
error_message.lower()
             )
         )
 
diff --git a/tests/unit_tests/db_engine_specs/test_gsheets.py 
b/tests/unit_tests/db_engine_specs/test_gsheets.py
index a2406c18fc3..aac67ea1fe2 100644
--- a/tests/unit_tests/db_engine_specs/test_gsheets.py
+++ b/tests/unit_tests/db_engine_specs/test_gsheets.py
@@ -757,6 +757,26 @@ def test_needs_oauth2_with_credentials_error(mocker: 
MockerFixture) -> None:
     assert GSheetsEngineSpec.needs_oauth2(ex) is True
 
 
+def test_needs_oauth2_with_default_credentials_not_found(
+    mocker: MockerFixture,
+) -> None:
+    """
+    Test that needs_oauth2 returns True when Application Default Credentials
+    are not configured.
+    """
+    from superset.db_engine_specs.gsheets import GSheetsEngineSpec
+
+    g = mocker.patch("superset.db_engine_specs.gsheets.g")
+    g.user = mocker.MagicMock()
+
+    ex = Exception(
+        "Your default credentials were not found. To set up Application 
Default "
+        "Credentials, see 
https://cloud.google.com/docs/authentication/external/";
+        "set-up-adc for more information."
+    )
+    assert GSheetsEngineSpec.needs_oauth2(ex) is True
+
+
 def test_needs_oauth2_with_other_error(mocker: MockerFixture) -> None:
     """
     Test that needs_oauth2 returns False for other errors.

Reply via email to