console: allow editing sources configuration

This adds the feature to edit data sources to the console

Co-authored-by: Aleksandra Sikora <9019397+beerose@users.noreply.github.com>
Co-authored-by: Rakesh Emmadi <12475069+rakeshkky@users.noreply.github.com>
Co-authored-by: Rikin Kachhia <54616969+rikinsk@users.noreply.github.com>
Co-authored-by: Martin Mark <74692114+martin-hasura@users.noreply.github.com>
GitOrigin-RevId: 40f97a362620e9cebe97a2267cb9fb143c32af5d
This commit is contained in:
Ikechukwu Eze 2021-05-19 05:15:42 +01:00 committed by hasura-bot
parent 8edd3f03a5
commit a7639145fe
24 changed files with 831 additions and 344 deletions

View File

@ -43,6 +43,7 @@ $ hasura metadata export -o json
- server: fix regression: `on_conflict` was missing in the schema for inserts in tables where the current user has no columns listed in their update permissions (fix #6804)
- server: fix one-to-one relationship bug which prevented adding one-to-one relationships which didn't have the same column name for target and source
- console: fix Postgres table creation when table has a non-lowercase name and a comment (#6760)
- console: allow editing sources configuration
- cli: fix regression - `metadata apply —dry-run` was overwriting local metadata files with metadata on server when it should just display the differences.
- server: decrease polling interval for scheduled triggers from 60 to 10 seconds
- server: Change `HASURA_GRAPHQL_SCHEMA_POLL_INTERVAL` env var to `HASURA_GRAPHQL_SCHEMA_SYNC_POLL_INTERVAL` and `schema-poll-interval` option to `--schema-sync-poll-interval`.

View File

@ -39,7 +39,7 @@ export const addsNewPostgresDatabaseWithUrl = () => {
cy.getBySel('connect-database-btn').click();
cy.get('.notification-success', { timeout: 10000 })
.should('be.visible')
.and('contain', 'Database added successfully!');
.and('contain', 'Data source added successfully!');
cy.url().should('eq', `${baseUrl}/data/manage`);
};
@ -59,7 +59,7 @@ export const addsNewPgDBWithConParams = () => {
cy.getBySel('connect-database-btn').click();
cy.get('.notification-success', { timeout: 10000 })
.should('be.visible')
.and('contain', 'Database added successfully!');
.and('contain', 'Data source added successfully!');
cy.url().should('eq', `${baseUrl}/data/manage`);
};
@ -73,7 +73,7 @@ export const addsNewPgDBWithEnvVar = () => {
cy.getBySel('connect-database-btn').click();
cy.get('.notification-success', { timeout: 10000 })
.should('be.visible')
.and('contain', 'Database added successfully!');
.and('contain', 'Data source added successfully!');
cy.url().should('eq', `${baseUrl}/data/manage`);
};

View File

@ -547,6 +547,10 @@ input {
margin-left: 10px;
}
.add_pad_left_mid {
padding-left: 10px;
}
.add_mar_small {
margin-right: 10px !important;
}
@ -1512,6 +1516,10 @@ code {
margin-right: 4px;
}
}
.label_disabled {
color: #4d4d4db3;
cursor: not-allowed;
}
.connect_db_radio_label {
margin-right: 24px;
@ -1560,7 +1568,7 @@ code {
flex-direction: row;
margin-top: 12px;
align-items: center;
justify-content: space-between;
justify-content: flex-start;
}
.connection_settings_form_input_layout {

View File

@ -4,37 +4,38 @@ import { ConnectDBActions, ConnectDBState, connectionTypes } from './state';
import { LabeledInput } from '../../../Common/LabeledInput';
import Tooltip from '../../../Common/Tooltip/Tooltip';
import { Driver, getSupportedDrivers } from '../../../../dataSources';
import { readFile } from './utils';
import styles from './DataSources.scss';
import JSONEditor from '../TablePermissions/JSONEditor';
import { SupportedFeaturesType } from '../../../../dataSources/types';
import { Path } from '../../../Common/utils/tsUtils';
type ConnectDatabaseFormProps = {
export interface ConnectDatabaseFormProps {
// Connect DB State Props
connectionDBState: ConnectDBState;
connectionDBStateDispatch: Dispatch<ConnectDBActions>;
// Connection Type Props - for the Radio buttons
updateConnectionTypeRadio: (e: ChangeEvent<HTMLInputElement>) => void;
changeConnectionType?: (value: string) => void;
connectionTypeState: string;
// Other Props
isreadreplica?: boolean;
isEditState?: boolean;
title?: string;
};
}
export const connectionRadios = [
{
value: connectionTypes.CONNECTION_PARAMS,
title: 'Connection Parameters',
disableOnEdit: true,
},
{
value: connectionTypes.DATABASE_URL,
title: 'Database URL',
disableOnEdit: false,
},
{
value: connectionTypes.ENV_VAR,
title: 'Environment Variable',
disableOnEdit: true,
},
];
@ -48,11 +49,24 @@ const dbTypePlaceholders: Record<Driver, string> = {
const defaultTitle = 'Connect Database Via';
const driverToLabel: Record<Driver, string> = {
mysql: 'MySQL',
postgres: 'PostgreSQL',
mssql: 'MS Server',
bigquery: 'BigQuery',
const driverToLabel: Record<
Driver,
{ label: string; defaultConnection: string; info?: string }
> = {
mysql: { label: 'MySQL', defaultConnection: 'DATABASE_URL' },
postgres: { label: 'PostgreSQL', defaultConnection: 'DATABASE_URL' },
mssql: {
label: 'MS Server',
defaultConnection: 'DATABASE_URL',
info:
'Only Database URLs and Environment Variables are available using MSSQL',
},
bigquery: {
label: 'BigQuery',
defaultConnection: 'CONNECTION_PARAMETERS',
info:
'Only Connection Parameters and Environment Variables are available using BigQuery',
},
};
const supportedDrivers = getSupportedDrivers('connectDbForm.enabled');
@ -60,9 +74,11 @@ const supportedDrivers = getSupportedDrivers('connectDbForm.enabled');
const ConnectDatabaseForm: React.FC<ConnectDatabaseFormProps> = ({
connectionDBState,
connectionDBStateDispatch,
changeConnectionType,
updateConnectionTypeRadio,
connectionTypeState,
isreadreplica = false,
isEditState = false,
title,
}) => {
const [currentConnectionParamState, toggleConnectionParamState] = useState(
@ -72,20 +88,29 @@ const ConnectDatabaseForm: React.FC<ConnectDatabaseFormProps> = ({
toggleConnectionParamState(value);
};
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files![0];
const addFileQueries = (content: string) => {
try {
connectionDBStateDispatch({
type: 'UPDATE_DB_BIGQUERY_SERVICE_ACCOUNT_FILE',
data: content,
});
} catch (error) {
console.log(error);
}
};
const isDBSupported = (driver: Driver, connectionType: string) => {
let ts = 'databaseURL';
if (connectionType === 'CONNECTION_PARAMETERS') {
ts = 'connectionParameters';
}
if (connectionType === 'ENVIRONMENT_VARIABLES') {
ts = 'environmentVariable';
}
return getSupportedDrivers(
`connectDbForm.${ts}` as Path<SupportedFeaturesType>
).includes(driver);
};
readFile(file, addFileQueries);
const handleDBChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value as Driver;
const isSupported = isDBSupported(value, connectionTypeState);
connectionDBStateDispatch({
type: 'UPDATE_DB_DRIVER',
data: value,
});
if (!isSupported && changeConnectionType) {
changeConnectionType(driverToLabel[value].defaultConnection);
}
};
return (
@ -100,7 +125,11 @@ const ConnectDatabaseForm: React.FC<ConnectDatabaseFormProps> = ({
{connectionRadios.map(radioBtn => (
<label
key={`label-${radioBtn.title}`}
className={styles.connect_db_radio_label}
className={`${styles.connect_db_radio_label} ${
!isDBSupported(connectionDBState.dbType, radioBtn.value)
? styles.label_disabled
: ''
}`}
>
<input
type="radio"
@ -114,14 +143,20 @@ const ConnectDatabaseForm: React.FC<ConnectDatabaseFormProps> = ({
defaultChecked={
connectionTypeState === connectionTypes.DATABASE_URL
}
// disabled={
// isEditState === radioBtn.disableOnEdit && isEditState
// }
disabled={
!isDBSupported(connectionDBState.dbType, radioBtn.value)
}
/>
{radioBtn.title}
</label>
))}
</div>
{driverToLabel[connectionDBState.dbType].info && (
<div className={styles.info_label}>
<i className="fa fa-info-circle" aria-hidden="true" />
&nbsp; <span>{driverToLabel[connectionDBState.dbType].info}</span>
</div>
)}
<div className={styles.connect_form_layout}>
{!isreadreplica && (
<>
@ -132,6 +167,7 @@ const ConnectDatabaseForm: React.FC<ConnectDatabaseFormProps> = ({
data: e.target.value,
})
}
disabled={isEditState}
value={connectionDBState.displayName}
label="Database Display Name"
placeholder="database name"
@ -146,27 +182,22 @@ const ConnectDatabaseForm: React.FC<ConnectDatabaseFormProps> = ({
<select
key="connect-db-type"
value={connectionDBState.dbType}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_DB_DRIVER',
data: e.target.value as Driver,
})
}
onChange={handleDBChange}
className={`form-control ${styles.connect_db_input_pad}`}
disabled={isEditState}
data-test="database-type"
>
{supportedDrivers.map(driver => (
<option key={driver} value={driver}>
{driverToLabel[driver]}
{driverToLabel[driver].label}
</option>
))}
</select>
</>
)}
{(connectionTypeState.includes(connectionTypes.DATABASE_URL) ||
(connectionTypeState.includes(connectionTypes.CONNECTION_PARAMS) &&
connectionDBState.dbType === 'mssql')) &&
connectionDBState.dbType !== 'bigquery' ? (
{connectionTypeState.includes(connectionTypes.DATABASE_URL) ||
(connectionTypeState.includes(connectionTypes.CONNECTION_PARAMS) &&
connectionDBState.dbType === 'mssql') ? (
<LabeledInput
label="Database URL"
onChange={e =>
@ -178,7 +209,6 @@ const ConnectDatabaseForm: React.FC<ConnectDatabaseFormProps> = ({
value={connectionDBState.databaseURLState.dbURL}
placeholder={dbTypePlaceholders[connectionDBState.dbType]}
data-test="database-url"
// disabled={isEditState}
/>
) : null}
{connectionTypeState.includes(connectionTypes.ENV_VAR) &&
@ -217,13 +247,19 @@ const ConnectDatabaseForm: React.FC<ConnectDatabaseFormProps> = ({
) : (
<div className={styles.add_mar_bottom_mid}>
<div className={styles.add_mar_bottom_mid}>
<b>Service Account File:</b>
<Tooltip message="Service account key file for bigquery db" />
<b>Service Account Key:</b>
<Tooltip message="Service account key for BigQuery data source" />
</div>
<input
type="file"
className={`form-control input-sm ${styles.inline_block}`}
onChange={handleFileUpload}
<JSONEditor
minLines={5}
initData="{}"
onChange={value => {
connectionDBStateDispatch({
type: 'UPDATE_DB_BIGQUERY_SERVICE_ACCOUNT',
data: value,
});
}}
data={connectionDBState.databaseURLState.serviceAccount}
/>
</div>
)}
@ -254,156 +290,168 @@ const ConnectDatabaseForm: React.FC<ConnectDatabaseFormProps> = ({
</>
) : null}
{connectionTypeState.includes(connectionTypes.CONNECTION_PARAMS) &&
connectionDBState.dbType === 'postgres' ? (
<>
<LabeledInput
label="Host"
placeholder="localhost"
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_DB_HOST',
data: e.target.value,
})
}
value={connectionDBState.connectionParamState.host}
data-test="host"
/>
<LabeledInput
label="Port"
placeholder="5432"
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_DB_PORT',
data: e.target.value,
})
}
value={connectionDBState.connectionParamState.port}
data-test="port"
/>
<LabeledInput
label="Username"
placeholder="postgres_user"
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_DB_USERNAME',
data: e.target.value,
})
}
value={connectionDBState.connectionParamState.username}
data-test="username"
/>
<LabeledInput
label="Password"
key="connect-db-password"
type="password"
placeholder="postgrespassword"
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_DB_PASSWORD',
data: e.target.value,
})
}
value={connectionDBState.connectionParamState.password}
data-test="password"
/>
<LabeledInput
key="connect-db-database-name"
label="Database Name"
placeholder="postgres"
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_DB_DATABASE_NAME',
data: e.target.value,
})
}
value={connectionDBState.connectionParamState.database}
data-test="database-name"
/>
</>
) : null}
<div className={styles.connection_settings_layout}>
<div className={styles.connection_settings_header}>
<a
href="#"
style={{ textDecoration: 'none' }}
onClick={toggleConnectionParams(!currentConnectionParamState)}
>
{currentConnectionParamState ? (
<i className="fa fa-caret-down" />
) : (
<i className="fa fa-caret-right" />
)}
{' '}
Connection Settings
</a>
</div>
{currentConnectionParamState ? (
<div className={styles.connection_settings_form}>
<div className={styles.connection_settings_form_input_layout}>
<LabeledInput
label="Max Connections"
type="number"
className={`form-control ${styles.connnection_settings_form_input}`}
placeholder="50"
value={
connectionDBState.connectionSettings?.max_connections ??
undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_MAX_CONNECTIONS',
data: e.target.value,
})
}
min="0"
boldlabel
data-test="max-connections"
/>
</div>
<div className={styles.connection_settings_form_input_layout}>
<LabeledInput
label="Idle Timeout"
type="number"
className={`form-control ${styles.connnection_settings_form_input}`}
placeholder="180"
value={
connectionDBState.connectionSettings?.idle_timeout ??
undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_IDLE_TIMEOUT',
data: e.target.value,
})
}
min="0"
boldlabel
data-test="idle-timeout"
/>
</div>
<div className={styles.connection_settings_form_input_layout}>
<LabeledInput
label="Retries"
type="number"
className={`form-control ${styles.connnection_settings_form_input}`}
placeholder="1"
value={
connectionDBState.connectionSettings?.retries ?? undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_RETRIES',
data: e.target.value,
})
}
min="0"
boldlabel
data-test="retries"
/>
</div>
getSupportedDrivers('connectDbForm.connectionParameters').includes(
connectionDBState.dbType
) &&
connectionDBState.dbType !== 'bigquery' && (
<>
<LabeledInput
label="Host"
placeholder="localhost"
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_DB_HOST',
data: e.target.value,
})
}
value={connectionDBState.connectionParamState.host}
data-test="host"
/>
<LabeledInput
label="Port"
placeholder="5432"
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_DB_PORT',
data: e.target.value,
})
}
value={connectionDBState.connectionParamState.port}
data-test="port"
/>
<LabeledInput
label="Username"
placeholder="postgres_user"
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_DB_USERNAME',
data: e.target.value,
})
}
value={connectionDBState.connectionParamState.username}
data-test="username"
/>
<LabeledInput
label="Password"
key="connect-db-password"
type="password"
placeholder="postgrespassword"
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_DB_PASSWORD',
data: e.target.value,
})
}
value={connectionDBState.connectionParamState.password}
data-test="password"
/>
<LabeledInput
key="connect-db-database-name"
label="Database Name"
placeholder="postgres"
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_DB_DATABASE_NAME',
data: e.target.value,
})
}
value={connectionDBState.connectionParamState.database}
data-test="database-name"
/>
</>
)}
{getSupportedDrivers('connectDbForm.connectionSettings').includes(
connectionDBState.dbType
) && (
<div className={styles.connection_settings_layout}>
<div className={styles.connection_settings_header}>
<a
href="#"
style={{ textDecoration: 'none' }}
onClick={toggleConnectionParams(!currentConnectionParamState)}
>
{currentConnectionParamState ? (
<i className="fa fa-caret-down" />
) : (
<i className="fa fa-caret-right" />
)}
{' '}
Connection Settings
</a>
</div>
) : null}
</div>
{currentConnectionParamState ? (
<div className={styles.connection_settings_form}>
<div className={styles.connection_settings_form_input_layout}>
<LabeledInput
label="Max Connections"
type="number"
className={`form-control ${styles.connnection_settings_form_input}`}
placeholder="50"
value={
connectionDBState.connectionSettings?.max_connections ??
undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_MAX_CONNECTIONS',
data: e.target.value,
})
}
min="0"
boldlabel
data-test="max-connections"
/>
</div>
<div className={styles.connection_settings_form_input_layout}>
<LabeledInput
label="Idle Timeout"
type="number"
className={`form-control ${styles.connnection_settings_form_input}`}
placeholder="180"
value={
connectionDBState.connectionSettings?.idle_timeout ??
undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_IDLE_TIMEOUT',
data: e.target.value,
})
}
min="0"
boldlabel
data-test="idle-timeout"
/>
</div>
{getSupportedDrivers('connectDbForm.retries').includes(
connectionDBState.dbType
) && (
<div className={styles.connection_settings_form_input_layout}>
<LabeledInput
label="Retries"
type="number"
className={`form-control ${styles.connnection_settings_form_input}`}
placeholder="1"
value={
connectionDBState.connectionSettings?.retries ??
undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_RETRIES',
data: e.target.value,
})
}
min="0"
boldlabel
data-test="retries"
/>
</div>
)}
</div>
) : null}
</div>
)}
</div>
</>
);

View File

@ -11,7 +11,6 @@ import { connect, ConnectedProps } from 'react-redux';
import Tabbed from './TabbedDataSourceConnection';
import { ReduxState } from '../../../../types';
import { mapDispatchToPropsEmpty } from '../../../Common/utils/reactUtils';
import Button from '../../../Common/Button';
import { showErrorNotification } from '../../Common/Notification';
import _push from '../push';
import {
@ -23,11 +22,14 @@ import {
defaultState,
makeReadReplicaConnectionObject,
} from './state';
import { getDatasourceURL, getErrorMessageFromMissingFields } from './utils';
import ConnectDatabaseForm from './ConnectDBForm';
import {
getDatasourceURL,
getErrorMessageFromMissingFields,
parsePgUrl,
} from './utils';
import ReadReplicaForm from './ReadReplicaForm';
import styles from './DataSources.scss';
import EditDataSource from './EditDataSource';
import DataSourceFormWrapper from './DataSourceFromWrapper';
import { getSupportedDrivers } from '../../../../dataSources';
interface ConnectDatabaseProps extends InjectedProps {}
@ -60,7 +62,7 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
connectionTypes.DATABASE_URL
);
const { sources = [], pathname = '' } = props;
const { sources = [], pathname } = props;
const isEditState =
pathname.includes('edit') || pathname.indexOf('edit') !== -1;
const paths = pathname.split('/');
@ -71,19 +73,74 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
useEffect(() => {
if (isEditState && currentSourceInfo) {
const connectionInfo = currentSourceInfo.configuration?.connection_info;
const databaseUrl =
connectionInfo?.database_url || connectionInfo?.connection_string;
connectDBDispatch({
type: 'INIT',
data: {
name: currentSourceInfo.name,
driver: currentSourceInfo.kind ?? 'postgres',
databaseUrl: getDatasourceURL(
currentSourceInfo?.configuration?.connection_info?.database_url
databaseUrl ?? connectionInfo?.connection_string
),
connectionSettings:
currentSourceInfo.configuration?.connection_info?.pool_settings ??
{},
connectionSettings: connectionInfo?.pool_settings ?? {},
},
});
if (
typeof databaseUrl === 'string' &&
currentSourceInfo.kind === 'postgres'
) {
const p = parsePgUrl(databaseUrl);
connectDBDispatch({
type: 'UPDATE_PARAM_STATE',
data: {
host: p.host?.replace(/:\d*$/, '') ?? '',
port: p.port ?? '',
database: p.pathname?.slice(1) ?? '',
username: p.username ?? '',
password: p.password ?? '',
},
});
}
if (typeof databaseUrl !== 'string' && databaseUrl?.from_env) {
changeConnectionType(connectionTypes.ENV_VAR);
connectDBDispatch({
type: 'UPDATE_DB_URL_ENV_VAR',
data: databaseUrl.from_env,
});
connectDBDispatch({
type: 'UPDATE_DB_URL',
data: '',
});
}
if (currentSourceInfo?.kind === 'bigquery') {
const conf = currentSourceInfo.configuration;
connectDBDispatch({
type: 'UPDATE_DB_BIGQUERY_DATASETS',
data: conf?.datasets?.join(', ') ?? '',
});
connectDBDispatch({
type: 'UPDATE_DB_BIGQUERY_PROJECT_ID',
data: conf?.project_id ?? '',
});
if (conf?.service_account?.from_env) {
changeConnectionType(connectionTypes.ENV_VAR);
connectDBDispatch({
type: 'UPDATE_DB_URL_ENV_VAR',
data: conf?.service_account?.from_env,
});
} else {
changeConnectionType(connectionTypes.CONNECTION_PARAMS);
connectDBDispatch({
type: 'UPDATE_DB_BIGQUERY_SERVICE_ACCOUNT',
data: JSON.stringify(conf?.service_account, null, 2) ?? '{}',
});
}
}
}
}, [isEditState, currentSourceInfo]);
@ -115,10 +172,6 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
return;
}
if (isEditState) {
// TODO: server to provide API
}
// TODO: check if permitted, if not pass undefined
const read_replicas = readReplicasState.map(replica =>
makeReadReplicaConnectionObject(replica)
@ -148,7 +201,8 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
connectionTypes.DATABASE_URL,
connectDBInputState,
onSuccessConnectDBCb,
read_replicas
read_replicas,
isEditState
)
.then(() => setLoading(false))
.catch(() => setLoading(false));
@ -175,7 +229,8 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
connectionTypes.ENV_VAR,
connectDBInputState,
onSuccessConnectDBCb,
read_replicas
read_replicas,
isEditState
)
.then(() => setLoading(false))
.catch(() => setLoading(false));
@ -211,7 +266,8 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
connectionTypes.CONNECTION_PARAMS,
connectDBInputState,
onSuccessConnectDBCb,
read_replicas
read_replicas,
isEditState
)
.then(() => setLoading(false))
.catch(() => setLoading(false));
@ -255,18 +311,36 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
});
};
return (
<Tabbed tabName="connect">
<form
onSubmit={onSubmit}
className={`${styles.connect_db_content} ${styles.connect_form_width}`}
>
<ConnectDatabaseForm
if (isEditState) {
return (
<EditDataSource>
<DataSourceFormWrapper
connectionDBState={connectDBInputState}
connectionDBStateDispatch={connectDBDispatch}
connectionTypeState={connectionType}
updateConnectionTypeRadio={onChangeConnectionType}
changeConnectionType={changeConnectionType}
isEditState={isEditState}
loading={loading}
onSubmit={onSubmit}
title="Edit Data Source"
/>
</EditDataSource>
);
}
return (
<Tabbed tabName="connect">
<DataSourceFormWrapper
connectionDBState={connectDBInputState}
connectionDBStateDispatch={connectDBDispatch}
connectionTypeState={connectionType}
updateConnectionTypeRadio={onChangeConnectionType}
changeConnectionType={changeConnectionType}
isEditState={isEditState}
loading={loading}
onSubmit={onSubmit}
>
{/* Should be rendered only on Pro and Cloud Console */}
{getSupportedDrivers('connectDbForm.read_replicas').includes(
connectDBInputState.dbType
@ -284,23 +358,7 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
onClickSaveReadReplicaCb={onClickSaveReadReplicaForm}
/>
)}
<div className={styles.add_button_layout}>
<Button
size="large"
color="yellow"
type="submit"
style={{
width: '70%',
...(loading && { cursor: 'progress' }),
}}
disabled={loading}
data-test="connect-database-btn"
>
{!isEditState ? 'Connect Database' : 'Edit Connection'}
</Button>
</div>
</form>
</DataSourceFormWrapper>
</Tabbed>
);
};
@ -311,7 +369,7 @@ const mapStateToProps = (state: ReduxState) => {
currentSchema: state.tables.currentSchema,
sources: state.metadata.metadataObject?.sources ?? [],
dbConnection: state.tables.dbConnection,
pathname: state?.routing?.locationBeforeTransitions?.pathname,
pathname: state?.routing?.locationBeforeTransitions?.pathname ?? '',
};
};

View File

@ -0,0 +1,43 @@
import React, { FormEvent } from 'react';
import { Button } from '../../../Common';
import ConnectDatabaseForm, { ConnectDatabaseFormProps } from './ConnectDBForm';
import styles from './DataSources.scss';
interface DataSourceFormWrapperProps extends ConnectDatabaseFormProps {
loading: boolean;
onSubmit: (e: FormEvent<HTMLFormElement>) => void;
}
const DataSourceFormWrapper: React.FC<DataSourceFormWrapperProps> = ({
onSubmit,
loading,
isEditState,
children,
...props
}) => {
return (
<form
onSubmit={onSubmit}
className={`${styles.connect_db_content} ${styles.connect_form_width}`}
>
<ConnectDatabaseForm isEditState={isEditState} {...props} />
{children}
<div className={styles.add_button_layout}>
<Button
size="large"
color="yellow"
type="submit"
style={{
width: '70%',
...(loading && { cursor: 'progress' }),
}}
disabled={loading}
data-test="connect-database-btn"
>
{!isEditState ? 'Connect Database' : 'Update Connection'}
</Button>
</div>
</form>
);
};
export default DataSourceFormWrapper;

View File

@ -54,3 +54,8 @@
margin-right: 8px;
color: rgb(214, 29, 29);
}
.info_label {
margin-bottom: 10px;
color: #4d4d4db3;
}

View File

@ -0,0 +1,35 @@
import React from 'react';
import Helmet from 'react-helmet';
import BreadCrumb from '../../../Common/Layout/BreadCrumb/BreadCrumb';
import { RightContainer } from '../../../Common/Layout/RightContainer';
import styles from './DataSources.scss';
const appPrefix = '/data';
const breadCrumbs = [
{
title: 'Data',
url: appPrefix,
},
{
title: 'Data Manager',
url: `${appPrefix}/manage`,
},
{
title: 'Edit Data Source',
url: '',
},
];
const EditDataSource: React.FC = ({ children }) => {
return (
<RightContainer>
<Helmet title="Edit Data Source - Hasura" />
<div className={styles.add_pad_left_mid}>
<BreadCrumb breadCrumbs={breadCrumbs} />
</div>
{children}
</RightContainer>
);
};
export default EditDataSource;

View File

@ -1,4 +1,3 @@
import { DataSource } from '../../../../metadata/types';
import { Driver } from '../../../../dataSources';
export const parseURI = (url: string) => {
@ -31,14 +30,6 @@ export const parseURI = (url: string) => {
}
};
export const getHostFromConnectionString = (datasource: DataSource) => {
if (typeof datasource.url === 'string' && datasource.driver === 'postgres') {
const { hostname = null } = parseURI(datasource.url);
return hostname;
}
// TODO: update this function with connection string for other databases
return null;
};
export const makeConnectionStringFromConnectionParams = ({
dbType,
host,

View File

@ -7,10 +7,9 @@ import styles from '../../../Common/Common.scss';
const appPrefix = '/data';
const TabbedDSConnection: React.FC<{ tabName: 'create' | 'connect' }> = ({
children,
tabName,
}) => {
const TabbedDSConnection: React.FC<{
tabName: 'create' | 'connect';
}> = ({ children, tabName }) => {
const breadCrumbs = [
{
title: 'Data',

View File

@ -30,7 +30,7 @@ export type ConnectDBState = {
connectionParamState: ConnectionParams;
databaseURLState: {
dbURL: string;
serviceAccountFile: string;
serviceAccount: string;
projectId: string;
datasets: string;
};
@ -52,7 +52,7 @@ export const defaultState: ConnectDBState = {
},
databaseURLState: {
dbURL: '',
serviceAccountFile: '',
serviceAccount: '',
projectId: '',
datasets: '',
},
@ -93,11 +93,12 @@ export const connectDataSource = (
typeConnection: string,
currentState: ConnectDBState,
cb: () => void,
replicas?: Omit<SourceConnectionInfo, 'connection_string'>[]
replicas?: Omit<SourceConnectionInfo, 'connection_string'>[],
isEditState = false
) => {
let databaseURL: string | { from_env: string } =
currentState.dbType === 'bigquery'
? currentState.databaseURLState.serviceAccountFile.trim()
? currentState.databaseURLState.serviceAccount.trim()
: currentState.databaseURLState.dbURL.trim();
if (
typeConnection === connectionTypes.ENV_VAR &&
@ -108,6 +109,7 @@ export const connectDataSource = (
databaseURL = { from_env: currentState.envVarState.envVar.trim() };
} else if (
typeConnection === connectionTypes.CONNECTION_PARAMS &&
currentState.dbType !== 'bigquery' &&
getSupportedDrivers('connectDbForm.connectionParameters').includes(
currentState.dbType
)
@ -126,6 +128,7 @@ export const connectDataSource = (
name: currentState.displayName.trim(),
dbUrl: databaseURL,
connection_pool_settings: currentState.connectionSettings,
replace_configuration: isEditState,
bigQuery: {
projectId: currentState.databaseURLState.projectId,
datasets: currentState.databaseURLState.datasets,
@ -148,9 +151,10 @@ export type ConnectDBActions =
connectionSettings: ConnectionSettings;
};
}
| { type: 'UPDATE_PARAM_STATE'; data: ConnectionParams }
| { type: 'UPDATE_DISPLAY_NAME'; data: string }
| { type: 'UPDATE_DB_URL'; data: string }
| { type: 'UPDATE_DB_BIGQUERY_SERVICE_ACCOUNT_FILE'; data: string }
| { type: 'UPDATE_DB_BIGQUERY_SERVICE_ACCOUNT'; data: string }
| { type: 'UPDATE_DB_BIGQUERY_PROJECT_ID'; data: string }
| { type: 'UPDATE_DB_BIGQUERY_DATASETS'; data: string }
| { type: 'UPDATE_DB_URL_ENV_VAR'; data: string }
@ -182,6 +186,11 @@ export const connectDBReducer = (
},
connectionSettings: action.data.connectionSettings,
};
case 'UPDATE_PARAM_STATE':
return {
...state,
connectionParamState: action.data,
};
case 'UPDATE_DISPLAY_NAME':
return {
...state,
@ -280,12 +289,12 @@ export const connectDBReducer = (
...state,
connectionSettings: action.data,
};
case 'UPDATE_DB_BIGQUERY_SERVICE_ACCOUNT_FILE':
case 'UPDATE_DB_BIGQUERY_SERVICE_ACCOUNT':
return {
...state,
databaseURLState: {
...state.databaseURLState,
serviceAccountFile: action.data,
serviceAccount: action.data,
},
};
case 'UPDATE_DB_BIGQUERY_DATASETS':

View File

@ -38,6 +38,31 @@ export const getDatasourceURL = (
return link.from_env.toString();
};
export function parsePgUrl(
url: string
): Partial<Omit<URL, 'searchParams' | 'toJSON'>> {
try {
const protocol = new URL(url).protocol;
const newUrl = url.replace(protocol, 'http://');
const parsed = new URL(newUrl);
return {
origin: parsed.origin.replace('http:', protocol),
hash: parsed.hash,
host: parsed.host,
hostname: parsed.hostname,
port: parsed.port,
href: parsed.href.replace('http:', protocol),
password: parsed.password,
pathname: parsed.pathname,
search: parsed.search,
username: parsed.username,
protocol,
};
} catch (error) {
return {};
}
}
type TableType = Record<string, { table_type: Table['table_type'] }>;
type SchemaType = Record<string, TableType>;
type SourceSchemasType = Record<string, SchemaType>;
@ -65,20 +90,3 @@ export const canReUseTableTypes = (
)
);
};
export const readFile = (
file: File | null,
callback: (content: string) => void
) => {
const reader = new FileReader();
reader.onload = event => {
const content = event.target!.result as string;
callback(content);
};
reader.onerror = event => {
console.error(`File could not be read! Code ${event.target!.error!.code}`);
};
if (file) reader.readAsText(file);
};

View File

@ -65,7 +65,6 @@ const trackAllItems = (sql, isMigration, migrationName, source, driver) => (
const tableDef = { name, schema };
const { allSchemas } = getState().tables;
const table = findTable(allSchemas, tableDef);
console.log({ table });
req = getTrackTableQuery({
tableDef,
source,

View File

@ -29,7 +29,7 @@ import CollapsibleToggle from './CollapsibleToggle';
type DatabaseListItemProps = {
dataSource: DataSource;
inconsistentObjects: InjectedProps['inconsistentObjects'];
// onEdit: (dbName: string) => void;
onEdit: (dbName: string) => void;
onReload: (name: string, driver: Driver, cb: () => void) => void;
onRemove: (name: string, driver: Driver, cb: () => void) => void;
pushRoute: (route: string) => void;
@ -38,7 +38,7 @@ type DatabaseListItemProps = {
};
const DatabaseListItem: React.FC<DatabaseListItemProps> = ({
// onEdit,
onEdit,
pushRoute,
onReload,
onRemove,
@ -102,7 +102,7 @@ const DatabaseListItem: React.FC<DatabaseListItemProps> = ({
<Button
size="xs"
color="white"
style={{ marginRight: '10px' }}
className={styles.add_mar_right_mid}
onClick={viewDB}
disabled={isInconsistentDataSource}
>
@ -120,6 +120,16 @@ const DatabaseListItem: React.FC<DatabaseListItemProps> = ({
>
{reloading ? 'Reloading...' : 'Reload'}
</Button>
<Button
size="xs"
color="white"
onClick={() => {
onEdit(dataSource.name);
}}
className={styles.add_mar_left_mid}
>
Edit
</Button>
<Button
className={`${styles.text_red}`}
size="xs"
@ -249,9 +259,9 @@ const ManageDatabase: React.FC<ManageDatabaseProps> = ({
if (route) dispatch(_push(route));
};
// const onEdit = (dbName: string) => {
// dispatch(_push(`/data/manage/edit/${dbName}`));
// };
const onEdit = (dbName: string) => {
dispatch(_push(`/data/manage/edit/${dbName}`));
};
return (
<RightContainer>
@ -286,6 +296,7 @@ const ManageDatabase: React.FC<ManageDatabaseProps> = ({
dataSource={data}
inconsistentObjects={inconsistentObjects}
pushRoute={pushRoute}
onEdit={onEdit}
onReload={onReload}
onRemove={onRemove}
dispatch={dispatch}

View File

@ -7,12 +7,14 @@ export interface JSONEditorProps {
initData: string;
onChange: (v: string) => void;
data: string;
minLines?: number;
}
const JSONEditor: React.FC<JSONEditorProps> = ({
initData,
onChange,
data,
minLines,
}) => {
const [value, setValue] = useState(initData || data || '');
const [annotations, setAnnotations] = useState<IAnnotation[]>([]);
@ -56,6 +58,7 @@ const JSONEditor: React.FC<JSONEditorProps> = ({
onChange={onEditorValueChange}
theme="github"
height="5em"
minLines={minLines || 1}
maxLines={15}
width="100%"
showPrintMargin={false}

View File

@ -153,10 +153,12 @@ export const supportedFeatures: SupportedFeaturesType = {
},
connectDbForm: {
enabled: globals.consoleType !== 'cloud',
connectionParameters: false,
databaseURL: true,
connectionParameters: true,
databaseURL: false,
environmentVariable: true,
read_replicas: false,
connectionSettings: false,
retries: false,
},
};

View File

@ -163,6 +163,8 @@ export const supportedFeatures: SupportedFeaturesType = {
databaseURL: true,
environmentVariable: true,
read_replicas: false,
connectionSettings: true,
retries: false,
},
};

View File

@ -586,6 +586,8 @@ export const supportedFeatures: SupportedFeaturesType = {
databaseURL: true,
environmentVariable: true,
read_replicas: true,
connectionSettings: true,
retries: true,
},
};

View File

@ -294,6 +294,8 @@ export type SupportedFeaturesType = {
databaseURL: boolean;
environmentVariable: boolean;
read_replicas: boolean;
connectionSettings: boolean;
retries: boolean;
};
};

View File

@ -123,6 +123,7 @@ export interface AddDataSourceRequest {
idle_timeout?: number; // in seconds
retries?: number;
};
replace_configuration?: boolean;
bigQuery: {
projectId: string;
datasets: string;
@ -255,7 +256,7 @@ export const addDataSource = (
headers: dataHeaders,
body: JSON.stringify(query),
};
const isEdit = data.payload.replace_configuration;
return dispatch(requestAction(Endpoints.metadata, options))
.then(() => {
dispatch({
@ -273,7 +274,9 @@ export const addDataSource = (
dispatch(
showNotification(
{
title: 'Database added successfully!',
title: `Data source ${
!isEdit ? 'added' : 'updated'
} successfully!`,
level: 'success',
autoDismiss: 0,
action: {
@ -291,9 +294,17 @@ export const addDataSource = (
})
.catch(err => {
console.error(err);
dispatch(_push('/data/manage/connect'));
if (!isEdit) {
dispatch(_push('/data/manage/connect'));
}
if (!skipNotification) {
dispatch(showErrorNotification('Add data source failed', null, err));
dispatch(
showErrorNotification(
`${!isEdit ? 'Add' : 'Updating'} data source failed`,
null,
err
)
);
}
return err;
});
@ -340,55 +351,6 @@ export const removeDataSource = (
});
};
export const editDataSource = (
oldName: string | undefined,
data: AddDataSourceRequest['data'],
onSuccessCb: () => void
): Thunk<Promise<void | ReduxState>, MetadataActions> => dispatch => {
return dispatch(
removeDataSource(
{ driver: data.driver, name: oldName ?? data.payload.name },
true
)
)
.then(() => {
// FIXME?: There might be a problem when or if the metadata is inconsistent,
// we should be providing a better error message for the same
dispatch(
addDataSource(
data,
() => {
dispatch(
showSuccessNotification(
'Successfully updated datasource details.'
)
);
onSuccessCb();
},
[],
true
)
).catch(err => {
console.error(err);
dispatch(
showErrorNotification(
'Failed to edit data source',
'There was a problem in editing the details of the datasource'
)
);
});
})
.catch(err => {
console.error(err);
dispatch(
showErrorNotification(
'Failed to edit data source',
'There was a problem in editing the details of the datasource'
)
);
});
};
export const replaceMetadata = (
newMetadata: HasuraMetadataV3,
successCb: () => void,

View File

@ -11,6 +11,7 @@ export const addSource = (
idle_timeout?: number;
retries?: number;
};
replace_configuration?: boolean;
bigQuery: {
projectId: string;
datasets: string;
@ -19,6 +20,7 @@ export const addSource = (
// supported only for PG sources at the moment
replicas?: Omit<SourceConnectionInfo, 'connection_string'>[]
) => {
const replace_configuration = payload.replace_configuration ?? false;
if (driver === 'mssql') {
return {
type: 'mssql_add_source',
@ -30,20 +32,26 @@ export const addSource = (
pool_settings: payload.connection_pool_settings,
},
},
replace_configuration,
},
};
}
if (driver === 'bigquery') {
const service_account =
typeof payload.dbUrl === 'string'
? JSON.parse(payload.dbUrl)
: payload.dbUrl;
return {
type: 'bigquery_add_source',
args: {
name: payload.name,
configuration: {
service_account: payload.dbUrl,
service_account,
project_id: payload.bigQuery.projectId,
datasets: payload.bigQuery.datasets.split(',').map(d => d.trim()),
},
replace_configuration,
},
};
}
@ -59,6 +67,7 @@ export const addSource = (
},
read_replicas: replicas?.length ? replicas : null,
},
replace_configuration,
},
};
};

View File

@ -884,7 +884,7 @@ export interface RestEndpointEntry {
export interface SourceConnectionInfo {
// used for SQL Server
connection_string: string;
connection_string: string | { from_env: string };
// used for Postgres
database_url: string | { from_env: string };
pool_settings: {

View File

@ -105,7 +105,8 @@ Remove a database with name ``pg1``:
{
"type": "pg_drop_source",
"args": {
"name": "pg1"
"name": "pg1",
"cascade": true
}
}
@ -125,3 +126,204 @@ Args syntax
- true
- :ref:`SourceName <SourceName>`
- Name of the Postgres database
* - cascade
- false
- Boolean
- When set to ``true``, the effect (if possible) is cascaded to any metadata dependent objects (relationships, permissions etc.) from other sources (default: ``false``)
mssql_add_source
----------------
``mssql_add_source`` is used to connect a MS SQL Server database to Hasura.
Add a database with name ``mssql1``:
.. code-block:: http
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "mssql_add_source",
"args": {
"name": "mssql1",
"configuration": {
"connection_info": {
"connection_string": {
"from_env": "<CONN_STRING_ENV_VAR>"
},
"pool_settings": {
"max_connections": 50,
"idle_timeout": 180
}
}
}
}
}
.. _mssql_add_source_syntax:
Args syntax
^^^^^^^^^^^
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - name
- true
- :ref:`SourceName <SourceName>`
- Name of the MS SQL Server database
* - configuration
- true
- :ref:`MsSQLConfiguration <MsSQLConfiguration>`
- Database connection configuration
* - replace_configuration
- false
- Boolean
- If set to ``true`` the configuration will be replaced if the source with
given name already exists (default: ``false``)
.. _mssql_drop_source:
mssql_drop_source
-----------------
``mssql_drop_source`` is used to remove a MS SQL Server database from Hasura.
Remove a database with name ``mssql1``:
.. code-block:: http
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "mssql_drop_source",
"args": {
"name": "mssql1"
}
}
.. _mssql_drop_source_syntax:
Args syntax
^^^^^^^^^^^
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - name
- true
- :ref:`SourceName <SourceName>`
- Name of the MS SQL Server database
* - cascade
- false
- Boolean
- When set to ``true``, the effect (if possible) is cascaded to any metadata dependent objects (relationships, permissions etc.) from other sources (default: ``false``)
.. _bigquery_add_source:
bigquery_add_source
-------------------
``bigquery_add_source`` is used to connect a BigQuery database to Hasura.
Add a database with name ``bigquery1``:
.. code-block:: http
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "bigquery_add_source",
"args": {
"name": "bigquery1",
"configuration": {
"service_account": "bigquery_service_account",
"project_id": "bigquery_project_id",
"datasets": "dataset1, dataset2"
}
}
}
.. _bigquery_add_source_syntax:
Args syntax
^^^^^^^^^^^
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - name
- true
- :ref:`SourceName <SourceName>`
- Name of the BigQuery database
* - configuration
- true
- :ref:`BigQueryConfiguration <BigQueryConfiguration>`
- Database connection configuration
* - replace_configuration
- false
- Boolean
- If set to ``true`` the configuration will be replaced if the source with
given name already exists (default: ``false``)
.. _bigquery_drop_source:
bigquery_drop_source
--------------------
``bigquery_drop_source`` is used to remove a BigQuery database from Hasura.
Remove a database with name ``bigquery1``:
.. code-block:: http
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "bigquery_drop_source",
"args": {
"name": "bigquery1"
}
}
.. _bigquery_drop_source_syntax:
Args syntax
^^^^^^^^^^^
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - name
- true
- :ref:`SourceName <SourceName>`
- Name of the BigQuery database
* - cascade
- false
- Boolean
- When set to ``true``, the effect (if possible) is cascaded to any metadata dependent objects (relationships, permissions etc.) from other sources (default: ``false``)

View File

@ -105,6 +105,50 @@ PGConfiguration
- [PGSourceConnectionInfo_]
- Optional list of read replica configuration *(supported only in cloud/enterprise versions)*
.. _MsSQLConfiguration:
MsSQLConfiguration
^^^^^^^^^^^^^^^^^^
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - connection_info
- true
- MsSQLSourceConnectionInfo_
- Connection parameters for the source
.. _BigQueryConfiguration:
BigQueryConfiguration
^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - service_account
- true
- ``JSON String`` | ``JSON`` | FromEnv_
- Service account for BigQuery database
* - project_id
- true
- ``String`` | FromEnv_
- Project Id for BigQuery database
* - datasets
- true
- ``[String]`` | FromEnv_
- List of BigQuery datasets
.. _PGSourceConnectionInfo:
PGSourceConnectionInfo
@ -122,7 +166,7 @@ PGSourceConnectionInfo
- ``String`` | FromEnv_
- The database connection URL string, or as an environment variable
* - pool_settings
- true
- false
- PGPoolSettings_
- Connection pool settings
* - use_prepared_statements
@ -136,6 +180,28 @@ PGSourceConnectionInfo
- The transaction isolation level in which the queries made to the source will be run with (default: ``read-committed``).
.. _MsSQLSourceConnectionInfo:
MsSQLSourceConnectionInfo
^^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - connection_string
- true
- ``String`` | FromEnv_
- The database connection string, or as an environment variable
* - pool_settings
- false
- MsSQLPoolSettings_
- Connection pool settings
.. _FromEnv:
FromEnv
@ -189,6 +255,28 @@ PGPoolSettings
passed, memory from large query results may not be reclaimed. (default: 600 sec)
.. _MsSQLPoolSettings:
MsSQLPoolSettings
^^^^^^^^^^^^^^^^^
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - max_connections
- false
- ``Integer``
- Maximum number of connections to be kept in the pool (default: 50)
* - idle_timeout
- false
- ``Integer``
- The idle timeout (in seconds) per connection (default: 180)
.. _PGColumnType:
PGColumnType