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 cd67d3747472..70f53ab941a1 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 d23e66927773..166382a15ca7 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 30436abbc4fa..dad8b337f958 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 4a8061236955..2820a921e6e9 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 ed30e7885b92..e38b4d96eccf 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 44a4ac3ca002..9365c021e27a 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 28b9a522be5e..fa006ad42502 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);