mirror of https://github.com/apache/superset.git
refactor: reduce number of api calls needed to fetch favorite status for charts and dashboards (#11502)
This commit is contained in:
parent
fd10c47bc6
commit
edb9619731
|
@ -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,8 +35,10 @@ const StyledLink = styled.a`
|
|||
|
||||
export default class FaveStar extends React.PureComponent<FaveStarProps> {
|
||||
componentDidMount() {
|
||||
if (this.props.fetchFaveStar) {
|
||||
this.props.fetchFaveStar(this.props.itemId);
|
||||
}
|
||||
}
|
||||
|
||||
onClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
|
@ -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,20 +165,18 @@ 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 || (
|
||||
<Cover>
|
||||
<a href={url}>
|
||||
<div className="gradient-container">
|
||||
|
@ -202,7 +198,6 @@ function ListViewCard({
|
|||
</CoverFooter>
|
||||
</Cover>
|
||||
)
|
||||
: null
|
||||
}
|
||||
>
|
||||
{loading && (
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,16 +354,20 @@ export function useFavoriteStatus(
|
|||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
}, [ids]);
|
||||
|
||||
const saveFaveStar = (id: number, isStarred: boolean) => {
|
||||
const saveFaveStar = useCallback(
|
||||
(id: number, isStarred: boolean) => {
|
||||
const urlSuffix = isStarred ? 'unselect' : 'select';
|
||||
|
||||
SupersetClient.get({
|
||||
endpoint: `${baseURL}/${id}/${urlSuffix}/`,
|
||||
endpoint: `/superset/favstar/${
|
||||
type === 'chart' ? FavStarClassName.CHART : FavStarClassName.DASHBOARD
|
||||
}/${id}/${urlSuffix}/`,
|
||||
}).then(
|
||||
() => {
|
||||
updateFavoriteStatus({ [id]: !isStarred });
|
||||
({ json }) => {
|
||||
updateFavoriteStatus({
|
||||
[id]: (json as { count: number })?.count > 0,
|
||||
});
|
||||
},
|
||||
createErrorHandler(errMsg =>
|
||||
handleErrorMsg(
|
||||
|
@ -349,14 +375,11 @@ export function useFavoriteStatus(
|
|||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
},
|
||||
[type],
|
||||
);
|
||||
|
||||
return [
|
||||
favoriteStatusRef,
|
||||
fetchFaveStar,
|
||||
saveFaveStar,
|
||||
favoriteStatus,
|
||||
] as const;
|
||||
return [saveFaveStar, favoriteStatus] as const;
|
||||
}
|
||||
|
||||
export const useChartEditModal = (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
]
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -340,6 +340,35 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin):
|
|||
expected_model.dashboard_title == data["result"][i]["dashboard_title"]
|
||||
)
|
||||
|
||||
@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):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue