mirror of https://github.com/apache/superset.git
feat: database extension registry (#23174)
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com> Co-authored-by: Lily Kuang <lily@preset.io>
This commit is contained in:
parent
e856e35c53
commit
6b5459121f
|
@ -29,7 +29,7 @@ type ReturningDisplayable<P = void> = (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<any>;
|
||||
/**
|
||||
* Descriptive text displayed under the logo or title to provide user with more context about the configuration section
|
||||
*/
|
||||
description: React.ComponentType<any>;
|
||||
/**
|
||||
* React component to render for display in the database connection configuration
|
||||
*/
|
||||
component: React.ComponentType<any>;
|
||||
/**
|
||||
* 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<SwitchProps>;
|
||||
'databaseconnection.extraOption': DatabaseConnectionExtension;
|
||||
'database.delete.related': React.ComponentType<DatabaseDeleteRelatedExtensionProps>;
|
||||
'dataset.delete.related': React.ComponentType<DatasetDeleteRelatedExtensionProps>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
onFocus,
|
||||
showHeader,
|
||||
value,
|
||||
tokenSeparators,
|
||||
notFoundContent,
|
||||
onFocus,
|
||||
onSelect,
|
||||
onDeselect,
|
||||
placeholder,
|
||||
showHeader,
|
||||
tokenSeparators,
|
||||
value,
|
||||
// ControlHeader props
|
||||
description,
|
||||
renderTrigger,
|
||||
|
@ -236,10 +241,12 @@ export default class SelectControl extends React.PureComponent {
|
|||
: true,
|
||||
header: showHeader && <ControlHeader {...headerProps} />,
|
||||
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,
|
||||
|
|
|
@ -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<ChangeEvent<HTMLInputElement>>;
|
||||
|
@ -45,6 +50,7 @@ const ExtraOptions = ({
|
|||
onEditorChange: Function;
|
||||
onExtraInputChange: EventHandler<ChangeEvent<HTMLInputElement>>;
|
||||
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 (
|
||||
<Collapse
|
||||
expandIconPosition="right"
|
||||
|
@ -437,6 +447,32 @@ const ExtraOptions = ({
|
|||
</StyledInputContainer>
|
||||
)}
|
||||
</Collapse.Panel>
|
||||
{extraExtension && ExtraExtensionComponent && ExtensionDescription && (
|
||||
<Collapse.Panel
|
||||
header={
|
||||
<div>
|
||||
{ExtraExtensionLogo && <ExtraExtensionLogo />}
|
||||
<span
|
||||
css={(theme: SupersetTheme) => ({
|
||||
fontSize: theme.typography.sizes.l,
|
||||
fontWeight: theme.typography.weights.bold,
|
||||
})}
|
||||
>
|
||||
{extraExtension?.title}
|
||||
</span>
|
||||
<p className="helper">
|
||||
<ExtensionDescription />
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
key={extraExtension?.title}
|
||||
collapsible={extraExtension.enabled?.() ? 'header' : 'disabled'}
|
||||
>
|
||||
<StyledInputContainer css={no_margin_bottom}>
|
||||
<ExtraExtensionComponent db={db} onEdit={extraExtension.onEdit} />
|
||||
</StyledInputContainer>
|
||||
</Collapse.Panel>
|
||||
)}
|
||||
<Collapse.Panel
|
||||
header={
|
||||
<div>
|
||||
|
|
|
@ -578,12 +578,31 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||
sshTunnelPrivateKeyPasswordFields,
|
||||
setSSHTunnelPrivateKeyPasswordFields,
|
||||
] = useState<string[]>([]);
|
||||
const [extraExtensionComponentState, setExtraExtensionComponentState] =
|
||||
useState<object>({});
|
||||
|
||||
const SSHTunnelSwitchComponent =
|
||||
extensionsRegistry.get('ssh_tunnel.form.switch') ?? SSHTunnelSwitch;
|
||||
|
||||
const [useSSHTunneling, setUseSSHTunneling] = useState<boolean>(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<DatabaseModalProps> = ({
|
|||
};
|
||||
|
||||
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<DatabaseModalProps> = ({
|
|||
);
|
||||
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<DatabaseModalProps> = ({
|
|||
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<DatabaseModalProps> = ({
|
|||
if (!editNewDb) {
|
||||
return (
|
||||
<ExtraOptions
|
||||
extraExtension={dbConfigExtraExtension}
|
||||
db={db as DatabaseObject}
|
||||
onInputChange={({ target }: { target: HTMLInputElement }) =>
|
||||
onChange(ActionType.inputChange, {
|
||||
|
@ -1807,6 +1865,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={<span>{t('Advanced')}</span>} key="2">
|
||||
<ExtraOptions
|
||||
extraExtension={dbConfigExtraExtension}
|
||||
db={db as DatabaseObject}
|
||||
onInputChange={({ target }: { target: HTMLInputElement }) =>
|
||||
onChange(ActionType.inputChange, {
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -534,9 +534,9 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
|||
databaseCurrentlyDeleting.sqllab_tab_count,
|
||||
)}
|
||||
</p>
|
||||
{DatabaseDeleteRelatedExtension && currentDatabase?.id && (
|
||||
{DatabaseDeleteRelatedExtension && (
|
||||
<DatabaseDeleteRelatedExtension
|
||||
databaseId={currentDatabase.id}
|
||||
database={databaseCurrentlyDeleting}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -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<DatasetListProps> = ({
|
|||
<SubMenu {...menuData} />
|
||||
{datasetCurrentlyDeleting && (
|
||||
<DeleteModal
|
||||
description={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,
|
||||
)}
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
{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,
|
||||
)}
|
||||
</p>
|
||||
{DatasetDeleteRelatedExtension && (
|
||||
<DatasetDeleteRelatedExtension
|
||||
dataset={datasetCurrentlyDeleting}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
onConfirm={() => {
|
||||
if (datasetCurrentlyDeleting) {
|
||||
handleDatasetDelete(datasetCurrentlyDeleting);
|
||||
|
|
Loading…
Reference in New Issue