diff --git a/superset-frontend/packages/superset-ui-core/src/ui-overrides/ExtensionsRegistry.ts b/superset-frontend/packages/superset-ui-core/src/ui-overrides/ExtensionsRegistry.ts index cd67d37474728..70f53ab941a1d 100644 --- a/superset-frontend/packages/superset-ui-core/src/ui-overrides/ExtensionsRegistry.ts +++ b/superset-frontend/packages/superset-ui-core/src/ui-overrides/ExtensionsRegistry.ts @@ -29,7 +29,7 @@ type ReturningDisplayable

= (props: P) => string | React.ReactElement; /** * This type defines all available extensions of Superset's default UI. - * Namespace the keys here to follow the form of 'some_domain.functonality.item'. + * Namespace the keys here to follow the form of 'some_domain.functionality.item'. * Take care to name your keys well, as the name describes what this extension point's role is in Superset. * * When defining a new option here, take care to keep any parameters to functions (or components) minimal. @@ -66,8 +66,48 @@ type RightMenuItemIconProps = { menuChild: MenuObjectChildProps; }; type DatabaseDeleteRelatedExtensionProps = { - databaseId: number; + database: object; }; +type DatasetDeleteRelatedExtensionProps = { + dataset: object; +}; + +/** + * Interface for extensions to database connections + */ +export interface DatabaseConnectionExtension { + /** + * Display title text for the extension show when creating a database connection + */ + title: string; + /** + * url or dataURI (recommended) of a logo to use in place of a title. title is fallback display if no logo is provided + */ + logo?: React.ComponentType; + /** + * Descriptive text displayed under the logo or title to provide user with more context about the configuration section + */ + description: React.ComponentType; + /** + * React component to render for display in the database connection configuration + */ + component: React.ComponentType; + /** + * Is the database extension enabled? + */ + enabled: () => boolean; + + /** + * Callback for onsave + */ + // TODO: we need to move the db types to superset-ui/core in order to import them correctly + onSave: (componentState: any, db: any) => any; + + /** + * Used for parent to store data + */ + onEdit?: (componentState: any) => void; +} export type Extensions = Partial<{ 'alertsreports.header.icon': React.ComponentType; @@ -83,7 +123,9 @@ export type Extensions = Partial<{ 'welcome.banner': React.ComponentType; 'welcome.main.replacement': React.ComponentType; 'ssh_tunnel.form.switch': React.ComponentType; + 'databaseconnection.extraOption': DatabaseConnectionExtension; 'database.delete.related': React.ComponentType; + 'dataset.delete.related': React.ComponentType; }>; /** diff --git a/superset-frontend/src/explore/components/controls/SelectControl.jsx b/superset-frontend/src/explore/components/controls/SelectControl.jsx index d23e66927773c..166382a15ca78 100644 --- a/superset-frontend/src/explore/components/controls/SelectControl.jsx +++ b/superset-frontend/src/explore/components/controls/SelectControl.jsx @@ -31,11 +31,14 @@ const propTypes = { disabled: PropTypes.bool, freeForm: PropTypes.bool, isLoading: PropTypes.bool, + mode: PropTypes.string, multi: PropTypes.bool, isMulti: PropTypes.bool, name: PropTypes.string.isRequired, onChange: PropTypes.func, onFocus: PropTypes.func, + onSelect: PropTypes.func, + onDeselect: PropTypes.func, value: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, @@ -174,12 +177,14 @@ export default class SelectControl extends React.PureComponent { label, multi, name, - placeholder, + notFoundContent, onFocus, + onSelect, + onDeselect, + placeholder, showHeader, - value, tokenSeparators, - notFoundContent, + value, // ControlHeader props description, renderTrigger, @@ -236,10 +241,12 @@ export default class SelectControl extends React.PureComponent { : true, header: showHeader && , loading: isLoading, - mode: isMulti || multi ? 'multiple' : 'single', + mode: this.props.mode || (isMulti || multi ? 'multiple' : 'single'), name: `select-${name}`, onChange: this.onChange, onFocus, + onSelect, + onDeselect, options: this.state.options, placeholder, sortComparator: this.props.sortComparator, diff --git a/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx b/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx index 30436abbc4fac..dad8b337f958d 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx +++ b/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx @@ -18,7 +18,11 @@ */ import React, { ChangeEvent, EventHandler } from 'react'; import cx from 'classnames'; -import { t, SupersetTheme } from '@superset-ui/core'; +import { + t, + SupersetTheme, + DatabaseConnectionExtension, +} from '@superset-ui/core'; import InfoTooltip from 'src/components/InfoTooltip'; import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox'; import Collapse from 'src/components/Collapse'; @@ -38,6 +42,7 @@ const ExtraOptions = ({ onEditorChange, onExtraInputChange, onExtraEditorChange, + extraExtension, }: { db: DatabaseObject | null; onInputChange: EventHandler>; @@ -45,6 +50,7 @@ const ExtraOptions = ({ onEditorChange: Function; onExtraInputChange: EventHandler>; onExtraEditorChange: Function; + extraExtension: DatabaseConnectionExtension | undefined; }) => { const expandableModalIsOpen = !!db?.expose_in_sqllab; const createAsOpen = !!(db?.allow_ctas || db?.allow_cvas); @@ -61,6 +67,10 @@ const ExtraOptions = ({ return value; }); + const ExtraExtensionComponent = extraExtension?.component; + const ExtraExtensionLogo = extraExtension?.logo; + const ExtensionDescription = extraExtension?.description; + return ( )} + {extraExtension && ExtraExtensionComponent && ExtensionDescription && ( + + {ExtraExtensionLogo && } + ({ + fontSize: theme.typography.sizes.l, + fontWeight: theme.typography.weights.bold, + })} + > + {extraExtension?.title} + +

+ +

+ + } + key={extraExtension?.title} + collapsible={extraExtension.enabled?.() ? 'header' : 'disabled'} + > + + + + + )} diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.tsx index 4a80612369557..2820a921e6e9c 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/index.tsx +++ b/superset-frontend/src/features/databases/DatabaseModal/index.tsx @@ -578,12 +578,31 @@ const DatabaseModal: FunctionComponent = ({ sshTunnelPrivateKeyPasswordFields, setSSHTunnelPrivateKeyPasswordFields, ] = useState([]); + const [extraExtensionComponentState, setExtraExtensionComponentState] = + useState({}); const SSHTunnelSwitchComponent = extensionsRegistry.get('ssh_tunnel.form.switch') ?? SSHTunnelSwitch; const [useSSHTunneling, setUseSSHTunneling] = useState(false); + let dbConfigExtraExtension = extensionsRegistry.get( + 'databaseconnection.extraOption', + ); + + if (dbConfigExtraExtension) { + // add method for db modal to store data + dbConfigExtraExtension = { + ...dbConfigExtraExtension, + onEdit: componentState => { + setExtraExtensionComponentState({ + ...extraExtensionComponentState, + ...componentState, + }); + }, + }; + } + const conf = useCommonConf(); const dbImages = getDatabaseImages(); const connectionAlert = getConnectionAlert(); @@ -715,6 +734,19 @@ const DatabaseModal: FunctionComponent = ({ }; const onSave = async () => { + let dbConfigExtraExtensionOnSaveError; + dbConfigExtraExtension + ?.onSave(extraExtensionComponentState, db) + .then(({ error }: { error: any }) => { + if (error) { + dbConfigExtraExtensionOnSaveError = error; + addDangerToast(error); + } + }); + if (dbConfigExtraExtensionOnSaveError) { + setLoading(false); + return; + } // Clone DB object const dbToUpdate = { ...(db || {}) }; @@ -803,6 +835,18 @@ const DatabaseModal: FunctionComponent = ({ ); if (result) { if (onDatabaseAdd) onDatabaseAdd(); + dbConfigExtraExtension + ?.onSave(extraExtensionComponentState, db) + .then(({ error }: { error: any }) => { + if (error) { + dbConfigExtraExtensionOnSaveError = error; + addDangerToast(error); + } + }); + if (dbConfigExtraExtensionOnSaveError) { + setLoading(false); + return; + } if (!editNewDb) { onClose(); addSuccessToast(t('Database settings updated')); @@ -817,6 +861,19 @@ const DatabaseModal: FunctionComponent = ({ if (dbId) { setHasConnectedDb(true); if (onDatabaseAdd) onDatabaseAdd(); + dbConfigExtraExtension + ?.onSave(extraExtensionComponentState, db) + .then(({ error }: { error: any }) => { + if (error) { + dbConfigExtraExtensionOnSaveError = error; + addDangerToast(error); + } + }); + if (dbConfigExtraExtensionOnSaveError) { + setLoading(false); + return; + } + if (useTabLayout) { // tab layout only has one step // so it should close immediately on save @@ -1596,6 +1653,7 @@ const DatabaseModal: FunctionComponent = ({ if (!editNewDb) { return ( onChange(ActionType.inputChange, { @@ -1807,6 +1865,7 @@ const DatabaseModal: FunctionComponent = ({ {t('Advanced')}} key="2"> onChange(ActionType.inputChange, { diff --git a/superset-frontend/src/features/databases/DatabaseModal/styles.ts b/superset-frontend/src/features/databases/DatabaseModal/styles.ts index ed30e7885b92f..e38b4d96eccf4 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/styles.ts +++ b/superset-frontend/src/features/databases/DatabaseModal/styles.ts @@ -573,6 +573,7 @@ export const StyledStickyHeader = styled.div` top: 0; z-index: ${({ theme }) => theme.zIndex.max}; background: ${({ theme }) => theme.colors.grayscale.light5}; + height: auto; `; export const StyledCatalogTable = styled.div` diff --git a/superset-frontend/src/pages/DatabaseList/index.tsx b/superset-frontend/src/pages/DatabaseList/index.tsx index 44a4ac3ca002d..9365c021e27ae 100644 --- a/superset-frontend/src/pages/DatabaseList/index.tsx +++ b/superset-frontend/src/pages/DatabaseList/index.tsx @@ -534,9 +534,9 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) { databaseCurrentlyDeleting.sqllab_tab_count, )}

- {DatabaseDeleteRelatedExtension && currentDatabase?.id && ( + {DatabaseDeleteRelatedExtension && ( )} diff --git a/superset-frontend/src/pages/DatasetList/index.tsx b/superset-frontend/src/pages/DatasetList/index.tsx index 28b9a522be5ed..fa006ad42502f 100644 --- a/superset-frontend/src/pages/DatasetList/index.tsx +++ b/superset-frontend/src/pages/DatasetList/index.tsx @@ -16,7 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { FeatureFlag, styled, SupersetClient, t } from '@superset-ui/core'; +import { + FeatureFlag, + getExtensionsRegistry, + styled, + SupersetClient, + t, +} from '@superset-ui/core'; import React, { FunctionComponent, useState, @@ -64,6 +70,11 @@ import { } from 'src/features/datasets/constants'; import DuplicateDatasetModal from 'src/features/datasets/DuplicateDatasetModal'; +const extensionsRegistry = getExtensionsRegistry(); +const DatasetDeleteRelatedExtension = extensionsRegistry.get( + 'dataset.delete.related', +); + const FlexRowContainer = styled.div` align-items: center; display: flex; @@ -707,12 +718,23 @@ const DatasetList: FunctionComponent = ({ {datasetCurrentlyDeleting && ( +

+ {t( + 'The dataset %s is linked to %s charts that appear on %s dashboards. Are you sure you want to continue? Deleting the dataset will break those objects.', + datasetCurrentlyDeleting.table_name, + datasetCurrentlyDeleting.chart_count, + datasetCurrentlyDeleting.dashboard_count, + )} +

+ {DatasetDeleteRelatedExtension && ( + + )} + + } onConfirm={() => { if (datasetCurrentlyDeleting) { handleDatasetDelete(datasetCurrentlyDeleting);