You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by su...@apache.org on 2021/02/18 09:32:36 UTC

[superset] 01/01: refactor: introduce api resource hooks, fix error owners

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

suddjian pushed a commit to branch api-resource-hooks
in repository https://gitbox.apache.org/repos/asf/superset.git

commit ea37f5ed5aabb0fadcff7fd9b9317746b9951396
Author: David Aaron Suddjian <aa...@gmail.com>
AuthorDate: Thu Feb 18 01:31:37 2021 -0800

    refactor: introduce api resource hooks, fix error owners
---
 superset-frontend/src/chart/Chart.jsx              |  15 +-
 superset-frontend/src/chart/ChartErrorMessage.tsx  |  63 +++++++
 superset-frontend/src/common/hooks/apiResources.ts | 184 +++++++++++++++++++++
 .../dashboard/components/gridComponents/Chart.jsx  |   1 -
 .../src/explore/components/ExploreChartPanel.jsx   |   1 -
 5 files changed, 251 insertions(+), 13 deletions(-)

diff --git a/superset-frontend/src/chart/Chart.jsx b/superset-frontend/src/chart/Chart.jsx
index 3f1596c..8bda7a8 100644
--- a/superset-frontend/src/chart/Chart.jsx
+++ b/superset-frontend/src/chart/Chart.jsx
@@ -25,9 +25,9 @@ import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
 import { Logger, LOG_ACTIONS_RENDER_CHART } from '../logger/LogUtils';
 import Loading from '../components/Loading';
 import RefreshChartOverlay from '../components/RefreshChartOverlay';
-import ErrorMessageWithStackTrace from '../components/ErrorMessage/ErrorMessageWithStackTrace';
 import ErrorBoundary from '../components/ErrorBoundary';
 import ChartRenderer from './ChartRenderer';
+import { ChartErrorMessage } from './ChartErrorMessage';
 
 const propTypes = {
   annotationData: PropTypes.object,
@@ -49,9 +49,6 @@ const propTypes = {
   timeout: PropTypes.number,
   vizType: PropTypes.string.isRequired,
   triggerRender: PropTypes.bool,
-  owners: PropTypes.arrayOf(
-    PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
-  ),
   // state
   chartAlert: PropTypes.string,
   chartStatus: PropTypes.string,
@@ -152,17 +149,13 @@ class Chart extends React.PureComponent {
   }
 
   renderErrorMessage(queryResponse) {
-    const { chartAlert, chartStackTrace, dashboardId, owners } = this.props;
+    const { chartId, chartAlert, chartStackTrace, dashboardId } = this.props;
 
     const error = queryResponse?.errors?.[0];
-    if (error) {
-      const extra = error.extra || {};
-      extra.owners = owners;
-      error.extra = extra;
-    }
     const message = chartAlert || queryResponse?.message;
     return (
-      <ErrorMessageWithStackTrace
+      <ChartErrorMessage
+        chartId={chartId}
         error={error}
         subtitle={message}
         copyText={message}
diff --git a/superset-frontend/src/chart/ChartErrorMessage.tsx b/superset-frontend/src/chart/ChartErrorMessage.tsx
new file mode 100644
index 0000000..123d60f
--- /dev/null
+++ b/superset-frontend/src/chart/ChartErrorMessage.tsx
@@ -0,0 +1,63 @@
+/**
+ * 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 React from 'react';
+import {
+  useApiV1Resource,
+  useResourceTransform,
+} from 'src/common/hooks/apiResources';
+import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace';
+import { SupersetError } from 'src/components/ErrorMessage/types';
+import Chart from 'src/types/Chart';
+
+interface Props {
+  chartId: string;
+  error?: SupersetError;
+}
+
+function extractOwnerNames({ owners }: Chart) {
+  if (!owners) return null;
+  return owners.map(owner => `${owner.first_name} ${owner.last_name}`);
+}
+
+function useChartOwnerNames(chartId: string) {
+  const { result } = useResourceTransform(
+    useApiV1Resource<Chart>(`/api/v1/chart/${chartId}`),
+    extractOwnerNames,
+  );
+  return result;
+}
+
+/**
+ * fetches the chart owners and adds them to the extra data of the error message
+ */
+export const ChartErrorMessage: React.FC<Props> = ({
+  chartId,
+  error,
+  ...props
+}) => {
+  const owners = useChartOwnerNames(chartId);
+  // don't mutate props
+  const ownedError = error && {
+    ...error,
+    extra: { ...error.extra, owners },
+  };
+
+  return <ErrorMessageWithStackTrace {...props} error={ownedError} />;
+};
diff --git a/superset-frontend/src/common/hooks/apiResources.ts b/superset-frontend/src/common/hooks/apiResources.ts
new file mode 100644
index 0000000..5ae54a7
--- /dev/null
+++ b/superset-frontend/src/common/hooks/apiResources.ts
@@ -0,0 +1,184 @@
+/**
+ * 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 { makeApi } from '@superset-ui/core';
+import { useEffect, useMemo, useRef, useState } from 'react';
+
+export enum ResourceStatus {
+  LOADING = 'loading',
+  COMPLETE = 'complete',
+  ERROR = 'error',
+}
+
+/**
+ * An object containing the data fetched from the API,
+ * as well as loading and error info
+ */
+export type Resource<T> = LoadingState | CompleteState<T> | ErrorState;
+
+// Trying out something a little different: a separate type per status.
+// This should let Typescript know whether a Resource has a result or error.
+// It's possible that I'm expecting too much from Typescript here.
+// If this ends up causing problems, we can change the type to:
+//
+// export type Resource<T> = {
+//   status: ResourceStatus;
+//   result: null | T;
+//   error: null | Error;
+// }
+
+type LoadingState = {
+  status: ResourceStatus.LOADING;
+  result: null;
+  error: null;
+};
+
+type CompleteState<T> = {
+  status: ResourceStatus.COMPLETE;
+  result: T;
+  error: null;
+};
+
+type ErrorState = {
+  status: ResourceStatus.ERROR;
+  result: null;
+  error: Error;
+};
+
+const initialState: LoadingState = {
+  status: ResourceStatus.LOADING,
+  result: null,
+  error: null,
+};
+
+/**
+ * A general-purpose hook to fetch the response from an endpoint.
+ * Returns the full response body from the API, including metadata.
+ *
+ * Note: You likely want {useApiV1Resource} instead of this!
+ *
+ * TODO Store the state in redux or something, share state between hook instances.
+ *
+ * TODO Include a function in the returned resource object to refresh the data.
+ *
+ * A core design decision here is composition > configuration,
+ * and every hook should only have one job.
+ * Please address new needs with new hooks if possible,
+ * rather than adding config options to this hook.
+ *
+ * @param endpoint The url where the resource is located.
+ */
+export function useApiResourceFullBody<RESULT>(
+  endpoint: string,
+): Resource<RESULT> {
+  const [resource, setResource] = useState<Resource<RESULT>>(initialState);
+  const cancelRef = useRef<() => void>(() => {});
+
+  useEffect(() => {
+    // If refresh is implemented, this will need to change.
+    // The state will need to keep
+    setResource(initialState);
+
+    // when this effect runs, the endpoint function has changed.
+    // cancel any current calls so that state doesn't get messed up.
+    cancelRef.current();
+    let cancelled = false;
+    cancelRef.current = () => {
+      cancelled = true;
+    };
+
+    const fetchResource = makeApi<{}, RESULT>({
+      method: 'GET',
+      endpoint,
+    });
+
+    // pass {} to fetchResource, only because makeApi requires it.
+    // we should change makeApi, and then remove that param.
+    // TODO support request cancellation in makeApi
+    fetchResource({})
+      .then(result => {
+        if (!cancelled) {
+          setResource({
+            status: ResourceStatus.COMPLETE,
+            result,
+            error: null,
+          });
+        }
+      })
+      .catch(error => {
+        if (!cancelled) {
+          setResource({
+            status: ResourceStatus.ERROR,
+            result: null,
+            error,
+          });
+        }
+      });
+
+    // Cancel the request when the component un-mounts
+    return () => {
+      cancelled = true;
+    };
+  }, [endpoint]);
+
+  return resource;
+}
+
+/**
+ * For when you want to transform the result of an api resource hook before using it.
+ *
+ * @param resource the Resource object returned from useApiV1Resource
+ * @param transformFn a callback that transforms the result object into the shape you want.
+ * Make sure to use a persistent function for this so it doesn't constantly recalculate!
+ */
+export function useResourceTransform<IN, OUT>(
+  resource: Resource<IN>,
+  transformFn: (result: IN) => OUT,
+): Resource<OUT> {
+  return useMemo(() => {
+    if (resource.status !== ResourceStatus.COMPLETE) {
+      // While incomplete, there is no result - no need to transform.
+      return resource;
+    }
+    return {
+      ...resource,
+      result: transformFn(resource.result),
+    };
+  }, [resource, transformFn]);
+}
+
+// returns the result from fetching an api resource
+const innerResult = <T>(responseBody: { result: T }) => responseBody.result;
+
+/**
+ * A general-purpose hook to fetch a Superset resource from a v1 API endpoint.
+ * Handles request lifecycle and async logic so you don't have to.
+ *
+ * This returns the data under the "result" field in the API response body.
+ * If you need the full response body, use {useFullApiResource} instead.
+ *
+ * @param endpoint The url where the resource is located.
+ */
+export function useApiV1Resource<RESULT>(endpoint: string): Resource<RESULT> {
+  // get the api "result" field within the resource result
+  return useResourceTransform(
+    useApiResourceFullBody<{ result: RESULT }>(endpoint),
+    innerResult,
+  );
+}
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx
index 7dd01e9..b3c7975 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx
@@ -361,7 +361,6 @@ export default class Chart extends React.Component {
             timeout={timeout}
             triggerQuery={chart.triggerQuery}
             vizType={slice.viz_type}
-            owners={slice.owners}
           />
         </div>
       </div>
diff --git a/superset-frontend/src/explore/components/ExploreChartPanel.jsx b/superset-frontend/src/explore/components/ExploreChartPanel.jsx
index 6962dfa..813b6a9 100644
--- a/superset-frontend/src/explore/components/ExploreChartPanel.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartPanel.jsx
@@ -198,7 +198,6 @@ const ExploreChartPanel = props => {
           errorMessage={props.errorMessage}
           formData={props.form_data}
           onQuery={props.onQuery}
-          owners={props?.slice?.owners}
           queriesResponse={chart.queriesResponse}
           refreshOverlayVisible={props.refreshOverlayVisible}
           setControlValue={props.actions.setControlValue}