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 {
|
interface FaveStarProps {
|
||||||
itemId: number;
|
itemId: number;
|
||||||
fetchFaveStar(id: number): any;
|
fetchFaveStar?: (id: number) => void;
|
||||||
saveFaveStar(id: number, isStarred: boolean): any;
|
saveFaveStar(id: number, isStarred: boolean): any;
|
||||||
isStarred: boolean;
|
isStarred: boolean;
|
||||||
showTooltip?: boolean;
|
showTooltip?: boolean;
|
||||||
|
@ -35,7 +35,9 @@ const StyledLink = styled.a`
|
||||||
|
|
||||||
export default class FaveStar extends React.PureComponent<FaveStarProps> {
|
export default class FaveStar extends React.PureComponent<FaveStarProps> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchFaveStar(this.props.itemId);
|
if (this.props.fetchFaveStar) {
|
||||||
|
this.props.fetchFaveStar(this.props.itemId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick = (e: React.MouseEvent) => {
|
onClick = (e: React.MouseEvent) => {
|
||||||
|
|
|
@ -152,11 +152,9 @@ interface CardProps {
|
||||||
coverLeft?: React.ReactNode;
|
coverLeft?: React.ReactNode;
|
||||||
coverRight?: React.ReactNode;
|
coverRight?: React.ReactNode;
|
||||||
actions: React.ReactNode | null;
|
actions: React.ReactNode | null;
|
||||||
showImg?: boolean;
|
|
||||||
rows?: number | string;
|
rows?: number | string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
isRecent?: boolean;
|
cover?: React.ReactNode | null;
|
||||||
renderCover?: React.ReactNode | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListViewCard({
|
function ListViewCard({
|
||||||
|
@ -167,42 +165,39 @@ function ListViewCard({
|
||||||
imgFallbackURL,
|
imgFallbackURL,
|
||||||
description,
|
description,
|
||||||
coverLeft,
|
coverLeft,
|
||||||
isRecent,
|
|
||||||
coverRight,
|
coverRight,
|
||||||
actions,
|
actions,
|
||||||
avatar,
|
avatar,
|
||||||
loading,
|
loading,
|
||||||
imgPosition = 'top',
|
imgPosition = 'top',
|
||||||
renderCover,
|
cover,
|
||||||
}: CardProps) {
|
}: CardProps) {
|
||||||
return (
|
return (
|
||||||
<StyledCard
|
<StyledCard
|
||||||
data-test="styled-card"
|
data-test="styled-card"
|
||||||
cover={
|
cover={
|
||||||
!isRecent
|
cover || (
|
||||||
? renderCover || (
|
<Cover>
|
||||||
<Cover>
|
<a href={url}>
|
||||||
<a href={url}>
|
<div className="gradient-container">
|
||||||
<div className="gradient-container">
|
<ImageLoader
|
||||||
<ImageLoader
|
src={imgURL || ''}
|
||||||
src={imgURL || ''}
|
fallback={imgFallbackURL || ''}
|
||||||
fallback={imgFallbackURL || ''}
|
isLoading={loading}
|
||||||
isLoading={loading}
|
position={imgPosition}
|
||||||
position={imgPosition}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</a>
|
<CoverFooter className="cover-footer">
|
||||||
<CoverFooter className="cover-footer">
|
{!loading && coverLeft && (
|
||||||
{!loading && coverLeft && (
|
<CoverFooterLeft>{coverLeft}</CoverFooterLeft>
|
||||||
<CoverFooterLeft>{coverLeft}</CoverFooterLeft>
|
)}
|
||||||
)}
|
{!loading && coverRight && (
|
||||||
{!loading && coverRight && (
|
<CoverFooterRight>{coverRight}</CoverFooterRight>
|
||||||
<CoverFooterRight>{coverRight}</CoverFooterRight>
|
)}
|
||||||
)}
|
</CoverFooter>
|
||||||
</CoverFooter>
|
</Cover>
|
||||||
</Cover>
|
)
|
||||||
)
|
|
||||||
: null
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{loading && (
|
{loading && (
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useFavoriteStatus } from 'src/views/CRUD/hooks';
|
|
||||||
import { t } from '@superset-ui/core';
|
import { t } from '@superset-ui/core';
|
||||||
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||||
import Icon from 'src/components/Icon';
|
import Icon from 'src/components/Icon';
|
||||||
|
@ -30,8 +29,6 @@ import FaveStar from 'src/components/FaveStar';
|
||||||
import FacePile from 'src/components/FacePile';
|
import FacePile from 'src/components/FacePile';
|
||||||
import { handleChartDelete } from '../utils';
|
import { handleChartDelete } from '../utils';
|
||||||
|
|
||||||
const FAVESTAR_BASE_URL = '/superset/favstar/slice';
|
|
||||||
|
|
||||||
interface ChartCardProps {
|
interface ChartCardProps {
|
||||||
chart: Chart;
|
chart: Chart;
|
||||||
hasPerm: (perm: string) => boolean;
|
hasPerm: (perm: string) => boolean;
|
||||||
|
@ -41,6 +38,8 @@ interface ChartCardProps {
|
||||||
addSuccessToast: (msg: string) => void;
|
addSuccessToast: (msg: string) => void;
|
||||||
refreshData: () => void;
|
refreshData: () => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
saveFavoriteStatus: (id: number, isStarred: boolean) => void;
|
||||||
|
favoriteStatus: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChartCard({
|
export default function ChartCard({
|
||||||
|
@ -52,14 +51,11 @@ export default function ChartCard({
|
||||||
addSuccessToast,
|
addSuccessToast,
|
||||||
refreshData,
|
refreshData,
|
||||||
loading,
|
loading,
|
||||||
|
saveFavoriteStatus,
|
||||||
|
favoriteStatus,
|
||||||
}: ChartCardProps) {
|
}: ChartCardProps) {
|
||||||
const canEdit = hasPerm('can_edit');
|
const canEdit = hasPerm('can_edit');
|
||||||
const canDelete = hasPerm('can_delete');
|
const canDelete = hasPerm('can_delete');
|
||||||
const [, fetchFaveStar, saveFaveStar, favoriteStatus] = useFavoriteStatus(
|
|
||||||
{},
|
|
||||||
FAVESTAR_BASE_URL,
|
|
||||||
addDangerToast,
|
|
||||||
);
|
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
|
@ -124,9 +120,8 @@ export default function ChartCard({
|
||||||
<ListViewCard.Actions>
|
<ListViewCard.Actions>
|
||||||
<FaveStar
|
<FaveStar
|
||||||
itemId={chart.id}
|
itemId={chart.id}
|
||||||
fetchFaveStar={fetchFaveStar}
|
saveFaveStar={saveFavoriteStatus}
|
||||||
saveFaveStar={saveFaveStar}
|
isStarred={favoriteStatus}
|
||||||
isStarred={!!favoriteStatus[chart.id]}
|
|
||||||
/>
|
/>
|
||||||
<Dropdown overlay={menu}>
|
<Dropdown overlay={menu}>
|
||||||
<Icon name="more-horiz" />
|
<Icon name="more-horiz" />
|
||||||
|
|
|
@ -47,7 +47,6 @@ import TooltipWrapper from 'src/components/TooltipWrapper';
|
||||||
import ChartCard from './ChartCard';
|
import ChartCard from './ChartCard';
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
const FAVESTAR_BASE_URL = '/superset/favstar/slice';
|
|
||||||
|
|
||||||
const createFetchDatasets = (handleError: (err: Response) => void) => async (
|
const createFetchDatasets = (handleError: (err: Response) => void) => async (
|
||||||
filterValue = '',
|
filterValue = '',
|
||||||
|
@ -105,9 +104,12 @@ function ChartList(props: ChartListProps) {
|
||||||
toggleBulkSelect,
|
toggleBulkSelect,
|
||||||
refreshData,
|
refreshData,
|
||||||
} = useListViewResource<Chart>('chart', t('chart'), props.addDangerToast);
|
} = useListViewResource<Chart>('chart', t('chart'), props.addDangerToast);
|
||||||
const [favoriteStatusRef, fetchFaveStar, saveFaveStar] = useFavoriteStatus(
|
|
||||||
{},
|
const chartIds = useMemo(() => charts.map(c => c.id), [charts]);
|
||||||
FAVESTAR_BASE_URL,
|
|
||||||
|
const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus(
|
||||||
|
'chart',
|
||||||
|
chartIds,
|
||||||
props.addDangerToast,
|
props.addDangerToast,
|
||||||
);
|
);
|
||||||
const {
|
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(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
|
@ -158,7 +149,13 @@ function ChartList(props: ChartListProps) {
|
||||||
row: {
|
row: {
|
||||||
original: { id },
|
original: { id },
|
||||||
},
|
},
|
||||||
}: any) => renderFaveStar(id),
|
}: any) => (
|
||||||
|
<FaveStar
|
||||||
|
itemId={id}
|
||||||
|
saveFaveStar={saveFavoriteStatus}
|
||||||
|
isStarred={favoriteStatus[id]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
Header: '',
|
Header: '',
|
||||||
id: 'favorite',
|
id: 'favorite',
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
|
@ -303,7 +300,7 @@ function ChartList(props: ChartListProps) {
|
||||||
hidden: !canEdit && !canDelete,
|
hidden: !canEdit && !canDelete,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[canEdit, canDelete],
|
[canEdit, canDelete, favoriteStatus],
|
||||||
);
|
);
|
||||||
|
|
||||||
const filters: Filters = [
|
const filters: Filters = [
|
||||||
|
@ -415,6 +412,8 @@ function ChartList(props: ChartListProps) {
|
||||||
addSuccessToast={props.addSuccessToast}
|
addSuccessToast={props.addSuccessToast}
|
||||||
refreshData={refreshData}
|
refreshData={refreshData}
|
||||||
loading={loading}
|
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 Label from 'src/components/Label';
|
||||||
import FacePile from 'src/components/FacePile';
|
import FacePile from 'src/components/FacePile';
|
||||||
import FaveStar from 'src/components/FaveStar';
|
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';
|
export interface DashboardCardProps {
|
||||||
|
isChart?: boolean;
|
||||||
const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
|
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({
|
function DashboardCard({
|
||||||
dashboard,
|
dashboard,
|
||||||
|
@ -43,15 +53,12 @@ function DashboardCard({
|
||||||
addDangerToast,
|
addDangerToast,
|
||||||
addSuccessToast,
|
addSuccessToast,
|
||||||
openDashboardEditModal,
|
openDashboardEditModal,
|
||||||
|
favoriteStatus,
|
||||||
|
saveFavoriteStatus,
|
||||||
}: DashboardCardProps) {
|
}: DashboardCardProps) {
|
||||||
const canEdit = hasPerm('can_edit');
|
const canEdit = hasPerm('can_edit');
|
||||||
const canDelete = hasPerm('can_delete');
|
const canDelete = hasPerm('can_delete');
|
||||||
const canExport = hasPerm('can_mulexport');
|
const canExport = hasPerm('can_mulexport');
|
||||||
const [, fetchFaveStar, saveFaveStar, favoriteStatus] = useFavoriteStatus(
|
|
||||||
{},
|
|
||||||
FAVESTAR_BASE_URL,
|
|
||||||
addDangerToast,
|
|
||||||
);
|
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
|
@ -123,16 +130,14 @@ function DashboardCard({
|
||||||
<ListViewCard.Actions>
|
<ListViewCard.Actions>
|
||||||
<FaveStar
|
<FaveStar
|
||||||
itemId={dashboard.id}
|
itemId={dashboard.id}
|
||||||
fetchFaveStar={fetchFaveStar}
|
saveFaveStar={saveFavoriteStatus}
|
||||||
saveFaveStar={saveFaveStar}
|
isStarred={favoriteStatus}
|
||||||
isStarred={!!favoriteStatus[dashboard.id]}
|
|
||||||
/>
|
/>
|
||||||
<Dropdown overlay={menu}>
|
<Dropdown overlay={menu}>
|
||||||
<Icon name="more-horiz" />
|
<Icon name="more-horiz" />
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</ListViewCard.Actions>
|
</ListViewCard.Actions>
|
||||||
}
|
}
|
||||||
showImg
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,6 @@ import Dashboard from 'src/dashboard/containers/Dashboard';
|
||||||
import DashboardCard from './DashboardCard';
|
import DashboardCard from './DashboardCard';
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
|
|
||||||
|
|
||||||
interface DashboardListProps {
|
interface DashboardListProps {
|
||||||
addDangerToast: (msg: string) => void;
|
addDangerToast: (msg: string) => void;
|
||||||
|
@ -81,9 +80,11 @@ function DashboardList(props: DashboardListProps) {
|
||||||
t('dashboard'),
|
t('dashboard'),
|
||||||
props.addDangerToast,
|
props.addDangerToast,
|
||||||
);
|
);
|
||||||
const [favoriteStatusRef, fetchFaveStar, saveFaveStar] = useFavoriteStatus(
|
|
||||||
{},
|
const dashboardIds = useMemo(() => dashboards.map(d => d.id), [dashboards]);
|
||||||
FAVESTAR_BASE_URL,
|
const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus(
|
||||||
|
'dashboard',
|
||||||
|
dashboardIds,
|
||||||
props.addDangerToast,
|
props.addDangerToast,
|
||||||
);
|
);
|
||||||
const [dashboardToEdit, setDashboardToEdit] = useState<Dashboard | null>(
|
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(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
|
@ -158,7 +148,13 @@ function DashboardList(props: DashboardListProps) {
|
||||||
row: {
|
row: {
|
||||||
original: { id },
|
original: { id },
|
||||||
},
|
},
|
||||||
}: any) => renderFaveStar(id),
|
}: any) => (
|
||||||
|
<FaveStar
|
||||||
|
itemId={id}
|
||||||
|
saveFaveStar={saveFavoriteStatus}
|
||||||
|
isStarred={favoriteStatus[id]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
Header: '',
|
Header: '',
|
||||||
id: 'favorite',
|
id: 'favorite',
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
|
@ -317,7 +313,7 @@ function DashboardList(props: DashboardListProps) {
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[canEdit, canDelete, canExport, favoriteStatusRef],
|
[canEdit, canDelete, canExport, favoriteStatus],
|
||||||
);
|
);
|
||||||
|
|
||||||
const filters: Filters = [
|
const filters: Filters = [
|
||||||
|
@ -404,15 +400,16 @@ function DashboardList(props: DashboardListProps) {
|
||||||
function renderCard(dashboard: Dashboard) {
|
function renderCard(dashboard: Dashboard) {
|
||||||
return (
|
return (
|
||||||
<DashboardCard
|
<DashboardCard
|
||||||
{...{
|
dashboard={dashboard}
|
||||||
dashboard,
|
hasPerm={hasPerm}
|
||||||
hasPerm,
|
bulkSelectEnabled={bulkSelectEnabled}
|
||||||
bulkSelectEnabled,
|
refreshData={refreshData}
|
||||||
refreshData,
|
loading={loading}
|
||||||
addDangerToast: props.addDangerToast,
|
addDangerToast={props.addDangerToast}
|
||||||
addSuccessToast: props.addSuccessToast,
|
addSuccessToast={props.addSuccessToast}
|
||||||
openDashboardEditModal,
|
openDashboardEditModal={openDashboardEditModal}
|
||||||
}}
|
saveFavoriteStatus={saveFavoriteStatus}
|
||||||
|
favoriteStatus={favoriteStatus[dashboard.id]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,8 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { SupersetClient, t } from '@superset-ui/core';
|
import { makeApi, SupersetClient, t } from '@superset-ui/core';
|
||||||
|
|
||||||
import { createErrorHandler } from 'src/views/CRUD/utils';
|
import { createErrorHandler } from 'src/views/CRUD/utils';
|
||||||
import { FetchDataConfig } from 'src/components/ListView';
|
import { FetchDataConfig } from 'src/components/ListView';
|
||||||
|
@ -58,7 +58,9 @@ export function useListViewResource<D extends object = any>(
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const infoParam = infoEnable ? '_info?q=(keys:!(permissions))' : '';
|
const infoParam = infoEnable
|
||||||
|
? `_info?q=${rison.encode({ keys: ['permissions'] })}`
|
||||||
|
: '';
|
||||||
SupersetClient.get({
|
SupersetClient.get({
|
||||||
endpoint: `/api/v1/${resource}/${infoParam}`,
|
endpoint: `/api/v1/${resource}/${infoParam}`,
|
||||||
}).then(
|
}).then(
|
||||||
|
@ -299,32 +301,52 @@ export function useSingleViewResource<D extends object = any>(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// the hooks api has some known limitations around stale state in closures.
|
enum FavStarClassName {
|
||||||
// See https://github.com/reactjs/rfcs/blob/master/text/0068-react-hooks.md#drawbacks
|
CHART = 'slice',
|
||||||
// the useRef hook is a way of getting around these limitations by having a consistent ref
|
DASHBOARD = 'Dashboard',
|
||||||
// that points to the most recent value.
|
}
|
||||||
|
|
||||||
|
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(
|
export function useFavoriteStatus(
|
||||||
initialState: FavoriteStatus,
|
type: 'chart' | 'dashboard',
|
||||||
baseURL: string,
|
ids: Array<string | number>,
|
||||||
handleErrorMsg: (message: string) => void,
|
handleErrorMsg: (message: string) => void,
|
||||||
) {
|
) {
|
||||||
const [favoriteStatus, setFavoriteStatus] = useState<FavoriteStatus>(
|
const [favoriteStatus, setFavoriteStatus] = useState<FavoriteStatus>({});
|
||||||
initialState,
|
|
||||||
);
|
|
||||||
const favoriteStatusRef = useRef<FavoriteStatus>(favoriteStatus);
|
|
||||||
useEffect(() => {
|
|
||||||
favoriteStatusRef.current = favoriteStatus;
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateFavoriteStatus = (update: FavoriteStatus) =>
|
const updateFavoriteStatus = (update: FavoriteStatus) =>
|
||||||
setFavoriteStatus(currentState => ({ ...currentState, ...update }));
|
setFavoriteStatus(currentState => ({ ...currentState, ...update }));
|
||||||
|
|
||||||
const fetchFaveStar = (id: number) => {
|
useEffect(() => {
|
||||||
SupersetClient.get({
|
if (!ids.length) {
|
||||||
endpoint: `${baseURL}/${id}/count/`,
|
return;
|
||||||
}).then(
|
}
|
||||||
({ json }) => {
|
favoriteApis[type](`q=${rison.encode(ids)}`).then(
|
||||||
updateFavoriteStatus({ [id]: json.count > 0 });
|
({ result }) => {
|
||||||
|
const update = result.reduce((acc, element) => {
|
||||||
|
acc[element.id] = element.value;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
updateFavoriteStatus(update);
|
||||||
},
|
},
|
||||||
createErrorHandler(errMsg =>
|
createErrorHandler(errMsg =>
|
||||||
handleErrorMsg(
|
handleErrorMsg(
|
||||||
|
@ -332,31 +354,32 @@ export function useFavoriteStatus(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
};
|
}, [ids]);
|
||||||
|
|
||||||
const saveFaveStar = (id: number, isStarred: boolean) => {
|
const saveFaveStar = useCallback(
|
||||||
const urlSuffix = isStarred ? 'unselect' : 'select';
|
(id: number, isStarred: boolean) => {
|
||||||
|
const urlSuffix = isStarred ? 'unselect' : 'select';
|
||||||
SupersetClient.get({
|
SupersetClient.get({
|
||||||
endpoint: `${baseURL}/${id}/${urlSuffix}/`,
|
endpoint: `/superset/favstar/${
|
||||||
}).then(
|
type === 'chart' ? FavStarClassName.CHART : FavStarClassName.DASHBOARD
|
||||||
() => {
|
}/${id}/${urlSuffix}/`,
|
||||||
updateFavoriteStatus({ [id]: !isStarred });
|
}).then(
|
||||||
},
|
({ json }) => {
|
||||||
createErrorHandler(errMsg =>
|
updateFavoriteStatus({
|
||||||
handleErrorMsg(
|
[id]: (json as { count: number })?.count > 0,
|
||||||
t('There was an error saving the favorite status: %s', errMsg),
|
});
|
||||||
|
},
|
||||||
|
createErrorHandler(errMsg =>
|
||||||
|
handleErrorMsg(
|
||||||
|
t('There was an error saving the favorite status: %s', errMsg),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
};
|
[type],
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
return [saveFaveStar, favoriteStatus] as const;
|
||||||
favoriteStatusRef,
|
|
||||||
fetchFaveStar,
|
|
||||||
saveFaveStar,
|
|
||||||
favoriteStatus,
|
|
||||||
] as const;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useChartEditModal = (
|
export const useChartEditModal = (
|
||||||
|
|
|
@ -45,17 +45,6 @@ export interface Dashboard {
|
||||||
loading?: boolean;
|
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 = {
|
export type SavedQueryObject = {
|
||||||
database: {
|
database: {
|
||||||
database_name: string;
|
database_name: string;
|
||||||
|
|
|
@ -60,7 +60,7 @@ const createFetchResourceMethod = (method: string) => (
|
||||||
export const getRecentAcitivtyObjs = (
|
export const getRecentAcitivtyObjs = (
|
||||||
userId: string | number,
|
userId: string | number,
|
||||||
recent: string,
|
recent: string,
|
||||||
addDangerToast: (arg0: string, arg1: string) => void,
|
addDangerToast: (arg1: string, arg2: any) => any,
|
||||||
) => {
|
) => {
|
||||||
const getParams = (filters?: Array<any>) => {
|
const getParams = (filters?: Array<any>) => {
|
||||||
const params = {
|
const params = {
|
||||||
|
|
|
@ -177,8 +177,8 @@ export default function ActivityTable({ user }: ActivityProps) {
|
||||||
return activityData[activeChild].map((e: ActivityObjects) => (
|
return activityData[activeChild].map((e: ActivityObjects) => (
|
||||||
<ListViewCard
|
<ListViewCard
|
||||||
key={`${e.id}`}
|
key={`${e.id}`}
|
||||||
isRecent
|
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
cover={<></>}
|
||||||
url={e.sql ? `/supserset/sqllab?queryId=${e.id}` : e.url}
|
url={e.sql ? `/supserset/sqllab?queryId=${e.id}` : e.url}
|
||||||
title={getFilterTitle(e)}
|
title={getFilterTitle(e)}
|
||||||
description={`Last Edited: ${moment(e.changed_on_utc).format(
|
description={`Last Edited: ${moment(e.changed_on_utc).format(
|
||||||
|
|
|
@ -16,9 +16,13 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import { t } from '@superset-ui/core';
|
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 withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
||||||
import { User } from 'src/types/bootstrapTypes';
|
import { User } from 'src/types/bootstrapTypes';
|
||||||
|
@ -51,6 +55,12 @@ function ChartTable({
|
||||||
refreshData,
|
refreshData,
|
||||||
fetchData,
|
fetchData,
|
||||||
} = useListViewResource<Chart>('chart', t('chart'), addDangerToast);
|
} = useListViewResource<Chart>('chart', t('chart'), addDangerToast);
|
||||||
|
const chartIds = useMemo(() => charts.map(c => c.id), [charts]);
|
||||||
|
const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus(
|
||||||
|
'chart',
|
||||||
|
chartIds,
|
||||||
|
addDangerToast,
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
sliceCurrentlyEditing,
|
sliceCurrentlyEditing,
|
||||||
openChartEditModal,
|
openChartEditModal,
|
||||||
|
@ -154,6 +164,8 @@ function ChartTable({
|
||||||
refreshData={refreshData}
|
refreshData={refreshData}
|
||||||
addDangerToast={addDangerToast}
|
addDangerToast={addDangerToast}
|
||||||
addSuccessToast={addSuccessToast}
|
addSuccessToast={addSuccessToast}
|
||||||
|
favoriteStatus={favoriteStatus[e.id]}
|
||||||
|
saveFavoriteStatus={saveFavoriteStatus}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</CardContainer>
|
</CardContainer>
|
||||||
|
|
|
@ -16,9 +16,9 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import { SupersetClient, t } from '@superset-ui/core';
|
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 { Dashboard, DashboardTableProps } from 'src/views/CRUD/types';
|
||||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
|
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
|
||||||
|
@ -42,7 +42,7 @@ function DashboardTable({
|
||||||
addSuccessToast,
|
addSuccessToast,
|
||||||
}: DashboardTableProps) {
|
}: DashboardTableProps) {
|
||||||
const {
|
const {
|
||||||
state: { loading, resourceCollection: dashboards, bulkSelectEnabled },
|
state: { loading, resourceCollection: dashboards },
|
||||||
setResourceCollection: setDashboards,
|
setResourceCollection: setDashboards,
|
||||||
hasPerm,
|
hasPerm,
|
||||||
refreshData,
|
refreshData,
|
||||||
|
@ -52,7 +52,12 @@ function DashboardTable({
|
||||||
t('dashboard'),
|
t('dashboard'),
|
||||||
addDangerToast,
|
addDangerToast,
|
||||||
);
|
);
|
||||||
|
const dashboardIds = useMemo(() => dashboards.map(c => c.id), [dashboards]);
|
||||||
|
const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus(
|
||||||
|
'dashboard',
|
||||||
|
dashboardIds,
|
||||||
|
addDangerToast,
|
||||||
|
);
|
||||||
const [editModal, setEditModal] = useState<Dashboard>();
|
const [editModal, setEditModal] = useState<Dashboard>();
|
||||||
const [dashboardFilter, setDashboardFilter] = useState('Mine');
|
const [dashboardFilter, setDashboardFilter] = useState('Mine');
|
||||||
|
|
||||||
|
@ -168,16 +173,16 @@ function DashboardTable({
|
||||||
<CardContainer>
|
<CardContainer>
|
||||||
{dashboards.map(e => (
|
{dashboards.map(e => (
|
||||||
<DashboardCard
|
<DashboardCard
|
||||||
{...{
|
dashboard={e}
|
||||||
dashboard: e,
|
hasPerm={hasPerm}
|
||||||
hasPerm,
|
bulkSelectEnabled={false}
|
||||||
bulkSelectEnabled,
|
refreshData={refreshData}
|
||||||
refreshData,
|
addDangerToast={addDangerToast}
|
||||||
addDangerToast,
|
addSuccessToast={addSuccessToast}
|
||||||
addSuccessToast,
|
loading={loading}
|
||||||
loading,
|
openDashboardEditModal={dashboard => setEditModal(dashboard)}
|
||||||
openDashboardEditModal: dashboard => setEditModal(dashboard),
|
saveFavoriteStatus={saveFavoriteStatus}
|
||||||
}}
|
favoriteStatus={favoriteStatus[e.id]}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</CardContainer>
|
</CardContainer>
|
||||||
|
|
|
@ -227,8 +227,7 @@ const SavedQueries = ({
|
||||||
rows={q.rows}
|
rows={q.rows}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
description={t('Last run ', q.end_time)}
|
description={t('Last run ', q.end_time)}
|
||||||
showImg={false}
|
cover={
|
||||||
renderCover={
|
|
||||||
<QueryData>
|
<QueryData>
|
||||||
<div className="holder">
|
<div className="holder">
|
||||||
<div className="title">{t('Tables')}</div>
|
<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.export import ExportChartsCommand
|
||||||
from superset.charts.commands.update import UpdateChartCommand
|
from superset.charts.commands.update import UpdateChartCommand
|
||||||
|
from superset.charts.dao import ChartDAO
|
||||||
from superset.charts.filters import ChartAllTextFilter, ChartFavoriteFilter, ChartFilter
|
from superset.charts.filters import ChartAllTextFilter, ChartFavoriteFilter, ChartFilter
|
||||||
from superset.charts.schemas import (
|
from superset.charts.schemas import (
|
||||||
CHART_SCHEMAS,
|
CHART_SCHEMAS,
|
||||||
|
@ -53,6 +54,7 @@ from superset.charts.schemas import (
|
||||||
ChartPutSchema,
|
ChartPutSchema,
|
||||||
get_delete_ids_schema,
|
get_delete_ids_schema,
|
||||||
get_export_ids_schema,
|
get_export_ids_schema,
|
||||||
|
get_fav_star_ids_schema,
|
||||||
openapi_spec_methods_override,
|
openapi_spec_methods_override,
|
||||||
screenshot_query_schema,
|
screenshot_query_schema,
|
||||||
thumbnail_query_schema,
|
thumbnail_query_schema,
|
||||||
|
@ -87,7 +89,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||||
RouteMethod.RELATED,
|
RouteMethod.RELATED,
|
||||||
"bulk_delete", # not using RouteMethod since locally defined
|
"bulk_delete", # not using RouteMethod since locally defined
|
||||||
"data",
|
"data",
|
||||||
"viz_types",
|
"favorite_status",
|
||||||
}
|
}
|
||||||
class_permission_name = "SliceModelView"
|
class_permission_name = "SliceModelView"
|
||||||
show_columns = [
|
show_columns = [
|
||||||
|
@ -176,6 +178,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||||
"screenshot_query_schema": screenshot_query_schema,
|
"screenshot_query_schema": screenshot_query_schema,
|
||||||
"get_delete_ids_schema": get_delete_ids_schema,
|
"get_delete_ids_schema": get_delete_ids_schema,
|
||||||
"get_export_ids_schema": get_export_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 """
|
""" Add extra schemas to the OpenAPI components schema section """
|
||||||
openapi_spec_methods = openapi_spec_methods_override
|
openapi_spec_methods = openapi_spec_methods_override
|
||||||
|
@ -773,3 +776,48 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
attachment_filename=filename,
|
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.charts.filters import ChartFilter
|
||||||
from superset.dao.base import BaseDAO
|
from superset.dao.base import BaseDAO
|
||||||
from superset.extensions import db
|
from superset.extensions import db
|
||||||
|
from superset.models.core import FavStar, FavStarClassName
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -66,3 +67,17 @@ class ChartDAO(BaseDAO):
|
||||||
db.session.merge(slc)
|
db.session.merge(slc)
|
||||||
if commit:
|
if commit:
|
||||||
db.session.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_export_ids_schema = {"type": "array", "items": {"type": "integer"}}
|
||||||
|
|
||||||
|
get_fav_star_ids_schema = {"type": "array", "items": {"type": "integer"}}
|
||||||
|
|
||||||
#
|
#
|
||||||
# Column schema descriptions
|
# 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 = (
|
CHART_SCHEMAS = (
|
||||||
ChartDataQueryContextSchema,
|
ChartDataQueryContextSchema,
|
||||||
ChartDataResponseSchema,
|
ChartDataResponseSchema,
|
||||||
|
@ -1049,4 +1063,5 @@ CHART_SCHEMAS = (
|
||||||
ChartDataGeodeticParseOptionsSchema,
|
ChartDataGeodeticParseOptionsSchema,
|
||||||
ChartGetDatasourceResponseSchema,
|
ChartGetDatasourceResponseSchema,
|
||||||
ChartCacheScreenshotResponseSchema,
|
ChartCacheScreenshotResponseSchema,
|
||||||
|
GetFavStarIdsSchema,
|
||||||
)
|
)
|
||||||
|
|
|
@ -44,6 +44,7 @@ from superset.dashboards.commands.exceptions import (
|
||||||
)
|
)
|
||||||
from superset.dashboards.commands.export import ExportDashboardsCommand
|
from superset.dashboards.commands.export import ExportDashboardsCommand
|
||||||
from superset.dashboards.commands.update import UpdateDashboardCommand
|
from superset.dashboards.commands.update import UpdateDashboardCommand
|
||||||
|
from superset.dashboards.dao import DashboardDAO
|
||||||
from superset.dashboards.filters import (
|
from superset.dashboards.filters import (
|
||||||
DashboardFavoriteFilter,
|
DashboardFavoriteFilter,
|
||||||
DashboardFilter,
|
DashboardFilter,
|
||||||
|
@ -54,6 +55,8 @@ from superset.dashboards.schemas import (
|
||||||
DashboardPutSchema,
|
DashboardPutSchema,
|
||||||
get_delete_ids_schema,
|
get_delete_ids_schema,
|
||||||
get_export_ids_schema,
|
get_export_ids_schema,
|
||||||
|
get_fav_star_ids_schema,
|
||||||
|
GetFavStarIdsSchema,
|
||||||
openapi_spec_methods_override,
|
openapi_spec_methods_override,
|
||||||
thumbnail_query_schema,
|
thumbnail_query_schema,
|
||||||
)
|
)
|
||||||
|
@ -78,6 +81,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||||
RouteMethod.EXPORT,
|
RouteMethod.EXPORT,
|
||||||
RouteMethod.RELATED,
|
RouteMethod.RELATED,
|
||||||
"bulk_delete", # not using RouteMethod since locally defined
|
"bulk_delete", # not using RouteMethod since locally defined
|
||||||
|
"favorite_status",
|
||||||
}
|
}
|
||||||
resource_name = "dashboard"
|
resource_name = "dashboard"
|
||||||
allow_browser_login = True
|
allow_browser_login = True
|
||||||
|
@ -181,10 +185,13 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||||
allowed_rel_fields = {"owners", "created_by"}
|
allowed_rel_fields = {"owners", "created_by"}
|
||||||
|
|
||||||
openapi_spec_tag = "Dashboards"
|
openapi_spec_tag = "Dashboards"
|
||||||
|
""" Override the name set for this collection of endpoints """
|
||||||
|
openapi_spec_component_schemas = (GetFavStarIdsSchema,)
|
||||||
apispec_parameter_schemas = {
|
apispec_parameter_schemas = {
|
||||||
"get_delete_ids_schema": get_delete_ids_schema,
|
"get_delete_ids_schema": get_delete_ids_schema,
|
||||||
"get_export_ids_schema": get_export_ids_schema,
|
"get_export_ids_schema": get_export_ids_schema,
|
||||||
"thumbnail_query_schema": thumbnail_query_schema,
|
"thumbnail_query_schema": thumbnail_query_schema,
|
||||||
|
"get_fav_star_ids_schema": get_fav_star_ids_schema,
|
||||||
}
|
}
|
||||||
openapi_spec_methods = openapi_spec_methods_override
|
openapi_spec_methods = openapi_spec_methods_override
|
||||||
""" Overrides GET methods OpenApi descriptions """
|
""" Overrides GET methods OpenApi descriptions """
|
||||||
|
@ -589,3 +596,48 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||||
return Response(
|
return Response(
|
||||||
FileWrapper(screenshot), mimetype="image/png", direct_passthrough=True
|
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.dao.base import BaseDAO
|
||||||
from superset.dashboards.filters import DashboardFilter
|
from superset.dashboards.filters import DashboardFilter
|
||||||
from superset.extensions import db
|
from superset.extensions import db
|
||||||
|
from superset.models.core import FavStar, FavStarClassName
|
||||||
from superset.models.dashboard import Dashboard
|
from superset.models.dashboard import Dashboard
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
from superset.utils.dashboard_filter_scopes_converter import copy_filter_scopes
|
from superset.utils.dashboard_filter_scopes_converter import copy_filter_scopes
|
||||||
|
@ -154,3 +155,19 @@ class DashboardDAO(BaseDAO):
|
||||||
if data.get("label_colors"):
|
if data.get("label_colors"):
|
||||||
md["label_colors"] = data.get("label_colors")
|
md["label_colors"] = data.get("label_colors")
|
||||||
dashboard.json_metadata = json.dumps(md)
|
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_delete_ids_schema = {"type": "array", "items": {"type": "integer"}}
|
||||||
get_export_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 = {
|
thumbnail_query_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {"force": {"type": "boolean"}},
|
"properties": {"force": {"type": "boolean"}},
|
||||||
|
@ -163,3 +164,15 @@ class DashboardPutSchema(BaseDashboardSchema):
|
||||||
validate=validate_json_metadata,
|
validate=validate_json_metadata,
|
||||||
)
|
)
|
||||||
published = fields.Boolean(description=published_description, allow_none=True)
|
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 contextlib import closing
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type
|
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
@ -707,6 +708,11 @@ class Log(Model): # pylint: disable=too-few-public-methods
|
||||||
referrer = Column(String(1024))
|
referrer = Column(String(1024))
|
||||||
|
|
||||||
|
|
||||||
|
class FavStarClassName(str, Enum):
|
||||||
|
CHART = "slice"
|
||||||
|
DASHBOARD = "Dashboard"
|
||||||
|
|
||||||
|
|
||||||
class FavStar(Model): # pylint: disable=too-few-public-methods
|
class FavStar(Model): # pylint: disable=too-few-public-methods
|
||||||
__tablename__ = "favstar"
|
__tablename__ = "favstar"
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ from tests.fixtures.unicode_dashboard import load_unicode_dashboard_with_slice
|
||||||
from tests.test_app import app
|
from tests.test_app import app
|
||||||
from superset.connectors.connector_registry import ConnectorRegistry
|
from superset.connectors.connector_registry import ConnectorRegistry
|
||||||
from superset.extensions import db, security_manager
|
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.dashboard import Dashboard
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
from superset.utils import core as utils
|
from superset.utils import core as utils
|
||||||
|
@ -776,6 +776,35 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||||
assert rv.status_code == 200
|
assert rv.status_code == 200
|
||||||
assert len(expected_models) == data["count"]
|
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")
|
@pytest.mark.usefixtures("load_unicode_dashboard_with_slice")
|
||||||
def test_get_charts_page(self):
|
def test_get_charts_page(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -31,7 +31,7 @@ from freezegun import freeze_time
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
from superset import db, security_manager
|
from superset import db, security_manager
|
||||||
from superset.models.dashboard import Dashboard
|
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.models.slice import Slice
|
||||||
from superset.views.base import generate_download_headers
|
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"]
|
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")
|
@pytest.mark.usefixtures("create_dashboards")
|
||||||
def test_get_dashboards_not_favorite_filter(self):
|
def test_get_dashboards_not_favorite_filter(self):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue