feat: Adds option to disable drill to detail per database (#27536)

This commit is contained in:
Michael S. Molina 2024-03-21 15:51:09 -03:00 committed by GitHub
parent fcceaf081c
commit 6e528426dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 170 additions and 106 deletions

View File

@ -3613,6 +3613,9 @@
"disable_data_preview": { "disable_data_preview": {
"type": "boolean" "type": "boolean"
}, },
"disable_drill_to_detail": {
"type": "boolean"
},
"explore_database_id": { "explore_database_id": {
"type": "integer" "type": "integer"
}, },

View File

@ -285,11 +285,11 @@ test('context menu for supported chart, no dimensions, no filters', async () =>
isContextMenu: true, isContextMenu: true,
}); });
await expectDrillToDetailDisabled( const message =
'Drill to detail is disabled because this chart does not group data by dimension value.', 'Drill to detail is disabled because this chart does not group data by dimension value.';
);
await expectDrillToDetailByDisabled(); await expectDrillToDetailDisabled(message);
await expectDrillToDetailByDisabled(message);
}); });
test('context menu for supported chart, no dimensions, 1 filter', async () => { test('context menu for supported chart, no dimensions, 1 filter', async () => {
@ -299,11 +299,11 @@ test('context menu for supported chart, no dimensions, 1 filter', async () => {
filters: [filterA], filters: [filterA],
}); });
await expectDrillToDetailDisabled( const message =
'Drill to detail is disabled because this chart does not group data by dimension value.', 'Drill to detail is disabled because this chart does not group data by dimension value.';
);
await expectDrillToDetailByDisabled(); await expectDrillToDetailDisabled(message);
await expectDrillToDetailByDisabled(message);
}); });
test('dropdown menu for supported chart, dimensions', async () => { test('dropdown menu for supported chart, dimensions', async () => {

View File

@ -30,13 +30,30 @@ import {
styled, styled,
t, t,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { useSelector } from 'react-redux';
import { Menu } from 'src/components/Menu'; import { Menu } from 'src/components/Menu';
import { RootState } from 'src/dashboard/types';
import DrillDetailModal from './DrillDetailModal'; import DrillDetailModal from './DrillDetailModal';
import { getSubmenuYOffset } from '../utils'; import { getSubmenuYOffset } from '../utils';
import { MenuItemTooltip } from '../DisabledMenuItemTooltip'; import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
import { MenuItemWithTruncation } from '../MenuItemWithTruncation'; import { MenuItemWithTruncation } from '../MenuItemWithTruncation';
const DRILL_TO_DETAIL_TEXT = t('Drill to detail by'); const DRILL_TO_DETAIL = t('Drill to detail');
const DRILL_TO_DETAIL_BY = t('Drill to detail by');
const DISABLED_REASONS = {
DATABASE: t(
'Drill to detail is disabled for this database. Change the database settings to enable it.',
),
NO_AGGREGATIONS: t(
'Drill to detail is disabled because this chart does not group data by dimension value.',
),
NO_FILTERS: t(
'Right-click on a dimension value to drill to detail by that value.',
),
NOT_SUPPORTED: t(
'Drill to detail by value is not yet supported for this chart type.',
),
};
const DisabledMenuItem = ({ children, ...props }: { children: ReactNode }) => ( const DisabledMenuItem = ({ children, ...props }: { children: ReactNode }) => (
<Menu.Item disabled {...props}> <Menu.Item disabled {...props}>
@ -94,6 +111,11 @@ const DrillDetailMenuItems = ({
submenuIndex = 0, submenuIndex = 0,
...props ...props
}: DrillDetailMenuItemsProps) => { }: DrillDetailMenuItemsProps) => {
const drillToDetailDisabled = useSelector<RootState, boolean | undefined>(
({ datasources }) =>
datasources[formData.datasource]?.database?.disable_drill_to_detail,
);
const [modalFilters, setFilters] = useState<BinaryQueryObjectFilterClause[]>( const [modalFilters, setFilters] = useState<BinaryQueryObjectFilterClause[]>(
[], [],
); );
@ -132,52 +154,6 @@ const DrillDetailMenuItems = ({
return isEmpty(metrics); return isEmpty(metrics);
}, [formData]); }, [formData]);
let drillToDetailMenuItem;
if (handlesDimensionContextMenu && noAggregations) {
drillToDetailMenuItem = (
<DisabledMenuItem {...props} key="drill-detail-no-aggregations">
{t('Drill to detail')}
<MenuItemTooltip
title={t(
'Drill to detail is disabled because this chart does not group data by dimension value.',
)}
/>
</DisabledMenuItem>
);
} else {
drillToDetailMenuItem = (
<Menu.Item
{...props}
key="drill-detail-no-filters"
onClick={openModal.bind(null, [])}
>
{t('Drill to detail')}
</Menu.Item>
);
}
let drillToDetailByMenuItem;
if (!handlesDimensionContextMenu) {
drillToDetailByMenuItem = (
<DisabledMenuItem {...props} key="drill-detail-by-chart-not-supported">
{DRILL_TO_DETAIL_TEXT}
<MenuItemTooltip
title={t(
'Drill to detail by value is not yet supported for this chart type.',
)}
/>
</DisabledMenuItem>
);
}
if (handlesDimensionContextMenu && noAggregations) {
drillToDetailByMenuItem = (
<DisabledMenuItem {...props} key="drill-detail-by-no-aggregations">
{DRILL_TO_DETAIL_TEXT}
</DisabledMenuItem>
);
}
// Ensure submenu doesn't appear offscreen // Ensure submenu doesn't appear offscreen
const submenuYOffset = useMemo( const submenuYOffset = useMemo(
() => () =>
@ -189,55 +165,76 @@ const DrillDetailMenuItems = ({
[contextMenuY, filters.length, submenuIndex], [contextMenuY, filters.length, submenuIndex],
); );
if (handlesDimensionContextMenu && !noAggregations && filters?.length) { let drillDisabled;
drillToDetailByMenuItem = ( let drillByDisabled;
<Menu.SubMenu if (drillToDetailDisabled) {
{...props} drillDisabled = DISABLED_REASONS.DATABASE;
popupOffset={[0, submenuYOffset]} drillByDisabled = DISABLED_REASONS.DATABASE;
popupClassName="chart-context-submenu" } else if (handlesDimensionContextMenu) {
title={DRILL_TO_DETAIL_TEXT} if (noAggregations) {
> drillDisabled = DISABLED_REASONS.NO_AGGREGATIONS;
<div data-test="drill-to-detail-by-submenu"> drillByDisabled = DISABLED_REASONS.NO_AGGREGATIONS;
{filters.map((filter, i) => ( } else if (!filters?.length) {
<MenuItemWithTruncation drillByDisabled = DISABLED_REASONS.NO_FILTERS;
{...props} }
tooltipText={`${DRILL_TO_DETAIL_TEXT} ${filter.formattedVal}`} } else {
key={`drill-detail-filter-${i}`} drillByDisabled = DISABLED_REASONS.NOT_SUPPORTED;
onClick={openModal.bind(null, [filter])}
>
{`${DRILL_TO_DETAIL_TEXT} `}
<StyledFilter stripHTML>{filter.formattedVal}</StyledFilter>
</MenuItemWithTruncation>
))}
{filters.length > 1 && (
<Menu.Item
{...props}
key="drill-detail-filter-all"
onClick={openModal.bind(null, filters)}
>
<div>
{`${DRILL_TO_DETAIL_TEXT} `}
<StyledFilter stripHTML={false}>{t('all')}</StyledFilter>
</div>
</Menu.Item>
)}
</div>
</Menu.SubMenu>
);
} }
if (handlesDimensionContextMenu && !noAggregations && !filters?.length) { const drillToDetailMenuItem = drillDisabled ? (
drillToDetailByMenuItem = ( <DisabledMenuItem {...props} key="drill-to-detail-disabled">
<DisabledMenuItem {...props} key="drill-detail-by-select-aggregation"> {DRILL_TO_DETAIL}
{DRILL_TO_DETAIL_TEXT} <MenuItemTooltip title={drillDisabled} />
<MenuItemTooltip </DisabledMenuItem>
title={t( ) : (
'Right-click on a dimension value to drill to detail by that value.', <Menu.Item
)} {...props}
/> key="drill-to-detail"
</DisabledMenuItem> onClick={openModal.bind(null, [])}
); >
} {DRILL_TO_DETAIL}
</Menu.Item>
);
const drillToDetailByMenuItem = drillByDisabled ? (
<DisabledMenuItem {...props} key="drill-to-detail-by-disabled">
{DRILL_TO_DETAIL_BY}
<MenuItemTooltip title={drillByDisabled} />
</DisabledMenuItem>
) : (
<Menu.SubMenu
{...props}
popupOffset={[0, submenuYOffset]}
popupClassName="chart-context-submenu"
title={DRILL_TO_DETAIL_BY}
>
<div data-test="drill-to-detail-by-submenu">
{filters.map((filter, i) => (
<MenuItemWithTruncation
{...props}
tooltipText={`${DRILL_TO_DETAIL_BY} ${filter.formattedVal}`}
key={`drill-detail-filter-${i}`}
onClick={openModal.bind(null, [filter])}
>
{`${DRILL_TO_DETAIL_BY} `}
<StyledFilter stripHTML>{filter.formattedVal}</StyledFilter>
</MenuItemWithTruncation>
))}
{filters.length > 1 && (
<Menu.Item
{...props}
key="drill-detail-filter-all"
onClick={openModal.bind(null, filters)}
>
<div>
{`${DRILL_TO_DETAIL_BY} `}
<StyledFilter stripHTML={false}>{t('all')}</StyledFilter>
</div>
</Menu.Item>
)}
</div>
</Menu.SubMenu>
);
return ( return (
<> <>

View File

@ -62,6 +62,7 @@ const fakeDatabaseApiResult = {
allows_subquery: 'Allows Subquery', allows_subquery: 'Allows Subquery',
allows_virtual_table_explore: 'Allows Virtual Table Explore', allows_virtual_table_explore: 'Allows Virtual Table Explore',
disable_data_preview: 'Disables SQL Lab Data Preview', disable_data_preview: 'Disables SQL Lab Data Preview',
disable_drill_to_detail: 'Disable Drill To Detail',
backend: 'Backend', backend: 'Backend',
changed_on: 'Changed On', changed_on: 'Changed On',
changed_on_delta_humanized: 'Changed On Delta Humanized', changed_on_delta_humanized: 'Changed On Delta Humanized',
@ -83,6 +84,7 @@ const fakeDatabaseApiResult = {
'allows_subquery', 'allows_subquery',
'allows_virtual_table_explore', 'allows_virtual_table_explore',
'disable_data_preview', 'disable_data_preview',
'disable_drill_to_detail',
'backend', 'backend',
'changed_on', 'changed_on',
'changed_on_delta_humanized', 'changed_on_delta_humanized',
@ -116,6 +118,7 @@ const fakeDatabaseApiResult = {
allows_subquery: true, allows_subquery: true,
allows_virtual_table_explore: true, allows_virtual_table_explore: true,
disable_data_preview: false, disable_data_preview: false,
disable_drill_to_detail: false,
backend: 'postgresql', backend: 'postgresql',
changed_on: '2021-03-09T19:02:07.141095', changed_on: '2021-03-09T19:02:07.141095',
changed_on_delta_humanized: 'a day ago', changed_on_delta_humanized: 'a day ago',
@ -136,6 +139,7 @@ const fakeDatabaseApiResult = {
allows_subquery: true, allows_subquery: true,
allows_virtual_table_explore: true, allows_virtual_table_explore: true,
disable_data_preview: false, disable_data_preview: false,
disable_drill_to_detail: false,
backend: 'mysql', backend: 'mysql',
changed_on: '2021-03-09T19:02:07.141095', changed_on: '2021-03-09T19:02:07.141095',
changed_on_delta_humanized: 'a day ago', changed_on_delta_humanized: 'a day ago',

View File

@ -29,6 +29,7 @@ import {
import { Dataset } from '@superset-ui/chart-controls'; import { Dataset } from '@superset-ui/chart-controls';
import { chart } from 'src/components/Chart/chartReducer'; import { chart } from 'src/components/Chart/chartReducer';
import componentTypes from 'src/dashboard/util/componentTypes'; import componentTypes from 'src/dashboard/util/componentTypes';
import Database from 'src/types/Database';
import { UrlParamEntries } from 'src/utils/urlUtils'; import { UrlParamEntries } from 'src/utils/urlUtils';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
@ -143,6 +144,7 @@ export type Datasource = Dataset & {
uid: string; uid: string;
column_types: GenericDataType[]; column_types: GenericDataType[];
table_name: string; table_name: string;
database?: Database;
}; };
export type DatasourcesState = { export type DatasourcesState = {
[key: string]: Datasource; [key: string]: Datasource;

View File

@ -571,6 +571,22 @@ const ExtraOptions = ({
)} )}
</div> </div>
</StyledInputContainer> </StyledInputContainer>
<StyledInputContainer css={no_margin_bottom}>
<div className="input-container">
<IndeterminateCheckbox
id="disable_drill_to_detail"
indeterminate={false}
checked={!!extraJson?.disable_drill_to_detail}
onChange={onExtraInputChange}
labelText={t('Disable drill to detail')}
/>
<InfoTooltip
tooltip={t(
'Disables the drill to detail feature for this database.',
)}
/>
</div>
</StyledInputContainer>
</Collapse.Panel> </Collapse.Panel>
</Collapse> </Collapse>
); );

View File

@ -222,6 +222,7 @@ export interface ExtraJson {
cancel_query_on_windows_unload?: boolean; // in Performance cancel_query_on_windows_unload?: boolean; // in Performance
cost_estimate_enabled?: boolean; // in SQL Lab cost_estimate_enabled?: boolean; // in SQL Lab
disable_data_preview?: boolean; // in SQL Lab disable_data_preview?: boolean; // in SQL Lab
disable_drill_to_detail?: boolean;
engine_params?: { engine_params?: {
catalog?: Record<string, string>; catalog?: Record<string, string>;
connect_args?: { connect_args?: {

View File

@ -43,6 +43,7 @@ beforeEach(() => {
allows_subquery: 'Allows Subquery', allows_subquery: 'Allows Subquery',
allows_virtual_table_explore: 'Allows Virtual Table Explore', allows_virtual_table_explore: 'Allows Virtual Table Explore',
disable_data_preview: 'Disables SQL Lab Data Preview', disable_data_preview: 'Disables SQL Lab Data Preview',
disable_drill_to_detail: 'Disable Drill To Detail',
backend: 'Backend', backend: 'Backend',
changed_on: 'Changed On', changed_on: 'Changed On',
changed_on_delta_humanized: 'Changed On Delta Humanized', changed_on_delta_humanized: 'Changed On Delta Humanized',
@ -65,6 +66,7 @@ beforeEach(() => {
'allows_subquery', 'allows_subquery',
'allows_virtual_table_explore', 'allows_virtual_table_explore',
'disable_data_preview', 'disable_data_preview',
'disable_drill_to_detail',
'backend', 'backend',
'changed_on', 'changed_on',
'changed_on_delta_humanized', 'changed_on_delta_humanized',
@ -99,6 +101,7 @@ beforeEach(() => {
allows_subquery: true, allows_subquery: true,
allows_virtual_table_explore: true, allows_virtual_table_explore: true,
disable_data_preview: false, disable_data_preview: false,
disable_drill_to_detail: false,
backend: 'postgresql', backend: 'postgresql',
changed_on: '2021-03-09T19:02:07.141095', changed_on: '2021-03-09T19:02:07.141095',
changed_on_delta_humanized: 'a day ago', changed_on_delta_humanized: 'a day ago',
@ -120,6 +123,7 @@ beforeEach(() => {
allows_subquery: true, allows_subquery: true,
allows_virtual_table_explore: true, allows_virtual_table_explore: true,
disable_data_preview: false, disable_data_preview: false,
disable_drill_to_detail: false,
backend: 'mysql', backend: 'mysql',
changed_on: '2021-03-09T19:02:07.141095', changed_on: '2021-03-09T19:02:07.141095',
changed_on_delta_humanized: 'a day ago', changed_on_delta_humanized: 'a day ago',

View File

@ -28,4 +28,5 @@ export default interface Database {
sqlalchemy_uri: string; sqlalchemy_uri: string;
catalog: object; catalog: object;
parameters: any; parameters: any;
disable_drill_to_detail?: boolean;
} }

View File

@ -215,6 +215,7 @@ class DatabaseSchema(Schema):
allows_cost_estimate = fields.Bool() allows_cost_estimate = fields.Bool()
allows_virtual_table_explore = fields.Bool() allows_virtual_table_explore = fields.Bool()
disable_data_preview = fields.Bool() disable_data_preview = fields.Bool()
disable_drill_to_detail = fields.Bool()
explore_database_id = fields.Int() explore_database_id = fields.Int()

View File

@ -176,6 +176,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
"id", "id",
"uuid", "uuid",
"disable_data_preview", "disable_data_preview",
"disable_drill_to_detail",
"engine_information", "engine_information",
] ]
add_columns = [ add_columns = [

View File

@ -132,7 +132,9 @@ extra_description = markdown(
"5. The ``allows_virtual_table_explore`` field is a boolean specifying " "5. The ``allows_virtual_table_explore`` field is a boolean specifying "
"whether or not the Explore button in SQL Lab results is shown.<br/>" "whether or not the Explore button in SQL Lab results is shown.<br/>"
"6. The ``disable_data_preview`` field is a boolean specifying whether or not data " "6. The ``disable_data_preview`` field is a boolean specifying whether or not data "
"preview queries will be run when fetching table metadata in SQL Lab.", "preview queries will be run when fetching table metadata in SQL Lab."
"7. The ``disable_drill_to_detail`` field is a boolean specifying whether or not"
"drill to detail is disabled for the database.",
True, True,
) )
get_export_ids_schema = {"type": "array", "items": {"type": "integer"}} get_export_ids_schema = {"type": "array", "items": {"type": "integer"}}
@ -750,6 +752,7 @@ class ImportV1DatabaseExtraSchema(Schema):
allows_virtual_table_explore = fields.Boolean(required=False) allows_virtual_table_explore = fields.Boolean(required=False)
cancel_query_on_windows_unload = fields.Boolean(required=False) cancel_query_on_windows_unload = fields.Boolean(required=False)
disable_data_preview = fields.Boolean(required=False) disable_data_preview = fields.Boolean(required=False)
disable_drill_to_detail = fields.Boolean(required=False)
version = fields.String(required=False, allow_none=True) version = fields.String(required=False, allow_none=True)

View File

@ -91,7 +91,6 @@ DB_CONNECTION_MUTATOR = config["DB_CONNECTION_MUTATOR"]
class KeyValue(Model): # pylint: disable=too-few-public-methods class KeyValue(Model): # pylint: disable=too-few-public-methods
"""Used for any type of key-value store""" """Used for any type of key-value store"""
__tablename__ = "keyvalue" __tablename__ = "keyvalue"
@ -116,7 +115,6 @@ class ConfigurationMethod(StrEnum):
class Database( class Database(
Model, AuditMixinNullable, ImportExportMixin Model, AuditMixinNullable, ImportExportMixin
): # pylint: disable=too-many-public-methods ): # pylint: disable=too-many-public-methods
"""An ORM object that stores Database related information""" """An ORM object that stores Database related information"""
__tablename__ = "dbs" __tablename__ = "dbs"
@ -229,6 +227,11 @@ class Database(
# this will prevent any 'trash value' strings from going through # this will prevent any 'trash value' strings from going through
return self.get_extra().get("disable_data_preview", False) is True return self.get_extra().get("disable_data_preview", False) is True
@property
def disable_drill_to_detail(self) -> bool:
# this will prevent any 'trash value' strings from going through
return self.get_extra().get("disable_drill_to_detail", False) is True
@property @property
def schema_options(self) -> dict[str, Any]: def schema_options(self) -> dict[str, Any]:
"""Additional schema display config for engines with complex schemas""" """Additional schema display config for engines with complex schemas"""
@ -248,6 +251,7 @@ class Database(
"schema_options": self.schema_options, "schema_options": self.schema_options,
"parameters": self.parameters, "parameters": self.parameters,
"disable_data_preview": self.disable_data_preview, "disable_data_preview": self.disable_data_preview,
"disable_drill_to_detail": self.disable_drill_to_detail,
"parameters_schema": self.parameters_schema, "parameters_schema": self.parameters_schema,
"engine_information": self.engine_information, "engine_information": self.engine_information,
} }

View File

@ -38,6 +38,7 @@ DATABASE_KEYS = [
"force_ctas_schema", "force_ctas_schema",
"id", "id",
"disable_data_preview", "disable_data_preview",
"disable_drill_to_detail",
] ]

View File

@ -147,7 +147,9 @@ class DatabaseMixin:
"whether or not the Explore button in SQL Lab results is shown<br/>" "whether or not the Explore button in SQL Lab results is shown<br/>"
"6. The ``disable_data_preview`` field is a boolean specifying whether or" "6. The ``disable_data_preview`` field is a boolean specifying whether or"
"not data preview queries will be run when fetching table metadata in" "not data preview queries will be run when fetching table metadata in"
"SQL Lab.", "SQL Lab."
"7. The ``disable_drill_to_detail`` field is a boolean specifying whether or"
"not drill to detail is disabled for the database.",
True, True,
), ),
"encrypted_extra": utils.markdown( "encrypted_extra": utils.markdown(

View File

@ -1073,6 +1073,29 @@ class TestCore(SupersetTestCase):
database.extra = json.dumps(extra) database.extra = json.dumps(extra)
self.assertEqual(database.disable_data_preview, False) self.assertEqual(database.disable_data_preview, False)
def test_disable_drill_to_detail(self):
# test that disable_drill_to_detail is False by default
database = utils.get_example_database()
self.assertEqual(database.disable_drill_to_detail, False)
# test that disable_drill_to_detail can be set to True
extra = database.get_extra()
extra["disable_drill_to_detail"] = True
database.extra = json.dumps(extra)
self.assertEqual(database.disable_drill_to_detail, True)
# test that disable_drill_to_detail can be set to False
extra = database.get_extra()
extra["disable_drill_to_detail"] = False
database.extra = json.dumps(extra)
self.assertEqual(database.disable_drill_to_detail, False)
# test that disable_drill_to_detail is not broken with bad values
extra = database.get_extra()
extra["disable_drill_to_detail"] = "trash value"
database.extra = json.dumps(extra)
self.assertEqual(database.disable_drill_to_detail, False)
def test_explore_database_id(self): def test_explore_database_id(self):
database = superset.utils.database.get_example_database() database = superset.utils.database.get_example_database()
explore_database = superset.utils.database.get_example_database() explore_database = superset.utils.database.get_example_database()

View File

@ -204,6 +204,7 @@ class TestDatabaseApi(SupersetTestCase):
"created_by", "created_by",
"database_name", "database_name",
"disable_data_preview", "disable_data_preview",
"disable_drill_to_detail",
"engine_information", "engine_information",
"explore_database_id", "explore_database_id",
"expose_in_sqllab", "expose_in_sqllab",