fix(dashboard): draft dashboards should be viewable (#14207)

* fix(dashboard): draft dashboards should have open access

* Remove a duplicate test
This commit is contained in:
Jesse Yang 2021-04-21 09:54:51 -07:00 committed by GitHub
parent 852e840575
commit 2dd20df03d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 126 additions and 158 deletions

View File

@ -37,28 +37,6 @@ export interface CardSortSelectOption {
value: any; value: any;
} }
type FilterOperator =
| 'sw'
| 'ew'
| 'ct'
| 'eq'
| 'nsw'
| 'new'
| 'nct'
| 'neq'
| 'gt'
| 'lt'
| 'rel_m_m'
| 'rel_o_m'
| 'title_or_slug'
| 'name_or_description'
| 'all_text'
| 'chart_all_text'
| 'dataset_is_null_or_empty'
| 'between'
| 'dashboard_is_favorite'
| 'chart_is_favorite';
export interface Filter { export interface Filter {
Header: ReactNode; Header: ReactNode;
id: string; id: string;
@ -104,7 +82,7 @@ export interface InternalFilter extends FilterValue {
Header?: string; Header?: string;
} }
export enum FilterOperators { export enum FilterOperator {
startsWith = 'sw', startsWith = 'sw',
endsWith = 'ew', endsWith = 'ew',
contains = 'ct', contains = 'ct',

View File

@ -26,7 +26,7 @@ import Button from 'src/components/Button';
import FacePile from 'src/components/FacePile'; import FacePile from 'src/components/FacePile';
import { Tooltip } from 'src/components/Tooltip'; import { Tooltip } from 'src/components/Tooltip';
import ListView, { import ListView, {
FilterOperators, FilterOperator,
Filters, Filters,
ListViewProps, ListViewProps,
} from 'src/components/ListView'; } from 'src/components/ListView';
@ -84,7 +84,7 @@ function AlertList({
() => [ () => [
{ {
id: 'type', id: 'type',
operator: FilterOperators.equals, operator: FilterOperator.equals,
value: isReportEnabled ? 'Report' : 'Alert', value: isReportEnabled ? 'Report' : 'Alert',
}, },
], ],
@ -373,7 +373,7 @@ function AlertList({
Header: t('Created by'), Header: t('Created by'),
id: 'created_by', id: 'created_by',
input: 'select', input: 'select',
operator: FilterOperators.relationOneMany, operator: FilterOperator.relationOneMany,
unfilteredLabel: 'All', unfilteredLabel: 'All',
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'report', 'report',
@ -389,7 +389,7 @@ function AlertList({
Header: t('Status'), Header: t('Status'),
id: 'last_state', id: 'last_state',
input: 'select', input: 'select',
operator: FilterOperators.equals, operator: FilterOperator.equals,
unfilteredLabel: 'Any', unfilteredLabel: 'Any',
selects: [ selects: [
{ label: t(`${AlertState.success}`), value: AlertState.success }, { label: t(`${AlertState.success}`), value: AlertState.success },
@ -403,7 +403,7 @@ function AlertList({
Header: t('Search'), Header: t('Search'),
id: 'name', id: 'name',
input: 'search', input: 'search',
operator: FilterOperators.contains, operator: FilterOperator.contains,
}, },
], ],
[], [],

View File

@ -27,7 +27,11 @@ import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
import withToasts from 'src/messageToasts/enhancers/withToasts'; import withToasts from 'src/messageToasts/enhancers/withToasts';
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu'; import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar'; import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
import ListView, { ListViewProps, Filters } from 'src/components/ListView'; import ListView, {
ListViewProps,
Filters,
FilterOperator,
} from 'src/components/ListView';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import DeleteModal from 'src/components/DeleteModal'; import DeleteModal from 'src/components/DeleteModal';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
@ -286,7 +290,7 @@ function AnnotationLayersList({
Header: t('Created by'), Header: t('Created by'),
id: 'created_by', id: 'created_by',
input: 'select', input: 'select',
operator: 'rel_o_m', operator: FilterOperator.relationOneMany,
unfilteredLabel: 'All', unfilteredLabel: 'All',
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'annotation_layer', 'annotation_layer',
@ -305,7 +309,7 @@ function AnnotationLayersList({
Header: t('Search'), Header: t('Search'),
id: 'name', id: 'name',
input: 'search', input: 'search',
operator: 'ct', operator: FilterOperator.contains,
}, },
], ],
[], [],

View File

@ -44,7 +44,7 @@ import ListView, {
ListViewProps, ListViewProps,
Filters, Filters,
SelectOption, SelectOption,
FilterOperators, FilterOperator,
} from 'src/components/ListView'; } from 'src/components/ListView';
import { getFromLocalStorage } from 'src/utils/localStorageHelpers'; import { getFromLocalStorage } from 'src/utils/localStorageHelpers';
import withToasts from 'src/messageToasts/enhancers/withToasts'; import withToasts from 'src/messageToasts/enhancers/withToasts';
@ -385,7 +385,7 @@ function ChartList(props: ChartListProps) {
Header: t('Owner'), Header: t('Owner'),
id: 'owners', id: 'owners',
input: 'select', input: 'select',
operator: FilterOperators.relationManyMany, operator: FilterOperator.relationManyMany,
unfilteredLabel: t('All'), unfilteredLabel: t('All'),
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'chart', 'chart',
@ -406,7 +406,7 @@ function ChartList(props: ChartListProps) {
Header: t('Created by'), Header: t('Created by'),
id: 'created_by', id: 'created_by',
input: 'select', input: 'select',
operator: FilterOperators.relationOneMany, operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'), unfilteredLabel: t('All'),
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'chart', 'chart',
@ -427,7 +427,7 @@ function ChartList(props: ChartListProps) {
Header: t('Viz type'), Header: t('Viz type'),
id: 'viz_type', id: 'viz_type',
input: 'select', input: 'select',
operator: FilterOperators.equals, operator: FilterOperator.equals,
unfilteredLabel: t('All'), unfilteredLabel: t('All'),
selects: registry selects: registry
.keys() .keys()
@ -451,7 +451,7 @@ function ChartList(props: ChartListProps) {
Header: t('Dataset'), Header: t('Dataset'),
id: 'datasource_id', id: 'datasource_id',
input: 'select', input: 'select',
operator: FilterOperators.equals, operator: FilterOperator.equals,
unfilteredLabel: t('All'), unfilteredLabel: t('All'),
fetchSelects: createFetchDatasets( fetchSelects: createFetchDatasets(
createErrorHandler(errMsg => createErrorHandler(errMsg =>
@ -470,7 +470,7 @@ function ChartList(props: ChartListProps) {
id: 'id', id: 'id',
urlDisplay: 'favorite', urlDisplay: 'favorite',
input: 'select', input: 'select',
operator: FilterOperators.chartIsFav, operator: FilterOperator.chartIsFav,
unfilteredLabel: t('Any'), unfilteredLabel: t('Any'),
selects: [ selects: [
{ label: t('Yes'), value: true }, { label: t('Yes'), value: true },
@ -481,7 +481,7 @@ function ChartList(props: ChartListProps) {
Header: t('Search'), Header: t('Search'),
id: 'slice_name', id: 'slice_name',
input: 'search', input: 'search',
operator: FilterOperators.chartAllText, operator: FilterOperator.chartAllText,
}, },
]; ];

View File

@ -30,7 +30,11 @@ import DeleteModal from 'src/components/DeleteModal';
import { Tooltip } from 'src/components/Tooltip'; import { Tooltip } from 'src/components/Tooltip';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar'; import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
import ListView, { ListViewProps, Filters } from 'src/components/ListView'; import ListView, {
ListViewProps,
Filters,
FilterOperator,
} from 'src/components/ListView';
import CssTemplateModal from './CssTemplateModal'; import CssTemplateModal from './CssTemplateModal';
import { TemplateObject } from './types'; import { TemplateObject } from './types';
@ -272,7 +276,7 @@ function CssTemplatesList({
Header: t('Created by'), Header: t('Created by'),
id: 'created_by', id: 'created_by',
input: 'select', input: 'select',
operator: 'rel_o_m', operator: FilterOperator.relationOneMany,
unfilteredLabel: 'All', unfilteredLabel: 'All',
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'css_template', 'css_template',
@ -291,7 +295,7 @@ function CssTemplatesList({
Header: t('Search'), Header: t('Search'),
id: 'template_name', id: 'template_name',
input: 'search', input: 'search',
operator: 'ct', operator: FilterOperator.contains,
}, },
], ],
[], [],

View File

@ -32,7 +32,7 @@ import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
import ListView, { import ListView, {
ListViewProps, ListViewProps,
Filters, Filters,
FilterOperators, FilterOperator,
} from 'src/components/ListView'; } from 'src/components/ListView';
import { getFromLocalStorage } from 'src/utils/localStorageHelpers'; import { getFromLocalStorage } from 'src/utils/localStorageHelpers';
import Owner from 'src/types/Owner'; import Owner from 'src/types/Owner';
@ -46,6 +46,7 @@ import ImportModelsModal from 'src/components/ImportModal/index';
import Dashboard from 'src/dashboard/containers/Dashboard'; import Dashboard from 'src/dashboard/containers/Dashboard';
import DashboardCard from './DashboardCard'; import DashboardCard from './DashboardCard';
import { DashboardStatus } from './types';
const PAGE_SIZE = 25; const PAGE_SIZE = 25;
const PASSWORDS_NEEDED_MESSAGE = t( const PASSWORDS_NEEDED_MESSAGE = t(
@ -230,9 +231,10 @@ function DashboardList(props: DashboardListProps) {
{ {
Cell: ({ Cell: ({
row: { row: {
original: { published }, original: { status },
}, },
}: any) => (published ? t('Published') : t('Draft')), }: any) =>
status === DashboardStatus.PUBLISHED ? t('Published') : t('Draft'),
Header: t('Status'), Header: t('Status'),
accessor: 'published', accessor: 'published',
size: 'xl', size: 'xl',
@ -362,7 +364,7 @@ function DashboardList(props: DashboardListProps) {
Header: t('Owner'), Header: t('Owner'),
id: 'owners', id: 'owners',
input: 'select', input: 'select',
operator: FilterOperators.relationManyMany, operator: FilterOperator.relationManyMany,
unfilteredLabel: t('All'), unfilteredLabel: t('All'),
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'dashboard', 'dashboard',
@ -383,7 +385,7 @@ function DashboardList(props: DashboardListProps) {
Header: t('Created by'), Header: t('Created by'),
id: 'created_by', id: 'created_by',
input: 'select', input: 'select',
operator: FilterOperators.relationOneMany, operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'), unfilteredLabel: t('All'),
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'dashboard', 'dashboard',
@ -404,11 +406,11 @@ function DashboardList(props: DashboardListProps) {
Header: t('Status'), Header: t('Status'),
id: 'published', id: 'published',
input: 'select', input: 'select',
operator: FilterOperators.equals, operator: FilterOperator.equals,
unfilteredLabel: t('Any'), unfilteredLabel: t('Any'),
selects: [ selects: [
{ label: t('Published'), value: true }, { label: t('Published'), value: true },
{ label: t('Unpublished'), value: false }, { label: t('Draft'), value: false },
], ],
}, },
{ {
@ -416,7 +418,7 @@ function DashboardList(props: DashboardListProps) {
id: 'id', id: 'id',
urlDisplay: 'favorite', urlDisplay: 'favorite',
input: 'select', input: 'select',
operator: FilterOperators.dashboardIsFav, operator: FilterOperator.dashboardIsFav,
unfilteredLabel: t('Any'), unfilteredLabel: t('Any'),
selects: [ selects: [
{ label: t('Yes'), value: true }, { label: t('Yes'), value: true },
@ -427,7 +429,7 @@ function DashboardList(props: DashboardListProps) {
Header: t('Search'), Header: t('Search'),
id: 'dashboard_title', id: 'dashboard_title',
input: 'search', input: 'search',
operator: FilterOperators.titleOrSlug, operator: FilterOperator.titleOrSlug,
}, },
]; ];

View File

@ -24,3 +24,8 @@ export type DashboardObject = {
position?: string; position?: string;
metadata?: string; metadata?: string;
}; };
export enum DashboardStatus {
PUBLISHED = 'published',
DRAFT = 'draft',
}

View File

@ -27,7 +27,7 @@ import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
import DeleteModal from 'src/components/DeleteModal'; import DeleteModal from 'src/components/DeleteModal';
import { Tooltip } from 'src/components/Tooltip'; import { Tooltip } from 'src/components/Tooltip';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
import ListView, { Filters } from 'src/components/ListView'; import ListView, { FilterOperator, Filters } from 'src/components/ListView';
import { commonMenuData } from 'src/views/CRUD/data/common'; import { commonMenuData } from 'src/views/CRUD/data/common';
import ImportModelsModal from 'src/components/ImportModal/index'; import ImportModelsModal from 'src/components/ImportModal/index';
import DatabaseModal from './DatabaseModal'; import DatabaseModal from './DatabaseModal';
@ -374,7 +374,7 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
Header: t('Expose in SQL Lab'), Header: t('Expose in SQL Lab'),
id: 'expose_in_sqllab', id: 'expose_in_sqllab',
input: 'select', input: 'select',
operator: 'eq', operator: FilterOperator.equals,
unfilteredLabel: 'All', unfilteredLabel: 'All',
selects: [ selects: [
{ label: 'Yes', value: true }, { label: 'Yes', value: true },
@ -393,7 +393,7 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
), ),
id: 'allow_run_async', id: 'allow_run_async',
input: 'select', input: 'select',
operator: 'eq', operator: FilterOperator.equals,
unfilteredLabel: 'All', unfilteredLabel: 'All',
selects: [ selects: [
{ label: 'Yes', value: true }, { label: 'Yes', value: true },
@ -404,7 +404,7 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
Header: t('Search'), Header: t('Search'),
id: 'database_name', id: 'database_name',
input: 'search', input: 'search',
operator: 'ct', operator: FilterOperator.contains,
}, },
], ],
[], [],

View File

@ -33,7 +33,11 @@ import { useListViewResource } from 'src/views/CRUD/hooks';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import DatasourceModal from 'src/datasource/DatasourceModal'; import DatasourceModal from 'src/datasource/DatasourceModal';
import DeleteModal from 'src/components/DeleteModal'; import DeleteModal from 'src/components/DeleteModal';
import ListView, { ListViewProps, Filters } from 'src/components/ListView'; import ListView, {
ListViewProps,
Filters,
FilterOperator,
} from 'src/components/ListView';
import SubMenu, { import SubMenu, {
SubMenuProps, SubMenuProps,
ButtonProps, ButtonProps,
@ -308,7 +312,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
{ {
Cell: ({ Cell: ({
row: { row: {
original: { owners = [], table_name: tableName }, original: { owners = [] },
}, },
}: any) => <FacePile users={owners} />, }: any) => <FacePile users={owners} />,
Header: t('Owners'), Header: t('Owners'),
@ -397,7 +401,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
Header: t('Owner'), Header: t('Owner'),
id: 'owners', id: 'owners',
input: 'select', input: 'select',
operator: 'rel_m_m', operator: FilterOperator.relationManyMany,
unfilteredLabel: 'All', unfilteredLabel: 'All',
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'dataset', 'dataset',
@ -416,7 +420,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
Header: t('Database'), Header: t('Database'),
id: 'database', id: 'database',
input: 'select', input: 'select',
operator: 'rel_o_m', operator: FilterOperator.relationManyMany,
unfilteredLabel: 'All', unfilteredLabel: 'All',
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'dataset', 'dataset',
@ -431,7 +435,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
Header: t('Schema'), Header: t('Schema'),
id: 'schema', id: 'schema',
input: 'select', input: 'select',
operator: 'eq', operator: FilterOperator.equals,
unfilteredLabel: 'All', unfilteredLabel: 'All',
fetchSelects: createFetchDistinct( fetchSelects: createFetchDistinct(
'dataset', 'dataset',
@ -446,7 +450,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
Header: t('Type'), Header: t('Type'),
id: 'sql', id: 'sql',
input: 'select', input: 'select',
operator: 'dataset_is_null_or_empty', operator: FilterOperator.datasetIsNullOrEmpty,
unfilteredLabel: 'All', unfilteredLabel: 'All',
selects: [ selects: [
{ label: 'Virtual', value: false }, { label: 'Virtual', value: false },
@ -457,7 +461,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
Header: t('Search'), Header: t('Search'),
id: 'table_name', id: 'table_name',
input: 'search', input: 'search',
operator: 'ct', operator: FilterOperator.contains,
}, },
], ],
[], [],

View File

@ -32,7 +32,7 @@ import Popover from 'src/components/Popover';
import { commonMenuData } from 'src/views/CRUD/data/common'; import { commonMenuData } from 'src/views/CRUD/data/common';
import ListView, { import ListView, {
Filters, Filters,
FilterOperators, FilterOperator,
ListViewProps, ListViewProps,
} from 'src/components/ListView'; } from 'src/components/ListView';
import Icon, { IconName } from 'src/components/Icon'; import Icon, { IconName } from 'src/components/Icon';
@ -340,7 +340,7 @@ function QueryList({ addDangerToast, addSuccessToast }: QueryListProps) {
Header: t('Database'), Header: t('Database'),
id: 'database', id: 'database',
input: 'select', input: 'select',
operator: FilterOperators.relationOneMany, operator: FilterOperator.relationOneMany,
unfilteredLabel: 'All', unfilteredLabel: 'All',
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'query', 'query',
@ -357,7 +357,7 @@ function QueryList({ addDangerToast, addSuccessToast }: QueryListProps) {
Header: t('State'), Header: t('State'),
id: 'status', id: 'status',
input: 'select', input: 'select',
operator: FilterOperators.equals, operator: FilterOperator.equals,
unfilteredLabel: 'All', unfilteredLabel: 'All',
fetchSelects: createFetchDistinct( fetchSelects: createFetchDistinct(
'query', 'query',
@ -374,7 +374,7 @@ function QueryList({ addDangerToast, addSuccessToast }: QueryListProps) {
Header: t('User'), Header: t('User'),
id: 'user', id: 'user',
input: 'select', input: 'select',
operator: FilterOperators.relationOneMany, operator: FilterOperator.relationOneMany,
unfilteredLabel: 'All', unfilteredLabel: 'All',
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'query', 'query',
@ -391,13 +391,13 @@ function QueryList({ addDangerToast, addSuccessToast }: QueryListProps) {
Header: t('Time range'), Header: t('Time range'),
id: 'start_time', id: 'start_time',
input: 'datetime_range', input: 'datetime_range',
operator: FilterOperators.between, operator: FilterOperator.between,
}, },
{ {
Header: t('Search by query text'), Header: t('Search by query text'),
id: 'sql', id: 'sql',
input: 'search', input: 'search',
operator: FilterOperators.contains, operator: FilterOperator.contains,
}, },
], ],
[addDangerToast], [addDangerToast],

View File

@ -35,7 +35,11 @@ import SubMenu, {
SubMenuProps, SubMenuProps,
ButtonProps, ButtonProps,
} from 'src/components/Menu/SubMenu'; } from 'src/components/Menu/SubMenu';
import ListView, { ListViewProps, Filters } from 'src/components/ListView'; import ListView, {
ListViewProps,
Filters,
FilterOperator,
} from 'src/components/ListView';
import DeleteModal from 'src/components/DeleteModal'; import DeleteModal from 'src/components/DeleteModal';
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar'; import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
import { Tooltip } from 'src/components/Tooltip'; import { Tooltip } from 'src/components/Tooltip';
@ -411,7 +415,7 @@ function SavedQueryList({
Header: t('Database'), Header: t('Database'),
id: 'database', id: 'database',
input: 'select', input: 'select',
operator: 'rel_o_m', operator: FilterOperator.relationOneMany,
unfilteredLabel: 'All', unfilteredLabel: 'All',
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'saved_query', 'saved_query',
@ -431,7 +435,7 @@ function SavedQueryList({
Header: t('Schema'), Header: t('Schema'),
id: 'schema', id: 'schema',
input: 'select', input: 'select',
operator: 'eq', operator: FilterOperator.equals,
unfilteredLabel: 'All', unfilteredLabel: 'All',
fetchSelects: createFetchDistinct( fetchSelects: createFetchDistinct(
'saved_query', 'saved_query',
@ -448,7 +452,7 @@ function SavedQueryList({
Header: t('Search'), Header: t('Search'),
id: 'label', id: 'label',
input: 'search', input: 'search',
operator: 'all_text', operator: FilterOperator.allText,
}, },
], ],
[addDangerToast], [addDangerToast],

View File

@ -52,8 +52,8 @@ from superset.dashboards.commands.importers.dispatcher import ImportDashboardsCo
from superset.dashboards.commands.update import UpdateDashboardCommand from superset.dashboards.commands.update import UpdateDashboardCommand
from superset.dashboards.dao import DashboardDAO from superset.dashboards.dao import DashboardDAO
from superset.dashboards.filters import ( from superset.dashboards.filters import (
DashboardAccessFilter,
DashboardFavoriteFilter, DashboardFavoriteFilter,
DashboardFilter,
DashboardTitleOrSlugFilter, DashboardTitleOrSlugFilter,
FilterRelatedRoles, FilterRelatedRoles,
) )
@ -105,6 +105,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
list_columns = [ list_columns = [
"id", "id",
"published", "published",
"status",
"slug", "slug",
"url", "url",
"css", "css",
@ -153,13 +154,13 @@ class DashboardRestApi(BaseSupersetModelRestApi):
search_columns = ( search_columns = (
"created_by", "created_by",
"changed_by",
"dashboard_title", "dashboard_title",
"id", "id",
"owners", "owners",
"roles",
"published", "published",
"roles",
"slug", "slug",
"changed_by",
) )
search_filters = { search_filters = {
"dashboard_title": [DashboardTitleOrSlugFilter], "dashboard_title": [DashboardTitleOrSlugFilter],
@ -173,7 +174,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
dashboard_get_response_schema = DashboardGetResponseSchema() dashboard_get_response_schema = DashboardGetResponseSchema()
dashboard_dataset_schema = DashboardDatasetSchema() dashboard_dataset_schema = DashboardDatasetSchema()
base_filters = [["slice", DashboardFilter, lambda: []]] base_filters = [["id", DashboardAccessFilter, lambda: []]]
order_rel_fields = { order_rel_fields = {
"slices": ("slice_name", "asc"), "slices": ("slice_name", "asc"),

View File

@ -18,16 +18,15 @@ import json
import logging import logging
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from flask_appbuilder.models.sqla.interface import SQLAInterface
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import contains_eager
from superset import security_manager
from superset.dao.base import BaseDAO from superset.dao.base import BaseDAO
from superset.dashboards.commands.exceptions import DashboardNotFoundError from superset.dashboards.commands.exceptions import DashboardNotFoundError
from superset.dashboards.filters import DashboardFilter from superset.dashboards.filters import DashboardAccessFilter
from superset.extensions import db from superset.extensions import db
from superset.models.core import FavStar, FavStarClassName from superset.models.core import FavStar, FavStarClassName
from superset.models.dashboard import Dashboard, id_or_slug_filter from superset.models.dashboard import Dashboard
from superset.models.slice import Slice from superset.models.slice import Slice
from superset.utils import core from superset.utils import core
from superset.utils.dashboard_filter_scopes_converter import copy_filter_scopes from superset.utils.dashboard_filter_scopes_converter import copy_filter_scopes
@ -37,42 +36,19 @@ logger = logging.getLogger(__name__)
class DashboardDAO(BaseDAO): class DashboardDAO(BaseDAO):
model_cls = Dashboard model_cls = Dashboard
base_filter = DashboardFilter base_filter = DashboardAccessFilter
@staticmethod @staticmethod
def get_by_id_or_slug(id_or_slug: str) -> Dashboard: def get_by_id_or_slug(id_or_slug: str) -> Dashboard:
query = ( dashboard = Dashboard.get(id_or_slug)
db.session.query(Dashboard)
.filter(id_or_slug_filter(id_or_slug))
.outerjoin(Slice, Dashboard.slices)
.outerjoin(Slice.table)
.outerjoin(Dashboard.owners)
.outerjoin(Dashboard.roles)
)
# Apply dashboard base filters
query = DashboardFilter("id", SQLAInterface(Dashboard, db.session)).apply(
query, None
)
dashboard = query.one_or_none()
if not dashboard: if not dashboard:
raise DashboardNotFoundError() raise DashboardNotFoundError()
security_manager.raise_for_dashboard_access(dashboard)
return dashboard return dashboard
@staticmethod @staticmethod
def get_datasets_for_dashboard(id_or_slug: str) -> List[Any]: def get_datasets_for_dashboard(id_or_slug: str) -> List[Any]:
query = ( dashboard = DashboardDAO.get_by_id_or_slug(id_or_slug)
db.session.query(Dashboard)
.filter(id_or_slug_filter(id_or_slug))
.outerjoin(Slice, Dashboard.slices)
.outerjoin(Slice.table)
)
# Apply dashboard base filters
query = DashboardFilter("id", SQLAInterface(Dashboard, db.session)).apply(
query, None
)
dashboard = query.one_or_none()
if not dashboard:
raise DashboardNotFoundError()
datasource_slices = core.indexed(dashboard.slices, "datasource") datasource_slices = core.indexed(dashboard.slices, "datasource")
data = [ data = [
datasource.data_for_slices(slices) datasource.data_for_slices(slices)
@ -83,22 +59,7 @@ class DashboardDAO(BaseDAO):
@staticmethod @staticmethod
def get_charts_for_dashboard(id_or_slug: str) -> List[Slice]: def get_charts_for_dashboard(id_or_slug: str) -> List[Slice]:
query = ( return DashboardDAO.get_by_id_or_slug(id_or_slug).slices
db.session.query(Dashboard)
.outerjoin(Slice, Dashboard.slices)
.outerjoin(Slice.table)
.filter(id_or_slug_filter(id_or_slug))
.options(contains_eager(Dashboard.slices))
)
# Apply dashboard base filters
query = DashboardFilter("id", SQLAInterface(Dashboard, db.session)).apply(
query, None
)
dashboard = query.one_or_none()
if not dashboard:
raise DashboardNotFoundError()
return dashboard.slices
@staticmethod @staticmethod
def validate_slug_uniqueness(slug: str) -> bool: def validate_slug_uniqueness(slug: str) -> bool:

View File

@ -57,16 +57,16 @@ class DashboardFavoriteFilter(BaseFavoriteFilter):
model = Dashboard model = Dashboard
class DashboardFilter(BaseFilter): class DashboardAccessFilter(BaseFilter):
""" """
List dashboards with the following criteria: List dashboards with the following criteria:
1. Those which the user owns 1. Those which the user owns
2. Those which the user has favorited 2. Those which the user has favorited
3. Those which have been published (if they have access to at least one slice) 3. Those which have been published (if they have access to at least one slice)
If the user is an admin show them all dashboards. If the user is an admin then show all dashboards.
This means they do not get curation but can still sort by "published" This means they do not get curation but can still sort by "published"
if they wish to see those dashboards which are published first if they wish to see those dashboards which are published first.
""" """
def apply(self, query: Query, value: Any) -> Query: def apply(self, query: Query, value: Any) -> Query:

View File

@ -184,6 +184,12 @@ class Dashboard( # pylint: disable=too-many-instance-attributes
meta = MetaData(bind=self.get_sqla_engine()) meta = MetaData(bind=self.get_sqla_engine())
meta.reflect() meta.reflect()
@property
def status(self) -> utils.DashboardStatus:
if self.published:
return utils.DashboardStatus.PUBLISHED
return utils.DashboardStatus.DRAFT
@renders("dashboard_title") @renders("dashboard_title")
def dashboard_link(self) -> Markup: def dashboard_link(self) -> Markup:
title = escape(self.dashboard_title or "<empty>") title = escape(self.dashboard_title or "<empty>")

View File

@ -25,7 +25,7 @@ from marshmallow import ValidationError
from superset.charts.filters import ChartFilter from superset.charts.filters import ChartFilter
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.dashboards.filters import DashboardFilter from superset.dashboards.filters import DashboardAccessFilter
from superset.databases.filters import DatabaseFilter from superset.databases.filters import DatabaseFilter
from superset.models.reports import ReportSchedule from superset.models.reports import ReportSchedule
from superset.reports.commands.bulk_delete import BulkDeleteReportScheduleCommand from superset.reports.commands.bulk_delete import BulkDeleteReportScheduleCommand
@ -170,7 +170,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
allowed_rel_fields = {"owners", "chart", "dashboard", "database", "created_by"} allowed_rel_fields = {"owners", "chart", "dashboard", "database", "created_by"}
filter_rel_fields = { filter_rel_fields = {
"chart": [["id", ChartFilter, lambda: []]], "chart": [["id", ChartFilter, lambda: []]],
"dashboard": [["id", DashboardFilter, lambda: []]], "dashboard": [["id", DashboardAccessFilter, lambda: []]],
"database": [["id", DatabaseFilter, lambda: []]], "database": [["id", DatabaseFilter, lambda: []]],
} }
text_field_rel_fields = { text_field_rel_fields = {

View File

@ -1139,7 +1139,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
@staticmethod @staticmethod
def can_access_based_on_dashboard(datasource: "BaseDatasource") -> bool: def can_access_based_on_dashboard(datasource: "BaseDatasource") -> bool:
from superset import db from superset import db
from superset.dashboards.filters import DashboardFilter from superset.dashboards.filters import DashboardAccessFilter
from superset.models.slice import Slice from superset.models.slice import Slice
from superset.models.dashboard import Dashboard from superset.models.dashboard import Dashboard
@ -1150,7 +1150,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
.filter(datasource_class.id == datasource.id) .filter(datasource_class.id == datasource.id)
) )
query = DashboardFilter("id", SQLAInterface(Dashboard, db.session)).apply( query = DashboardAccessFilter("id", SQLAInterface(Dashboard, db.session)).apply(
query, None query, None
) )

View File

@ -263,6 +263,13 @@ class QueryStatus(str, Enum): # pylint: disable=too-few-public-methods
TIMED_OUT: str = "timed_out" TIMED_OUT: str = "timed_out"
class DashboardStatus(str, Enum):
"""Dashboard status used for frontend filters"""
PUBLISHED = "published"
DRAFT = "draft"
class ReservedUrlParameters(str, Enum): class ReservedUrlParameters(str, Enum):
""" """
Reserved URL parameters that are used internally by Superset. These will not be Reserved URL parameters that are used internally by Superset. These will not be

View File

@ -17,7 +17,7 @@
from flask import Markup from flask import Markup
from flask_babel import lazy_gettext as _ from flask_babel import lazy_gettext as _
from superset.dashboards.filters import DashboardFilter from superset.dashboards.filters import DashboardAccessFilter
from superset.views.chart.filters import SliceFilter from superset.views.chart.filters import SliceFilter
@ -88,6 +88,6 @@ class SliceMixin: # pylint: disable=too-few-public-methods
"viz_type": _("Visualization Type"), "viz_type": _("Visualization Type"),
} }
add_form_query_rel_fields = {"dashboards": [["name", DashboardFilter, None]]} add_form_query_rel_fields = {"dashboards": [["name", DashboardAccessFilter, None]]}
edit_form_query_rel_fields = add_form_query_rel_fields edit_form_query_rel_fields = add_form_query_rel_fields

View File

@ -16,7 +16,7 @@
# under the License. # under the License.
from flask_babel import lazy_gettext as _ from flask_babel import lazy_gettext as _
from ...dashboards.filters import DashboardFilter from ...dashboards.filters import DashboardAccessFilter
from ..base import check_ownership from ..base import check_ownership
@ -73,7 +73,7 @@ class DashboardMixin: # pylint: disable=too-few-public-methods
"visible in the list of all dashboards" "visible in the list of all dashboards"
), ),
} }
base_filters = [["slice", DashboardFilter, lambda: []]] base_filters = [["slice", DashboardAccessFilter, lambda: []]]
label_columns = { label_columns = {
"dashboard_link": _("Dashboard"), "dashboard_link": _("Dashboard"),
"dashboard_title": _("Title"), "dashboard_title": _("Title"),

View File

@ -437,11 +437,11 @@ class TestDashboard(SupersetTestCase):
self.test_save_dash("alpha") self.test_save_dash("alpha")
@pytest.mark.usefixtures("load_energy_table_with_slice", "load_dashboard") @pytest.mark.usefixtures("load_energy_table_with_slice", "load_dashboard")
def test_users_can_view_published_dashboard(self): def test_users_can_list_published_dashboard(self):
self.login("alpha") self.login("alpha")
resp = self.get_resp("/api/v1/dashboard/") resp = self.get_resp("/api/v1/dashboard/")
self.assertNotIn(f"/superset/dashboard/{pytest.hidden_dash_slug}/", resp) assert f"/superset/dashboard/{pytest.hidden_dash_slug}/" not in resp
self.assertIn(f"/superset/dashboard/{pytest.published_dash_slug}/", resp) assert f"/superset/dashboard/{pytest.published_dash_slug}/" in resp
def test_users_can_view_own_dashboard(self): def test_users_can_view_own_dashboard(self):
user = security_manager.find_user("gamma") user = security_manager.find_user("gamma")

View File

@ -190,11 +190,14 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices") @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
def test_get_dashboard_datasets_not_allowed(self): def test_get_draft_dashboard_datasets(self):
"""
All users should have access to dashboards without roles
"""
self.login(username="gamma") self.login(username="gamma")
uri = "api/v1/dashboard/world_health/datasets" uri = "api/v1/dashboard/world_health/datasets"
response = self.get_assert_metric(uri, "get_datasets") response = self.get_assert_metric(uri, "get_datasets")
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 200)
@pytest.mark.usefixtures("create_dashboards") @pytest.mark.usefixtures("create_dashboards")
def get_dashboard_by_slug(self): def get_dashboard_by_slug(self):
@ -215,12 +218,15 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
@pytest.mark.usefixtures("create_dashboards") @pytest.mark.usefixtures("create_dashboards")
def get_dashboard_by_slug_not_allowed(self): def get_draft_dashboard_by_slug(self):
"""
All users should have access to dashboards without roles
"""
self.login(username="gamma") self.login(username="gamma")
dashboard = self.dashboards[0] dashboard = self.dashboards[0]
uri = f"api/v1/dashboard/{dashboard.slug}" uri = f"api/v1/dashboard/{dashboard.slug}"
response = self.get_assert_metric(uri, "get") response = self.get_assert_metric(uri, "get")
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 200)
@pytest.mark.usefixtures("create_dashboards") @pytest.mark.usefixtures("create_dashboards")
def test_get_dashboard_charts(self): def test_get_dashboard_charts(self):
@ -266,15 +272,15 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
@pytest.mark.usefixtures("create_dashboards") @pytest.mark.usefixtures("create_dashboards")
def test_get_dashboard_charts_not_allowed(self): def test_get_draft_dashboard_charts(self):
""" """
Dashboard API: Test getting charts on a dashboard a user does not have access to All users should have access to draft dashboards without roles
""" """
self.login(username="gamma") self.login(username="gamma")
dashboard = self.dashboards[0] dashboard = self.dashboards[0]
uri = f"api/v1/dashboard/{dashboard.id}/charts" uri = f"api/v1/dashboard/{dashboard.id}/charts"
response = self.get_assert_metric(uri, "get_charts") response = self.get_assert_metric(uri, "get_charts")
self.assertEqual(response.status_code, 404) assert response.status_code == 200
@pytest.mark.usefixtures("create_dashboards") @pytest.mark.usefixtures("create_dashboards")
def test_get_dashboard_charts_empty(self): def test_get_dashboard_charts_empty(self):
@ -382,7 +388,7 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
self.login(username="gamma") self.login(username="gamma")
uri = f"api/v1/dashboard/{dashboard.id}" uri = f"api/v1/dashboard/{dashboard.id}"
rv = self.client.get(uri) rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404) self.assertEqual(rv.status_code, 200)
# rollback changes # rollback changes
db.session.delete(dashboard) db.session.delete(dashboard)
db.session.commit() db.session.commit()

View File

@ -208,20 +208,6 @@ class TestDashboardDatasetSecurity(DashboardTestCase):
finally: finally:
self.revoke_public_access_to_table(accessed_table) self.revoke_public_access_to_table(accessed_table)
def test_get_dashboard_api_no_data_access(self):
"""
Dashboard API: Test get dashboard without data access
"""
admin = self.get_user("admin")
dashboard = create_dashboard_to_db(
random_title(), random_slug(), owners=[admin]
)
self.login(username="gamma")
uri = DASHBOARD_API_URL_FORMAT.format(dashboard.id)
rv = self.client.get(uri)
self.assert404(rv)
def test_get_dashboards_api_no_data_access(self): def test_get_dashboards_api_no_data_access(self):
""" """
Dashboard API: Test get dashboards no data access Dashboard API: Test get dashboards no data access