mirror of
https://github.com/apache/superset.git
synced 2026-04-20 16:44:46 +00:00
feat: db modal split (#14470)
* refactor: split db modal file (#14436) * split db modal file * fix tests * fix extra options * update database id methodology
This commit is contained in:
committed by
GitHub
parent
12baba03cc
commit
9b42eec064
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@superset-ui/core';
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useEffect,
|
||||
useState,
|
||||
useReducer,
|
||||
Reducer,
|
||||
} from 'react';
|
||||
import Tabs from 'src/components/Tabs';
|
||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||
import {
|
||||
testDatabaseConnection,
|
||||
useSingleViewResource,
|
||||
} from 'src/views/CRUD/hooks';
|
||||
import { useCommonConf } from 'src/views/CRUD/data/database/state';
|
||||
import { DatabaseObject } from 'src/views/CRUD/data/database/types';
|
||||
import ExtraOptions from './ExtraOptions';
|
||||
import SqlAlchemyForm from './SqlAlchemyForm';
|
||||
import { StyledBasicTab, StyledModal } from './styles';
|
||||
|
||||
interface DatabaseModalProps {
|
||||
addDangerToast: (msg: string) => void;
|
||||
addSuccessToast: (msg: string) => void;
|
||||
onDatabaseAdd?: (database?: DatabaseObject) => void; // TODO: should we add a separate function for edit?
|
||||
onHide: () => void;
|
||||
show: boolean;
|
||||
databaseId: number | undefined; // If included, will go into edit mode
|
||||
}
|
||||
|
||||
enum ActionType {
|
||||
textChange,
|
||||
inputChange,
|
||||
editorChange,
|
||||
fetched,
|
||||
reset,
|
||||
}
|
||||
|
||||
interface DBReducerPayloadType {
|
||||
target?: string;
|
||||
name: string;
|
||||
json?: {};
|
||||
type?: string;
|
||||
checked?: boolean;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
type DBReducerActionType =
|
||||
| {
|
||||
type:
|
||||
| ActionType.textChange
|
||||
| ActionType.inputChange
|
||||
| ActionType.editorChange;
|
||||
payload: DBReducerPayloadType;
|
||||
}
|
||||
| {
|
||||
type: ActionType.fetched;
|
||||
payload: Partial<DatabaseObject>;
|
||||
}
|
||||
| {
|
||||
type: ActionType.reset;
|
||||
};
|
||||
|
||||
function dbReducer(
|
||||
state: Partial<DatabaseObject> | null,
|
||||
action: DBReducerActionType,
|
||||
): Partial<DatabaseObject> | null {
|
||||
const trimmedState = {
|
||||
...(state || {}),
|
||||
database_name: state?.database_name?.trim() || '',
|
||||
sqlalchemy_uri: state?.sqlalchemy_uri || '',
|
||||
};
|
||||
|
||||
switch (action.type) {
|
||||
case ActionType.inputChange:
|
||||
if (action.payload.type === 'checkbox') {
|
||||
return {
|
||||
...trimmedState,
|
||||
[action.payload.name]: action.payload.checked,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...trimmedState,
|
||||
[action.payload.name]: action.payload.value,
|
||||
};
|
||||
case ActionType.editorChange:
|
||||
return {
|
||||
...trimmedState,
|
||||
[action.payload.name]: action.payload.json,
|
||||
};
|
||||
case ActionType.textChange:
|
||||
return {
|
||||
...trimmedState,
|
||||
[action.payload.name]: action.payload.value,
|
||||
};
|
||||
case ActionType.fetched:
|
||||
return {
|
||||
...action.payload,
|
||||
};
|
||||
case ActionType.reset:
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_TAB_KEY = '1';
|
||||
|
||||
const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
addDangerToast,
|
||||
addSuccessToast,
|
||||
onDatabaseAdd,
|
||||
onHide,
|
||||
show,
|
||||
databaseId,
|
||||
}) => {
|
||||
const [db, setDB] = useReducer<
|
||||
Reducer<Partial<DatabaseObject> | null, DBReducerActionType>
|
||||
>(dbReducer, null);
|
||||
const [tabKey, setTabKey] = useState<string>(DEFAULT_TAB_KEY);
|
||||
const conf = useCommonConf();
|
||||
|
||||
const isEditMode = !!databaseId;
|
||||
const useSqlAlchemyForm = true; // TODO: set up logic
|
||||
const hasConnectedDb = false; // TODO: set up logic
|
||||
|
||||
// Database fetch logic
|
||||
const {
|
||||
state: { loading: dbLoading, resource: dbFetched },
|
||||
fetchResource,
|
||||
createResource,
|
||||
updateResource,
|
||||
} = useSingleViewResource<DatabaseObject>(
|
||||
'database',
|
||||
t('database'),
|
||||
addDangerToast,
|
||||
);
|
||||
|
||||
// Test Connection logic
|
||||
const testConnection = () => {
|
||||
if (!db?.sqlalchemy_uri) {
|
||||
addDangerToast(t('Please enter a SQLAlchemy URI to test'));
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = {
|
||||
sqlalchemy_uri: db?.sqlalchemy_uri || '',
|
||||
database_name: db?.database_name?.trim() || undefined,
|
||||
impersonate_user: db?.impersonate_user || undefined,
|
||||
extra: db?.extra || undefined,
|
||||
encrypted_extra: db?.encrypted_extra || undefined,
|
||||
server_cert: db?.server_cert || undefined,
|
||||
};
|
||||
|
||||
testDatabaseConnection(connection, addDangerToast, addSuccessToast);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setDB({ type: ActionType.reset });
|
||||
onHide();
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
if (isEditMode) {
|
||||
// databaseId will not be null if isEditMode is true
|
||||
// db will have at least a database_name and sqlalchemy_uri
|
||||
// in order for the button to not be disabled
|
||||
updateResource(databaseId as number, db as DatabaseObject).then(
|
||||
result => {
|
||||
if (result) {
|
||||
if (onDatabaseAdd) {
|
||||
onDatabaseAdd();
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if (db) {
|
||||
// Create
|
||||
db.database_name = db?.database_name?.trim();
|
||||
createResource(db as DatabaseObject).then(dbId => {
|
||||
if (dbId) {
|
||||
if (onDatabaseAdd) {
|
||||
onDatabaseAdd();
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const disableSave = !(db?.database_name?.trim() && db?.sqlalchemy_uri);
|
||||
|
||||
const onChange = (type: any, payload: any) => {
|
||||
setDB({ type, payload } as DBReducerActionType);
|
||||
};
|
||||
|
||||
// Initialize
|
||||
const fetchDB = () => {
|
||||
if (isEditMode && databaseId) {
|
||||
if (!dbLoading) {
|
||||
fetchResource(databaseId).catch(e =>
|
||||
addDangerToast(
|
||||
t(
|
||||
'Sorry there was an error fetching database information: %s',
|
||||
e.message,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
setTabKey(DEFAULT_TAB_KEY);
|
||||
}
|
||||
if (databaseId && show) {
|
||||
fetchDB();
|
||||
}
|
||||
}, [show, databaseId]);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: can we include these values in the original fetch?
|
||||
if (dbFetched) {
|
||||
setDB({
|
||||
type: ActionType.fetched,
|
||||
payload: {
|
||||
...dbFetched,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [dbFetched]);
|
||||
|
||||
const tabChange = (key: string) => {
|
||||
setTabKey(key);
|
||||
};
|
||||
|
||||
return isEditMode || useSqlAlchemyForm ? (
|
||||
<StyledModal
|
||||
name="database"
|
||||
className="database-modal"
|
||||
disablePrimaryButton={disableSave}
|
||||
height="600px"
|
||||
onHandledPrimaryAction={onSave}
|
||||
onHide={onClose}
|
||||
primaryButtonName={isEditMode ? t('Save') : t('Connect')}
|
||||
width="500px"
|
||||
show={show}
|
||||
title={
|
||||
<h4>{isEditMode ? t('Edit database') : t('Connect a database')}</h4>
|
||||
}
|
||||
>
|
||||
<Tabs
|
||||
defaultActiveKey={DEFAULT_TAB_KEY}
|
||||
activeKey={tabKey}
|
||||
onTabClick={tabChange}
|
||||
>
|
||||
<StyledBasicTab tab={<span>{t('Basic')}</span>} key="1">
|
||||
{useSqlAlchemyForm ? (
|
||||
<SqlAlchemyForm
|
||||
db={db as DatabaseObject}
|
||||
onInputChange={({ target }: { target: HTMLInputElement }) =>
|
||||
onChange(ActionType.inputChange, {
|
||||
type: target.type,
|
||||
name: target.name,
|
||||
checked: target.checked,
|
||||
value: target.value,
|
||||
})
|
||||
}
|
||||
conf={conf}
|
||||
testConnection={testConnection}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<p>TODO: db form</p>
|
||||
</div>
|
||||
)}
|
||||
</StyledBasicTab>
|
||||
<Tabs.TabPane tab={<span>{t('Advanced')}</span>} key="2">
|
||||
<ExtraOptions
|
||||
db={db as DatabaseObject}
|
||||
onInputChange={({ target }: { target: HTMLInputElement }) =>
|
||||
onChange(ActionType.inputChange, {
|
||||
type: target.type,
|
||||
name: target.name,
|
||||
checked: target.checked,
|
||||
value: target.value,
|
||||
})
|
||||
}
|
||||
onTextChange={({ target }: { target: HTMLTextAreaElement }) =>
|
||||
onChange(ActionType.textChange, {
|
||||
name: target.name,
|
||||
value: target.value,
|
||||
})
|
||||
}
|
||||
onEditorChange={(payload: { name: string; json: any }) =>
|
||||
onChange(ActionType.editorChange, payload)
|
||||
}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</StyledModal>
|
||||
) : (
|
||||
<StyledModal
|
||||
name="database"
|
||||
className="database-modal"
|
||||
disablePrimaryButton={disableSave}
|
||||
height="600px"
|
||||
onHandledPrimaryAction={onSave}
|
||||
onHide={onClose}
|
||||
primaryButtonName={hasConnectedDb ? t('Connect') : t('Finish')}
|
||||
width="500px"
|
||||
show={show}
|
||||
title={<h4>{t('Connect a database')}</h4>}
|
||||
>
|
||||
<div>
|
||||
<p>TODO: db form</p>
|
||||
</div>
|
||||
</StyledModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default withToasts(DatabaseModal);
|
||||
Reference in New Issue
Block a user