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