From a1b302a1b3aa3a6eccb1e9a8914e3a739ebcb223 Mon Sep 17 00:00:00 2001
From: Alexander Polyankin <alexander.polyankin@metabase.com>
Date: Tue, 7 Feb 2023 21:33:44 +0200
Subject: [PATCH] Show query error in the parameter modal (#28119)

---
 .../ValuesSourceModal.unit.spec.tsx           | 47 +++++++++++++++++-
 .../ValuesSourceTypeModal.tsx                 | 48 +++++++++++--------
 .../test/__support__/server-mocks/dataset.ts  |  4 ++
 3 files changed, 78 insertions(+), 21 deletions(-)

diff --git a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceModal.unit.spec.tsx b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceModal.unit.spec.tsx
index d6468b4e693..e87a9b72ea5 100644
--- a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceModal.unit.spec.tsx
+++ b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceModal.unit.spec.tsx
@@ -12,6 +12,7 @@ import {
 import {
   setupCardsEndpoints,
   setupCollectionsEndpoints,
+  setupErrorParameterValuesEndpoints,
   setupParameterValuesEndpoints,
   setupUnauthorizedCardsEndpoints,
 } from "__support__/server-mocks";
@@ -135,6 +136,10 @@ describe("ValuesSourceModal", () => {
         }),
       });
 
+      expect(
+        await screen.findByText(/We don’t have any cached values/),
+      ).toBeInTheDocument();
+
       userEvent.click(screen.getByRole("radio", { name: "Custom list" }));
       expect(screen.getByRole("radio", { name: "Custom list" })).toBeChecked();
       expect(screen.getByRole("textbox")).toHaveValue("A\nB");
@@ -241,7 +246,7 @@ describe("ValuesSourceModal", () => {
       });
     });
 
-    it("should display an error message when the user has no access to the card", async () => {
+    it("should display a message when the user has no access to the card", async () => {
       setup({
         parameter: createMockUiParameter({
           values_source_type: "card",
@@ -263,6 +268,37 @@ describe("ValuesSourceModal", () => {
       ).toBeInTheDocument();
     });
 
+    it("should display a message when there is an error in the underlying query", async () => {
+      setup({
+        parameter: createMockUiParameter({
+          values_source_type: "card",
+          values_source_config: {
+            card_id: 1,
+            value_field: ["field", 2, null],
+          },
+        }),
+        cards: [
+          createMockCard({
+            id: 1,
+            name: "Products",
+            result_metadata: [
+              createMockField({
+                id: 2,
+                display_name: "Category",
+                base_type: "type/Text",
+                semantic_type: "type/Category",
+              }),
+            ],
+          }),
+        ],
+        hasParameterValuesError: true,
+      });
+
+      expect(
+        await screen.findByText("An error occurred in your query"),
+      ).toBeInTheDocument();
+    });
+
     it("should copy card values when switching to custom list", async () => {
       setup({
         parameter: createMockUiParameter({
@@ -339,6 +375,7 @@ interface SetupOpts {
   parameterValues?: ParameterValues;
   cards?: Card[];
   hasDataAccess?: boolean;
+  hasParameterValuesError?: boolean;
 }
 
 const setup = ({
@@ -346,6 +383,7 @@ const setup = ({
   parameterValues = createMockParameterValues(),
   cards = [],
   hasDataAccess = true,
+  hasParameterValuesError = false,
 }: SetupOpts = {}) => {
   const scope = nock(location.origin);
   const onSubmit = jest.fn();
@@ -354,7 +392,12 @@ const setup = ({
   if (hasDataAccess) {
     setupCollectionsEndpoints(scope, [createMockCollection(ROOT_COLLECTION)]);
     setupCardsEndpoints(scope, cards);
-    setupParameterValuesEndpoints(scope, parameterValues);
+
+    if (!hasParameterValuesError) {
+      setupParameterValuesEndpoints(scope, parameterValues);
+    } else {
+      setupErrorParameterValuesEndpoints(scope);
+    }
   } else {
     setupUnauthorizedCardsEndpoints(scope, cards);
   }
diff --git a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx
index b1dfbe5c445..4e48aba8e0e 100644
--- a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx
+++ b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx
@@ -1,7 +1,7 @@
 import React, {
   ChangeEvent,
   useCallback,
-  useEffect,
+  useLayoutEffect,
   useMemo,
   useState,
 } from "react";
@@ -201,20 +201,20 @@ const FieldSourceModal = ({
   onChangeSourceType,
   onChangeSourceConfig,
 }: FieldSourceModalProps) => {
-  const parameterValues = useParameterValues({
+  const { values, isLoading } = useParameterValues({
     parameter,
     sourceType,
     sourceConfig,
     onFetchParameterValues,
   });
 
-  const parameterValuesText = useMemo(
-    () => getValuesText(getSourceValues(parameterValues)),
-    [parameterValues],
+  const valuesText = useMemo(
+    () => getValuesText(getSourceValues(values)),
+    [values],
   );
 
   const hasFields = getFields(parameter).length > 0;
-  const hasEmptyValues = parameterValues && parameterValues.length === 0;
+  const hasEmptyValues = !isLoading && values.length === 0;
 
   return (
     <ModalBodyWithPane>
@@ -223,7 +223,7 @@ const FieldSourceModal = ({
           <ModalLabel>{t`Where values should come from`}</ModalLabel>
           <SourceTypeOptions
             parameter={parameter}
-            parameterValues={parameterValues}
+            parameterValues={values}
             sourceType={sourceType}
             sourceConfig={sourceConfig}
             onChangeSourceType={onChangeSourceType}
@@ -241,7 +241,7 @@ const FieldSourceModal = ({
             {t`We don’t have any cached values for the connected fields. Try one of the other options, or change this widget to a search box.`}
           </ModalEmptyState>
         ) : (
-          <ModalTextArea value={parameterValuesText} readOnly fullWidth />
+          <ModalTextArea value={valuesText} readOnly fullWidth />
         )}
       </ModalMain>
     </ModalBodyWithPane>
@@ -279,16 +279,16 @@ const CardSourceModal = ({
     return getFieldByReference(fields, sourceConfig.value_field);
   }, [fields, sourceConfig]);
 
-  const parameterValues = useParameterValues({
+  const { values, isError } = useParameterValues({
     parameter,
     sourceType,
     sourceConfig,
     onFetchParameterValues,
   });
 
-  const parameterValuesText = useMemo(
-    () => getValuesText(getSourceValues(parameterValues)),
-    [parameterValues],
+  const valuesText = useMemo(
+    () => getValuesText(getSourceValues(values)),
+    [values],
   );
 
   const handleFieldChange = useCallback(
@@ -308,7 +308,7 @@ const CardSourceModal = ({
           <ModalLabel>{t`Where values should come from`}</ModalLabel>
           <SourceTypeOptions
             parameter={parameter}
-            parameterValues={parameterValues}
+            parameterValues={values}
             sourceType={sourceType}
             sourceConfig={sourceConfig}
             onChangeSourceType={onChangeSourceType}
@@ -354,8 +354,10 @@ const CardSourceModal = ({
           <ModalEmptyState>{t`Pick a model or question`}</ModalEmptyState>
         ) : !selectedField ? (
           <ModalEmptyState>{t`Pick a column`}</ModalEmptyState>
+        ) : isError ? (
+          <ModalEmptyState>{t`An error occurred in your query`}</ModalEmptyState>
         ) : (
-          <ModalTextArea value={parameterValuesText} readOnly fullWidth />
+          <ModalTextArea value={valuesText} readOnly fullWidth />
         )}
       </ModalMain>
     </ModalBodyWithPane>
@@ -446,6 +448,12 @@ const getSourceTypeOptions = (
   ];
 };
 
+interface ParameterValuesState {
+  values: ParameterValue[];
+  isLoading?: boolean;
+  isError?: boolean;
+}
+
 interface UseParameterValuesOpts {
   parameter: Parameter;
   sourceType: ValuesSourceType;
@@ -461,7 +469,7 @@ const useParameterValues = ({
   sourceConfig,
   onFetchParameterValues,
 }: UseParameterValuesOpts) => {
-  const [values, setValues] = useState<ParameterValue[]>();
+  const [state, setState] = useState<ParameterValuesState>({ values: [] });
   const handleFetchValues = useSafeAsyncFunction(onFetchParameterValues);
   const isValidSource = isValidSourceConfig(sourceType, sourceConfig);
 
@@ -474,15 +482,17 @@ const useParameterValues = ({
     [initialParameter, sourceType, sourceConfig],
   );
 
-  useEffect(() => {
+  useLayoutEffect(() => {
     if (isValidSource) {
+      setState(({ values }) => ({ values, isLoading: true }));
+
       handleFetchValues({ parameter })
-        .then(({ values }) => setValues(values))
-        .catch(() => setValues([]));
+        .then(({ values }) => setState({ values }))
+        .catch(() => setState({ values: [], isError: true }));
     }
   }, [parameter, isValidSource, handleFetchValues]);
 
-  return values;
+  return state;
 };
 
 const mapStateToProps = (
diff --git a/frontend/test/__support__/server-mocks/dataset.ts b/frontend/test/__support__/server-mocks/dataset.ts
index b87e3ca5d39..a50cbe36e12 100644
--- a/frontend/test/__support__/server-mocks/dataset.ts
+++ b/frontend/test/__support__/server-mocks/dataset.ts
@@ -7,3 +7,7 @@ export function setupParameterValuesEndpoints(
 ) {
   scope.post("/api/dataset/parameter/values").reply(200, parameterValues);
 }
+
+export function setupErrorParameterValuesEndpoints(scope: Scope) {
+  scope.post("/api/dataset/parameter/values").reply(500);
+}
-- 
GitLab