refactor: reduce number of api calls needed to fetch favorite status for charts and dashboards (#11502)

This commit is contained in:
ʈᵃᵢ 2020-11-02 21:26:14 -08:00 committed by GitHub
parent fd10c47bc6
commit edb9619731
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 421 additions and 176 deletions

View File

@ -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) => {

View File

@ -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 && (

View File

@ -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" />

View File

@ -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}
/>
);
}

View File

@ -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
/>
);
}

View File

@ -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]}
/>
);
}

View File

@ -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(
),
),
);
};
}, [ids]);
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),
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 = (

View File

@ -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;

View File

@ -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 = {

View File

@ -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(

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)

View File

@ -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()
]

View File

@ -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,
)

View File

@ -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)

View File

@ -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()
]

View File

@ -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",
)

View File

@ -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"

View File

@ -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):
"""

View File

@ -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):
"""