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": {
"type": "boolean"
},
"disable_drill_to_detail": {
"type": "boolean"
},
"explore_database_id": {
"type": "integer"
},

View File

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

View File

@ -30,13 +30,30 @@ import {
styled,
t,
} from '@superset-ui/core';
import { useSelector } from 'react-redux';
import { Menu } from 'src/components/Menu';
import { RootState } from 'src/dashboard/types';
import DrillDetailModal from './DrillDetailModal';
import { getSubmenuYOffset } from '../utils';
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
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 }) => (
<Menu.Item disabled {...props}>
@ -94,6 +111,11 @@ const DrillDetailMenuItems = ({
submenuIndex = 0,
...props
}: DrillDetailMenuItemsProps) => {
const drillToDetailDisabled = useSelector<RootState, boolean | undefined>(
({ datasources }) =>
datasources[formData.datasource]?.database?.disable_drill_to_detail,
);
const [modalFilters, setFilters] = useState<BinaryQueryObjectFilterClause[]>(
[],
);
@ -132,52 +154,6 @@ const DrillDetailMenuItems = ({
return isEmpty(metrics);
}, [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
const submenuYOffset = useMemo(
() =>
@ -189,55 +165,76 @@ const DrillDetailMenuItems = ({
[contextMenuY, filters.length, submenuIndex],
);
if (handlesDimensionContextMenu && !noAggregations && filters?.length) {
drillToDetailByMenuItem = (
<Menu.SubMenu
{...props}
popupOffset={[0, submenuYOffset]}
popupClassName="chart-context-submenu"
title={DRILL_TO_DETAIL_TEXT}
>
<div data-test="drill-to-detail-by-submenu">
{filters.map((filter, i) => (
<MenuItemWithTruncation
{...props}
tooltipText={`${DRILL_TO_DETAIL_TEXT} ${filter.formattedVal}`}
key={`drill-detail-filter-${i}`}
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>
);
let drillDisabled;
let drillByDisabled;
if (drillToDetailDisabled) {
drillDisabled = DISABLED_REASONS.DATABASE;
drillByDisabled = DISABLED_REASONS.DATABASE;
} else if (handlesDimensionContextMenu) {
if (noAggregations) {
drillDisabled = DISABLED_REASONS.NO_AGGREGATIONS;
drillByDisabled = DISABLED_REASONS.NO_AGGREGATIONS;
} else if (!filters?.length) {
drillByDisabled = DISABLED_REASONS.NO_FILTERS;
}
} else {
drillByDisabled = DISABLED_REASONS.NOT_SUPPORTED;
}
if (handlesDimensionContextMenu && !noAggregations && !filters?.length) {
drillToDetailByMenuItem = (
<DisabledMenuItem {...props} key="drill-detail-by-select-aggregation">
{DRILL_TO_DETAIL_TEXT}
<MenuItemTooltip
title={t(
'Right-click on a dimension value to drill to detail by that value.',
)}
/>
</DisabledMenuItem>
);
}
const drillToDetailMenuItem = drillDisabled ? (
<DisabledMenuItem {...props} key="drill-to-detail-disabled">
{DRILL_TO_DETAIL}
<MenuItemTooltip title={drillDisabled} />
</DisabledMenuItem>
) : (
<Menu.Item
{...props}
key="drill-to-detail"
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 (
<>

View File

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

View File

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

View File

@ -571,6 +571,22 @@ const ExtraOptions = ({
)}
</div>
</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>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -132,7 +132,9 @@ extra_description = markdown(
"5. The ``allows_virtual_table_explore`` field is a boolean specifying "
"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 "
"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,
)
get_export_ids_schema = {"type": "array", "items": {"type": "integer"}}
@ -750,6 +752,7 @@ class ImportV1DatabaseExtraSchema(Schema):
allows_virtual_table_explore = fields.Boolean(required=False)
cancel_query_on_windows_unload = 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)

View File

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

View File

@ -38,6 +38,7 @@ DATABASE_KEYS = [
"force_ctas_schema",
"id",
"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/>"
"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.",
"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,
),
"encrypted_extra": utils.markdown(

View File

@ -1073,6 +1073,29 @@ class TestCore(SupersetTestCase):
database.extra = json.dumps(extra)
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):
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",
"database_name",
"disable_data_preview",
"disable_drill_to_detail",
"engine_information",
"explore_database_id",
"expose_in_sqllab",