You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ta...@apache.org on 2020/11/03 05:29:22 UTC

[incubator-superset] branch master updated: refactor: reduce number of api calls needed to fetch favorite status for charts and dashboards (#11502)

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

tai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new edb9619  refactor: reduce number of api calls needed to fetch favorite status for charts and dashboards (#11502)
edb9619 is described below

commit edb9619731658151ed864321ede06980d767501d
Author: ʈᵃᵢ <td...@gmail.com>
AuthorDate: Mon Nov 2 21:26:14 2020 -0800

    refactor: reduce number of api calls needed to fetch favorite status for charts and dashboards (#11502)
---
 superset-frontend/src/components/FaveStar.tsx      |   6 +-
 .../src/components/ListViewCard/index.tsx          |  53 +++++-----
 .../src/views/CRUD/chart/ChartCard.tsx             |  17 ++--
 .../src/views/CRUD/chart/ChartList.tsx             |  33 +++---
 .../src/views/CRUD/dashboard/DashboardCard.tsx     |  31 +++---
 .../src/views/CRUD/dashboard/DashboardList.tsx     |  49 +++++----
 superset-frontend/src/views/CRUD/hooks.ts          | 113 +++++++++++++--------
 superset-frontend/src/views/CRUD/types.ts          |  11 --
 superset-frontend/src/views/CRUD/utils.tsx         |   2 +-
 .../src/views/CRUD/welcome/ActivityTable.tsx       |   2 +-
 .../src/views/CRUD/welcome/ChartTable.tsx          |  16 ++-
 .../src/views/CRUD/welcome/DashboardTable.tsx      |  33 +++---
 .../src/views/CRUD/welcome/SavedQueries.tsx        |   3 +-
 superset/charts/api.py                             |  50 ++++++++-
 superset/charts/dao.py                             |  15 +++
 superset/charts/schemas.py                         |  15 +++
 superset/dashboards/api.py                         |  52 ++++++++++
 superset/dashboards/dao.py                         |  17 ++++
 superset/dashboards/schemas.py                     |  13 +++
 superset/models/core.py                            |   6 ++
 tests/charts/api_tests.py                          |  31 +++++-
 tests/dashboards/api_tests.py                      |  31 +++++-
 22 files changed, 422 insertions(+), 177 deletions(-)

diff --git a/superset-frontend/src/components/FaveStar.tsx b/superset-frontend/src/components/FaveStar.tsx
index 378db09..aeb4885 100644
--- a/superset-frontend/src/components/FaveStar.tsx
+++ b/superset-frontend/src/components/FaveStar.tsx
@@ -23,7 +23,7 @@ import Icon from './Icon';
 
 interface FaveStarProps {
   itemId: number;
-  fetchFaveStar(id: number): any;
+  fetchFaveStar?: (id: number) => void;
   saveFaveStar(id: number, isStarred: boolean): any;
   isStarred: boolean;
   showTooltip?: boolean;
@@ -35,7 +35,9 @@ const StyledLink = styled.a`
 
 export default class FaveStar extends React.PureComponent<FaveStarProps> {
   componentDidMount() {
-    this.props.fetchFaveStar(this.props.itemId);
+    if (this.props.fetchFaveStar) {
+      this.props.fetchFaveStar(this.props.itemId);
+    }
   }
 
   onClick = (e: React.MouseEvent) => {
diff --git a/superset-frontend/src/components/ListViewCard/index.tsx b/superset-frontend/src/components/ListViewCard/index.tsx
index 8849b4b..b63f3af 100644
--- a/superset-frontend/src/components/ListViewCard/index.tsx
+++ b/superset-frontend/src/components/ListViewCard/index.tsx
@@ -152,11 +152,9 @@ interface CardProps {
   coverLeft?: React.ReactNode;
   coverRight?: React.ReactNode;
   actions: React.ReactNode | null;
-  showImg?: boolean;
   rows?: number | string;
   avatar?: string;
-  isRecent?: boolean;
-  renderCover?: React.ReactNode | null;
+  cover?: React.ReactNode | null;
 }
 
 function ListViewCard({
@@ -167,42 +165,39 @@ function ListViewCard({
   imgFallbackURL,
   description,
   coverLeft,
-  isRecent,
   coverRight,
   actions,
   avatar,
   loading,
   imgPosition = 'top',
-  renderCover,
+  cover,
 }: CardProps) {
   return (
     <StyledCard
       data-test="styled-card"
       cover={
-        !isRecent
-          ? renderCover || (
-              <Cover>
-                <a href={url}>
-                  <div className="gradient-container">
-                    <ImageLoader
-                      src={imgURL || ''}
-                      fallback={imgFallbackURL || ''}
-                      isLoading={loading}
-                      position={imgPosition}
-                    />
-                  </div>
-                </a>
-                <CoverFooter className="cover-footer">
-                  {!loading && coverLeft && (
-                    <CoverFooterLeft>{coverLeft}</CoverFooterLeft>
-                  )}
-                  {!loading && coverRight && (
-                    <CoverFooterRight>{coverRight}</CoverFooterRight>
-                  )}
-                </CoverFooter>
-              </Cover>
-            )
-          : null
+        cover || (
+          <Cover>
+            <a href={url}>
+              <div className="gradient-container">
+                <ImageLoader
+                  src={imgURL || ''}
+                  fallback={imgFallbackURL || ''}
+                  isLoading={loading}
+                  position={imgPosition}
+                />
+              </div>
+            </a>
+            <CoverFooter className="cover-footer">
+              {!loading && coverLeft && (
+                <CoverFooterLeft>{coverLeft}</CoverFooterLeft>
+              )}
+              {!loading && coverRight && (
+                <CoverFooterRight>{coverRight}</CoverFooterRight>
+              )}
+            </CoverFooter>
+          </Cover>
+        )
       }
     >
       {loading && (
diff --git a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx
index 5dd6bd8..3536e5a 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx
@@ -17,7 +17,6 @@
  * under the License.
  */
 import React from 'react';
-import { useFavoriteStatus } from 'src/views/CRUD/hooks';
 import { t } from '@superset-ui/core';
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import Icon from 'src/components/Icon';
@@ -30,8 +29,6 @@ import FaveStar from 'src/components/FaveStar';
 import FacePile from 'src/components/FacePile';
 import { handleChartDelete } from '../utils';
 
-const FAVESTAR_BASE_URL = '/superset/favstar/slice';
-
 interface ChartCardProps {
   chart: Chart;
   hasPerm: (perm: string) => boolean;
@@ -41,6 +38,8 @@ interface ChartCardProps {
   addSuccessToast: (msg: string) => void;
   refreshData: () => void;
   loading: boolean;
+  saveFavoriteStatus: (id: number, isStarred: boolean) => void;
+  favoriteStatus: boolean;
 }
 
 export default function ChartCard({
@@ -52,14 +51,11 @@ export default function ChartCard({
   addSuccessToast,
   refreshData,
   loading,
+  saveFavoriteStatus,
+  favoriteStatus,
 }: ChartCardProps) {
   const canEdit = hasPerm('can_edit');
   const canDelete = hasPerm('can_delete');
-  const [, fetchFaveStar, saveFaveStar, favoriteStatus] = useFavoriteStatus(
-    {},
-    FAVESTAR_BASE_URL,
-    addDangerToast,
-  );
 
   const menu = (
     <Menu>
@@ -124,9 +120,8 @@ export default function ChartCard({
         <ListViewCard.Actions>
           <FaveStar
             itemId={chart.id}
-            fetchFaveStar={fetchFaveStar}
-            saveFaveStar={saveFaveStar}
-            isStarred={!!favoriteStatus[chart.id]}
+            saveFaveStar={saveFavoriteStatus}
+            isStarred={favoriteStatus}
           />
           <Dropdown overlay={menu}>
             <Icon name="more-horiz" />
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index 91cb458..ed9953a 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -47,7 +47,6 @@ import TooltipWrapper from 'src/components/TooltipWrapper';
 import ChartCard from './ChartCard';
 
 const PAGE_SIZE = 25;
-const FAVESTAR_BASE_URL = '/superset/favstar/slice';
 
 const createFetchDatasets = (handleError: (err: Response) => void) => async (
   filterValue = '',
@@ -105,9 +104,12 @@ function ChartList(props: ChartListProps) {
     toggleBulkSelect,
     refreshData,
   } = useListViewResource<Chart>('chart', t('chart'), props.addDangerToast);
-  const [favoriteStatusRef, fetchFaveStar, saveFaveStar] = useFavoriteStatus(
-    {},
-    FAVESTAR_BASE_URL,
+
+  const chartIds = useMemo(() => charts.map(c => c.id), [charts]);
+
+  const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus(
+    'chart',
+    chartIds,
     props.addDangerToast,
   );
   const {
@@ -140,17 +142,6 @@ function ChartList(props: ChartListProps) {
     );
   }
 
-  function renderFaveStar(id: number) {
-    return (
-      <FaveStar
-        itemId={id}
-        fetchFaveStar={fetchFaveStar}
-        saveFaveStar={saveFaveStar}
-        isStarred={!!favoriteStatusRef.current[id]}
-      />
-    );
-  }
-
   const columns = useMemo(
     () => [
       {
@@ -158,7 +149,13 @@ function ChartList(props: ChartListProps) {
           row: {
             original: { id },
           },
-        }: any) => renderFaveStar(id),
+        }: any) => (
+          <FaveStar
+            itemId={id}
+            saveFaveStar={saveFavoriteStatus}
+            isStarred={favoriteStatus[id]}
+          />
+        ),
         Header: '',
         id: 'favorite',
         disableSortBy: true,
@@ -303,7 +300,7 @@ function ChartList(props: ChartListProps) {
         hidden: !canEdit && !canDelete,
       },
     ],
-    [canEdit, canDelete],
+    [canEdit, canDelete, favoriteStatus],
   );
 
   const filters: Filters = [
@@ -415,6 +412,8 @@ function ChartList(props: ChartListProps) {
         addSuccessToast={props.addSuccessToast}
         refreshData={refreshData}
         loading={loading}
+        favoriteStatus={favoriteStatus[chart.id]}
+        saveFavoriteStatus={saveFavoriteStatus}
       />
     );
   }
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx
index f8713d6..b19c726 100644
--- a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx
@@ -29,11 +29,21 @@ import Icon from 'src/components/Icon';
 import Label from 'src/components/Label';
 import FacePile from 'src/components/FacePile';
 import FaveStar from 'src/components/FaveStar';
-import { DashboardCardProps } from 'src/views/CRUD/types';
+import { Dashboard } from 'src/views/CRUD/types';
 
-import { useFavoriteStatus } from 'src/views/CRUD/hooks';
-
-const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
+export interface DashboardCardProps {
+  isChart?: boolean;
+  dashboard: Dashboard;
+  hasPerm: (name: string) => boolean;
+  bulkSelectEnabled: boolean;
+  refreshData: () => void;
+  loading: boolean;
+  addDangerToast: (msg: string) => void;
+  addSuccessToast: (msg: string) => void;
+  openDashboardEditModal?: (d: Dashboard) => void;
+  saveFavoriteStatus: (id: number, isStarred: boolean) => void;
+  favoriteStatus: boolean;
+}
 
 function DashboardCard({
   dashboard,
@@ -43,15 +53,12 @@ function DashboardCard({
   addDangerToast,
   addSuccessToast,
   openDashboardEditModal,
+  favoriteStatus,
+  saveFavoriteStatus,
 }: DashboardCardProps) {
   const canEdit = hasPerm('can_edit');
   const canDelete = hasPerm('can_delete');
   const canExport = hasPerm('can_mulexport');
-  const [, fetchFaveStar, saveFaveStar, favoriteStatus] = useFavoriteStatus(
-    {},
-    FAVESTAR_BASE_URL,
-    addDangerToast,
-  );
 
   const menu = (
     <Menu>
@@ -123,16 +130,14 @@ function DashboardCard({
         <ListViewCard.Actions>
           <FaveStar
             itemId={dashboard.id}
-            fetchFaveStar={fetchFaveStar}
-            saveFaveStar={saveFaveStar}
-            isStarred={!!favoriteStatus[dashboard.id]}
+            saveFaveStar={saveFavoriteStatus}
+            isStarred={favoriteStatus}
           />
           <Dropdown overlay={menu}>
             <Icon name="more-horiz" />
           </Dropdown>
         </ListViewCard.Actions>
       }
-      showImg
     />
   );
 }
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
index d8acca3..7e2e6ae 100644
--- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
@@ -42,7 +42,6 @@ import Dashboard from 'src/dashboard/containers/Dashboard';
 import DashboardCard from './DashboardCard';
 
 const PAGE_SIZE = 25;
-const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
 
 interface DashboardListProps {
   addDangerToast: (msg: string) => void;
@@ -81,9 +80,11 @@ function DashboardList(props: DashboardListProps) {
     t('dashboard'),
     props.addDangerToast,
   );
-  const [favoriteStatusRef, fetchFaveStar, saveFaveStar] = useFavoriteStatus(
-    {},
-    FAVESTAR_BASE_URL,
+
+  const dashboardIds = useMemo(() => dashboards.map(d => d.id), [dashboards]);
+  const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus(
+    'dashboard',
+    dashboardIds,
     props.addDangerToast,
   );
   const [dashboardToEdit, setDashboardToEdit] = useState<Dashboard | null>(
@@ -140,17 +141,6 @@ function DashboardList(props: DashboardListProps) {
     );
   }
 
-  function renderFaveStar(id: number) {
-    return (
-      <FaveStar
-        itemId={id}
-        fetchFaveStar={fetchFaveStar}
-        saveFaveStar={saveFaveStar}
-        isStarred={!!favoriteStatusRef.current[id]}
-      />
-    );
-  }
-
   const columns = useMemo(
     () => [
       {
@@ -158,7 +148,13 @@ function DashboardList(props: DashboardListProps) {
           row: {
             original: { id },
           },
-        }: any) => renderFaveStar(id),
+        }: any) => (
+          <FaveStar
+            itemId={id}
+            saveFaveStar={saveFavoriteStatus}
+            isStarred={favoriteStatus[id]}
+          />
+        ),
         Header: '',
         id: 'favorite',
         disableSortBy: true,
@@ -317,7 +313,7 @@ function DashboardList(props: DashboardListProps) {
         disableSortBy: true,
       },
     ],
-    [canEdit, canDelete, canExport, favoriteStatusRef],
+    [canEdit, canDelete, canExport, favoriteStatus],
   );
 
   const filters: Filters = [
@@ -404,15 +400,16 @@ function DashboardList(props: DashboardListProps) {
   function renderCard(dashboard: Dashboard) {
     return (
       <DashboardCard
-        {...{
-          dashboard,
-          hasPerm,
-          bulkSelectEnabled,
-          refreshData,
-          addDangerToast: props.addDangerToast,
-          addSuccessToast: props.addSuccessToast,
-          openDashboardEditModal,
-        }}
+        dashboard={dashboard}
+        hasPerm={hasPerm}
+        bulkSelectEnabled={bulkSelectEnabled}
+        refreshData={refreshData}
+        loading={loading}
+        addDangerToast={props.addDangerToast}
+        addSuccessToast={props.addSuccessToast}
+        openDashboardEditModal={openDashboardEditModal}
+        saveFavoriteStatus={saveFavoriteStatus}
+        favoriteStatus={favoriteStatus[dashboard.id]}
       />
     );
   }
diff --git a/superset-frontend/src/views/CRUD/hooks.ts b/superset-frontend/src/views/CRUD/hooks.ts
index b083bde..2373a54 100644
--- a/superset-frontend/src/views/CRUD/hooks.ts
+++ b/superset-frontend/src/views/CRUD/hooks.ts
@@ -17,8 +17,8 @@
  * under the License.
  */
 import rison from 'rison';
-import { useState, useEffect, useCallback, useRef } from 'react';
-import { SupersetClient, t } from '@superset-ui/core';
+import { useState, useEffect, useCallback } from 'react';
+import { makeApi, SupersetClient, t } from '@superset-ui/core';
 
 import { createErrorHandler } from 'src/views/CRUD/utils';
 import { FetchDataConfig } from 'src/components/ListView';
@@ -58,7 +58,9 @@ export function useListViewResource<D extends object = any>(
   }
 
   useEffect(() => {
-    const infoParam = infoEnable ? '_info?q=(keys:!(permissions))' : '';
+    const infoParam = infoEnable
+      ? `_info?q=${rison.encode({ keys: ['permissions'] })}`
+      : '';
     SupersetClient.get({
       endpoint: `/api/v1/${resource}/${infoParam}`,
     }).then(
@@ -299,32 +301,52 @@ export function useSingleViewResource<D extends object = any>(
   };
 }
 
-// the hooks api has some known limitations around stale state in closures.
-// See https://github.com/reactjs/rfcs/blob/master/text/0068-react-hooks.md#drawbacks
-// the useRef hook is a way of getting around these limitations by having a consistent ref
-// that points to the most recent value.
+enum FavStarClassName {
+  CHART = 'slice',
+  DASHBOARD = 'Dashboard',
+}
+
+type FavoriteStatusResponse = {
+  result: Array<{
+    id: string;
+    value: boolean;
+  }>;
+};
+
+const favoriteApis = {
+  chart: makeApi<string, FavoriteStatusResponse>({
+    requestType: 'search',
+    method: 'GET',
+    endpoint: '/api/v1/chart/favorite_status',
+  }),
+  dashboard: makeApi<string, FavoriteStatusResponse>({
+    requestType: 'search',
+    method: 'GET',
+    endpoint: '/api/v1/dashboard/favorite_status',
+  }),
+};
+
 export function useFavoriteStatus(
-  initialState: FavoriteStatus,
-  baseURL: string,
+  type: 'chart' | 'dashboard',
+  ids: Array<string | number>,
   handleErrorMsg: (message: string) => void,
 ) {
-  const [favoriteStatus, setFavoriteStatus] = useState<FavoriteStatus>(
-    initialState,
-  );
-  const favoriteStatusRef = useRef<FavoriteStatus>(favoriteStatus);
-  useEffect(() => {
-    favoriteStatusRef.current = favoriteStatus;
-  });
+  const [favoriteStatus, setFavoriteStatus] = useState<FavoriteStatus>({});
 
   const updateFavoriteStatus = (update: FavoriteStatus) =>
     setFavoriteStatus(currentState => ({ ...currentState, ...update }));
 
-  const fetchFaveStar = (id: number) => {
-    SupersetClient.get({
-      endpoint: `${baseURL}/${id}/count/`,
-    }).then(
-      ({ json }) => {
-        updateFavoriteStatus({ [id]: json.count > 0 });
+  useEffect(() => {
+    if (!ids.length) {
+      return;
+    }
+    favoriteApis[type](`q=${rison.encode(ids)}`).then(
+      ({ result }) => {
+        const update = result.reduce((acc, element) => {
+          acc[element.id] = element.value;
+          return acc;
+        }, {});
+        updateFavoriteStatus(update);
       },
       createErrorHandler(errMsg =>
         handleErrorMsg(
@@ -332,31 +354,32 @@ export function useFavoriteStatus(
         ),
       ),
     );
-  };
-
-  const saveFaveStar = (id: number, isStarred: boolean) => {
-    const urlSuffix = isStarred ? 'unselect' : 'select';
-
-    SupersetClient.get({
-      endpoint: `${baseURL}/${id}/${urlSuffix}/`,
-    }).then(
-      () => {
-        updateFavoriteStatus({ [id]: !isStarred });
-      },
-      createErrorHandler(errMsg =>
-        handleErrorMsg(
-          t('There was an error saving the favorite status: %s', errMsg),
+  }, [ids]);
+
+  const saveFaveStar = useCallback(
+    (id: number, isStarred: boolean) => {
+      const urlSuffix = isStarred ? 'unselect' : 'select';
+      SupersetClient.get({
+        endpoint: `/superset/favstar/${
+          type === 'chart' ? FavStarClassName.CHART : FavStarClassName.DASHBOARD
+        }/${id}/${urlSuffix}/`,
+      }).then(
+        ({ json }) => {
+          updateFavoriteStatus({
+            [id]: (json as { count: number })?.count > 0,
+          });
+        },
+        createErrorHandler(errMsg =>
+          handleErrorMsg(
+            t('There was an error saving the favorite status: %s', errMsg),
+          ),
         ),
-      ),
-    );
-  };
+      );
+    },
+    [type],
+  );
 
-  return [
-    favoriteStatusRef,
-    fetchFaveStar,
-    saveFaveStar,
-    favoriteStatus,
-  ] as const;
+  return [saveFaveStar, favoriteStatus] as const;
 }
 
 export const useChartEditModal = (
diff --git a/superset-frontend/src/views/CRUD/types.ts b/superset-frontend/src/views/CRUD/types.ts
index 0946247..39d0c81 100644
--- a/superset-frontend/src/views/CRUD/types.ts
+++ b/superset-frontend/src/views/CRUD/types.ts
@@ -45,17 +45,6 @@ export interface Dashboard {
   loading?: boolean;
 }
 
-export interface DashboardCardProps {
-  isChart?: boolean;
-  dashboard: Dashboard;
-  hasPerm: (name: string) => boolean;
-  bulkSelectEnabled: boolean;
-  refreshData: () => void;
-  addDangerToast: (msg: string) => void;
-  addSuccessToast: (msg: string) => void;
-  openDashboardEditModal?: (d: Dashboard) => void;
-}
-
 export type SavedQueryObject = {
   database: {
     database_name: string;
diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx
index ee7cbeb..1fc457f 100644
--- a/superset-frontend/src/views/CRUD/utils.tsx
+++ b/superset-frontend/src/views/CRUD/utils.tsx
@@ -60,7 +60,7 @@ const createFetchResourceMethod = (method: string) => (
 export const getRecentAcitivtyObjs = (
   userId: string | number,
   recent: string,
-  addDangerToast: (arg0: string, arg1: string) => void,
+  addDangerToast: (arg1: string, arg2: any) => any,
 ) => {
   const getParams = (filters?: Array<any>) => {
     const params = {
diff --git a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
index ecaddf3..6a3387c 100644
--- a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
@@ -177,8 +177,8 @@ export default function ActivityTable({ user }: ActivityProps) {
     return activityData[activeChild].map((e: ActivityObjects) => (
       <ListViewCard
         key={`${e.id}`}
-        isRecent
         loading={loading}
+        cover={<></>}
         url={e.sql ? `/supserset/sqllab?queryId=${e.id}` : e.url}
         title={getFilterTitle(e)}
         description={`Last Edited: ${moment(e.changed_on_utc).format(
diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
index 9a12dfd..feca46e 100644
--- a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
@@ -16,9 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useMemo } from 'react';
 import { t } from '@superset-ui/core';
-import { useListViewResource, useChartEditModal } from 'src/views/CRUD/hooks';
+import {
+  useListViewResource,
+  useChartEditModal,
+  useFavoriteStatus,
+} from 'src/views/CRUD/hooks';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
 import PropertiesModal from 'src/explore/components/PropertiesModal';
 import { User } from 'src/types/bootstrapTypes';
@@ -51,6 +55,12 @@ function ChartTable({
     refreshData,
     fetchData,
   } = useListViewResource<Chart>('chart', t('chart'), addDangerToast);
+  const chartIds = useMemo(() => charts.map(c => c.id), [charts]);
+  const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus(
+    'chart',
+    chartIds,
+    addDangerToast,
+  );
   const {
     sliceCurrentlyEditing,
     openChartEditModal,
@@ -154,6 +164,8 @@ function ChartTable({
               refreshData={refreshData}
               addDangerToast={addDangerToast}
               addSuccessToast={addSuccessToast}
+              favoriteStatus={favoriteStatus[e.id]}
+              saveFavoriteStatus={saveFavoriteStatus}
             />
           ))}
         </CardContainer>
diff --git a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
index 67536cd..8643fc0 100644
--- a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
@@ -16,9 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useMemo } from 'react';
 import { SupersetClient, t } from '@superset-ui/core';
-import { useListViewResource } from 'src/views/CRUD/hooks';
+import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
 import { Dashboard, DashboardTableProps } from 'src/views/CRUD/types';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
 import PropertiesModal from 'src/dashboard/components/PropertiesModal';
@@ -42,7 +42,7 @@ function DashboardTable({
   addSuccessToast,
 }: DashboardTableProps) {
   const {
-    state: { loading, resourceCollection: dashboards, bulkSelectEnabled },
+    state: { loading, resourceCollection: dashboards },
     setResourceCollection: setDashboards,
     hasPerm,
     refreshData,
@@ -52,7 +52,12 @@ function DashboardTable({
     t('dashboard'),
     addDangerToast,
   );
-
+  const dashboardIds = useMemo(() => dashboards.map(c => c.id), [dashboards]);
+  const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus(
+    'dashboard',
+    dashboardIds,
+    addDangerToast,
+  );
   const [editModal, setEditModal] = useState<Dashboard>();
   const [dashboardFilter, setDashboardFilter] = useState('Mine');
 
@@ -168,16 +173,16 @@ function DashboardTable({
         <CardContainer>
           {dashboards.map(e => (
             <DashboardCard
-              {...{
-                dashboard: e,
-                hasPerm,
-                bulkSelectEnabled,
-                refreshData,
-                addDangerToast,
-                addSuccessToast,
-                loading,
-                openDashboardEditModal: dashboard => setEditModal(dashboard),
-              }}
+              dashboard={e}
+              hasPerm={hasPerm}
+              bulkSelectEnabled={false}
+              refreshData={refreshData}
+              addDangerToast={addDangerToast}
+              addSuccessToast={addSuccessToast}
+              loading={loading}
+              openDashboardEditModal={dashboard => setEditModal(dashboard)}
+              saveFavoriteStatus={saveFavoriteStatus}
+              favoriteStatus={favoriteStatus[e.id]}
             />
           ))}
         </CardContainer>
diff --git a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx
index dc0590b..f7b6509 100644
--- a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx
@@ -227,8 +227,7 @@ const SavedQueries = ({
               rows={q.rows}
               loading={loading}
               description={t('Last run ', q.end_time)}
-              showImg={false}
-              renderCover={
+              cover={
                 <QueryData>
                   <div className="holder">
                     <div className="title">{t('Tables')}</div>
diff --git a/superset/charts/api.py b/superset/charts/api.py
index 3ab4e4a..94dd375 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -45,6 +45,7 @@ from superset.charts.commands.exceptions import (
 )
 from superset.charts.commands.export import ExportChartsCommand
 from superset.charts.commands.update import UpdateChartCommand
+from superset.charts.dao import ChartDAO
 from superset.charts.filters import ChartAllTextFilter, ChartFavoriteFilter, ChartFilter
 from superset.charts.schemas import (
     CHART_SCHEMAS,
@@ -53,6 +54,7 @@ from superset.charts.schemas import (
     ChartPutSchema,
     get_delete_ids_schema,
     get_export_ids_schema,
+    get_fav_star_ids_schema,
     openapi_spec_methods_override,
     screenshot_query_schema,
     thumbnail_query_schema,
@@ -87,7 +89,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
         RouteMethod.RELATED,
         "bulk_delete",  # not using RouteMethod since locally defined
         "data",
-        "viz_types",
+        "favorite_status",
     }
     class_permission_name = "SliceModelView"
     show_columns = [
@@ -176,6 +178,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
         "screenshot_query_schema": screenshot_query_schema,
         "get_delete_ids_schema": get_delete_ids_schema,
         "get_export_ids_schema": get_export_ids_schema,
+        "get_fav_star_ids_schema": get_fav_star_ids_schema,
     }
     """ Add extra schemas to the OpenAPI components schema section """
     openapi_spec_methods = openapi_spec_methods_override
@@ -773,3 +776,48 @@ class ChartRestApi(BaseSupersetModelRestApi):
             as_attachment=True,
             attachment_filename=filename,
         )
+
+    @expose("/favorite_status/", methods=["GET"])
+    @protect()
+    @safe
+    @statsd_metrics
+    @rison(get_fav_star_ids_schema)
+    def favorite_status(self, **kwargs: Any) -> Response:
+        """Favorite stars for Charts
+        ---
+        get:
+          description: >-
+            Check favorited dashboards for current user
+          parameters:
+          - in: query
+            name: q
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/get_fav_star_ids_schema'
+          responses:
+            200:
+              description:
+              content:
+                application/json:
+                  schema:
+                    $ref: "#/components/schemas/GetFavStarIdsSchema"
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        requested_ids = kwargs["rison"]
+        charts = ChartDAO.find_by_ids(requested_ids)
+        if not charts:
+            return self.response_404()
+        favorited_chart_ids = ChartDAO.favorited_ids(charts, g.user.id)
+        res = [
+            {"id": request_id, "value": request_id in favorited_chart_ids}
+            for request_id in requested_ids
+        ]
+        return self.response(200, result=res)
diff --git a/superset/charts/dao.py b/superset/charts/dao.py
index cb59868..2a80f82 100644
--- a/superset/charts/dao.py
+++ b/superset/charts/dao.py
@@ -22,6 +22,7 @@ from sqlalchemy.exc import SQLAlchemyError
 from superset.charts.filters import ChartFilter
 from superset.dao.base import BaseDAO
 from superset.extensions import db
+from superset.models.core import FavStar, FavStarClassName
 from superset.models.slice import Slice
 
 if TYPE_CHECKING:
@@ -66,3 +67,17 @@ class ChartDAO(BaseDAO):
         db.session.merge(slc)
         if commit:
             db.session.commit()
+
+    @staticmethod
+    def favorited_ids(charts: List[Slice], current_user_id: int) -> List[FavStar]:
+        ids = [chart.id for chart in charts]
+        return [
+            star.obj_id
+            for star in db.session.query(FavStar.obj_id)
+            .filter(
+                FavStar.class_name == FavStarClassName.CHART,
+                FavStar.obj_id.in_(ids),
+                FavStar.user_id == current_user_id,
+            )
+            .all()
+        ]
diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py
index 60d468a..026ad13 100644
--- a/superset/charts/schemas.py
+++ b/superset/charts/schemas.py
@@ -47,6 +47,8 @@ screenshot_query_schema = {
 }
 get_export_ids_schema = {"type": "array", "items": {"type": "integer"}}
 
+get_fav_star_ids_schema = {"type": "array", "items": {"type": "integer"}}
+
 #
 # Column schema descriptions
 #
@@ -1031,6 +1033,18 @@ class ChartDataResponseSchema(Schema):
     )
 
 
+class ChartFavStarResponseResult(Schema):
+    id = fields.Integer(description="The Chart id")
+    value = fields.Boolean(description="The FaveStar value")
+
+
+class GetFavStarIdsSchema(Schema):
+    result = fields.List(
+        fields.Nested(ChartFavStarResponseResult),
+        description="A list of results for each corresponding chart in the request",
+    )
+
+
 CHART_SCHEMAS = (
     ChartDataQueryContextSchema,
     ChartDataResponseSchema,
@@ -1049,4 +1063,5 @@ CHART_SCHEMAS = (
     ChartDataGeodeticParseOptionsSchema,
     ChartGetDatasourceResponseSchema,
     ChartCacheScreenshotResponseSchema,
+    GetFavStarIdsSchema,
 )
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index cba494a..815bfd1 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -44,6 +44,7 @@ from superset.dashboards.commands.exceptions import (
 )
 from superset.dashboards.commands.export import ExportDashboardsCommand
 from superset.dashboards.commands.update import UpdateDashboardCommand
+from superset.dashboards.dao import DashboardDAO
 from superset.dashboards.filters import (
     DashboardFavoriteFilter,
     DashboardFilter,
@@ -54,6 +55,8 @@ from superset.dashboards.schemas import (
     DashboardPutSchema,
     get_delete_ids_schema,
     get_export_ids_schema,
+    get_fav_star_ids_schema,
+    GetFavStarIdsSchema,
     openapi_spec_methods_override,
     thumbnail_query_schema,
 )
@@ -78,6 +81,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
         RouteMethod.EXPORT,
         RouteMethod.RELATED,
         "bulk_delete",  # not using RouteMethod since locally defined
+        "favorite_status",
     }
     resource_name = "dashboard"
     allow_browser_login = True
@@ -181,10 +185,13 @@ class DashboardRestApi(BaseSupersetModelRestApi):
     allowed_rel_fields = {"owners", "created_by"}
 
     openapi_spec_tag = "Dashboards"
+    """ Override the name set for this collection of endpoints """
+    openapi_spec_component_schemas = (GetFavStarIdsSchema,)
     apispec_parameter_schemas = {
         "get_delete_ids_schema": get_delete_ids_schema,
         "get_export_ids_schema": get_export_ids_schema,
         "thumbnail_query_schema": thumbnail_query_schema,
+        "get_fav_star_ids_schema": get_fav_star_ids_schema,
     }
     openapi_spec_methods = openapi_spec_methods_override
     """ Overrides GET methods OpenApi descriptions """
@@ -589,3 +596,48 @@ class DashboardRestApi(BaseSupersetModelRestApi):
         return Response(
             FileWrapper(screenshot), mimetype="image/png", direct_passthrough=True
         )
+
+    @expose("/favorite_status/", methods=["GET"])
+    @protect()
+    @safe
+    @statsd_metrics
+    @rison(get_fav_star_ids_schema)
+    def favorite_status(self, **kwargs: Any) -> Response:
+        """Favorite Stars for Dashboards
+        ---
+        get:
+          description: >-
+            Check favorited dashboards for current user
+          parameters:
+          - in: query
+            name: q
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/get_fav_star_ids_schema'
+          responses:
+            200:
+              description:
+              content:
+                application/json:
+                  schema:
+                    $ref: "#/components/schemas/GetFavStarIdsSchema"
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        requested_ids = kwargs["rison"]
+        dashboards = DashboardDAO.find_by_ids(requested_ids)
+        if not dashboards:
+            return self.response_404()
+        favorited_dashboard_ids = DashboardDAO.favorited_ids(dashboards, g.user.id)
+        res = [
+            {"id": request_id, "value": request_id in favorited_dashboard_ids}
+            for request_id in requested_ids
+        ]
+        return self.response(200, result=res)
diff --git a/superset/dashboards/dao.py b/superset/dashboards/dao.py
index 35db331..65bdc69 100644
--- a/superset/dashboards/dao.py
+++ b/superset/dashboards/dao.py
@@ -23,6 +23,7 @@ from sqlalchemy.exc import SQLAlchemyError
 from superset.dao.base import BaseDAO
 from superset.dashboards.filters import DashboardFilter
 from superset.extensions import db
+from superset.models.core import FavStar, FavStarClassName
 from superset.models.dashboard import Dashboard
 from superset.models.slice import Slice
 from superset.utils.dashboard_filter_scopes_converter import copy_filter_scopes
@@ -154,3 +155,19 @@ class DashboardDAO(BaseDAO):
         if data.get("label_colors"):
             md["label_colors"] = data.get("label_colors")
         dashboard.json_metadata = json.dumps(md)
+
+    @staticmethod
+    def favorited_ids(
+        dashboards: List[Dashboard], current_user_id: int
+    ) -> List[FavStar]:
+        ids = [dash.id for dash in dashboards]
+        return [
+            star.obj_id
+            for star in db.session.query(FavStar.obj_id)
+            .filter(
+                FavStar.class_name == FavStarClassName.DASHBOARD,
+                FavStar.obj_id.in_(ids),
+                FavStar.user_id == current_user_id,
+            )
+            .all()
+        ]
diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py
index 848a80c..b6bbc37 100644
--- a/superset/dashboards/schemas.py
+++ b/superset/dashboards/schemas.py
@@ -26,6 +26,7 @@ from superset.utils import core as utils
 
 get_delete_ids_schema = {"type": "array", "items": {"type": "integer"}}
 get_export_ids_schema = {"type": "array", "items": {"type": "integer"}}
+get_fav_star_ids_schema = {"type": "array", "items": {"type": "integer"}}
 thumbnail_query_schema = {
     "type": "object",
     "properties": {"force": {"type": "boolean"}},
@@ -163,3 +164,15 @@ class DashboardPutSchema(BaseDashboardSchema):
         validate=validate_json_metadata,
     )
     published = fields.Boolean(description=published_description, allow_none=True)
+
+
+class ChartFavStarResponseResult(Schema):
+    id = fields.Integer(description="The Chart id")
+    value = fields.Boolean(description="The FaveStar value")
+
+
+class GetFavStarIdsSchema(Schema):
+    result = fields.List(
+        fields.Nested(ChartFavStarResponseResult),
+        description="A list of results for each corresponding chart in the request",
+    )
diff --git a/superset/models/core.py b/superset/models/core.py
index 6d8a29c..952ba83 100755
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -22,6 +22,7 @@ import textwrap
 from contextlib import closing
 from copy import deepcopy
 from datetime import datetime
+from enum import Enum
 from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type
 
 import numpy
@@ -707,6 +708,11 @@ class Log(Model):  # pylint: disable=too-few-public-methods
     referrer = Column(String(1024))
 
 
+class FavStarClassName(str, Enum):
+    CHART = "slice"
+    DASHBOARD = "Dashboard"
+
+
 class FavStar(Model):  # pylint: disable=too-few-public-methods
     __tablename__ = "favstar"
 
diff --git a/tests/charts/api_tests.py b/tests/charts/api_tests.py
index 429d26c..00decdc 100644
--- a/tests/charts/api_tests.py
+++ b/tests/charts/api_tests.py
@@ -35,7 +35,7 @@ from tests.fixtures.unicode_dashboard import load_unicode_dashboard_with_slice
 from tests.test_app import app
 from superset.connectors.connector_registry import ConnectorRegistry
 from superset.extensions import db, security_manager
-from superset.models.core import FavStar
+from superset.models.core import FavStar, FavStarClassName
 from superset.models.dashboard import Dashboard
 from superset.models.slice import Slice
 from superset.utils import core as utils
@@ -776,6 +776,35 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin):
         assert rv.status_code == 200
         assert len(expected_models) == data["count"]
 
+    @pytest.mark.usefixtures("create_charts")
+    def test_get_current_user_favorite_status(self):
+        """
+        Dataset API: Test get current user favorite stars
+        """
+        admin = self.get_user("admin")
+        users_favorite_ids = [
+            star.obj_id
+            for star in db.session.query(FavStar.obj_id)
+            .filter(
+                and_(
+                    FavStar.user_id == admin.id,
+                    FavStar.class_name == FavStarClassName.CHART,
+                )
+            )
+            .all()
+        ]
+
+        assert users_favorite_ids
+        arguments = [s.id for s in db.session.query(Slice.id).all()]
+        self.login(username="admin")
+        uri = f"api/v1/chart/favorite_status/?q={prison.dumps(arguments)}"
+        rv = self.client.get(uri)
+        data = json.loads(rv.data.decode("utf-8"))
+        assert rv.status_code == 200
+        for res in data["result"]:
+            if res["id"] in users_favorite_ids:
+                assert res["value"]
+
     @pytest.mark.usefixtures("load_unicode_dashboard_with_slice")
     def test_get_charts_page(self):
         """
diff --git a/tests/dashboards/api_tests.py b/tests/dashboards/api_tests.py
index 7df1176..162ea22 100644
--- a/tests/dashboards/api_tests.py
+++ b/tests/dashboards/api_tests.py
@@ -31,7 +31,7 @@ from freezegun import freeze_time
 from sqlalchemy import and_
 from superset import db, security_manager
 from superset.models.dashboard import Dashboard
-from superset.models.core import FavStar
+from superset.models.core import FavStar, FavStarClassName
 from superset.models.slice import Slice
 from superset.views.base import generate_download_headers
 
@@ -341,6 +341,35 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin):
             )
 
     @pytest.mark.usefixtures("create_dashboards")
+    def test_get_current_user_favorite_status(self):
+        """
+        Dataset API: Test get current user favorite stars
+        """
+        admin = self.get_user("admin")
+        users_favorite_ids = [
+            star.obj_id
+            for star in db.session.query(FavStar.obj_id)
+            .filter(
+                and_(
+                    FavStar.user_id == admin.id,
+                    FavStar.class_name == FavStarClassName.DASHBOARD,
+                )
+            )
+            .all()
+        ]
+
+        assert users_favorite_ids
+        arguments = [dash.id for dash in db.session.query(Dashboard.id).all()]
+        self.login(username="admin")
+        uri = f"api/v1/dashboard/favorite_status/?q={prison.dumps(arguments)}"
+        rv = self.client.get(uri)
+        data = json.loads(rv.data.decode("utf-8"))
+        assert rv.status_code == 200
+        for res in data["result"]:
+            if res["id"] in users_favorite_ids:
+                assert res["value"]
+
+    @pytest.mark.usefixtures("create_dashboards")
     def test_get_dashboards_not_favorite_filter(self):
         """
         Dashboard API: Test get dashboards not favorite filter