mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
console/pro-console: add read replicas support
Co-authored-by: Aleksandra Sikora <9019397+beerose@users.noreply.github.com> GitOrigin-RevId: 5e49ad67852b53bd4ba4fd195bdc538706902fd1
This commit is contained in:
parent
130e27e755
commit
fe1c5afb01
@ -25,7 +25,7 @@ export const expandConnectionSettingsform = () => {
|
||||
};
|
||||
|
||||
export const failsOnEmptyFormSubmission = () => {
|
||||
cy.getBySel('save-database').click();
|
||||
cy.getBySel('connect-database-btn').click();
|
||||
cy.get('.notification-error').should('be.visible');
|
||||
};
|
||||
|
||||
@ -36,7 +36,7 @@ export const addsNewPostgresDatabaseWithUrl = () => {
|
||||
cy.getBySel('max-connections').type('50');
|
||||
cy.getBySel('idle-timeout').type('180');
|
||||
cy.getBySel('retries').type('1');
|
||||
cy.getBySel('save-database').click();
|
||||
cy.getBySel('connect-database-btn').click();
|
||||
cy.get('.notification-success', { timeout: 10000 })
|
||||
.should('be.visible')
|
||||
.and('contain', 'Database added successfully!');
|
||||
@ -56,7 +56,7 @@ export const addsNewPgDBWithConParams = () => {
|
||||
cy.getBySel('password').type(config.password);
|
||||
}
|
||||
cy.getBySel('database-name').type(config.dbName);
|
||||
cy.getBySel('save-database').click();
|
||||
cy.getBySel('connect-database-btn').click();
|
||||
cy.get('.notification-success', { timeout: 10000 })
|
||||
.should('be.visible')
|
||||
.and('contain', 'Database added successfully!');
|
||||
@ -70,7 +70,7 @@ export const addsNewPgDBWithEnvVar = () => {
|
||||
cy.getBySel('database-display-name').type('testDB3');
|
||||
cy.getBySel('database-type').select('postgres');
|
||||
cy.getBySel('database-url-env').type('HASURA_GRAPHQL_DATABASE_URL');
|
||||
cy.getBySel('save-database').click();
|
||||
cy.getBySel('connect-database-btn').click();
|
||||
cy.get('.notification-success', { timeout: 10000 })
|
||||
.should('be.visible')
|
||||
.and('contain', 'Database added successfully!');
|
||||
@ -82,7 +82,7 @@ export const failDuplicateNameDb = () => {
|
||||
cy.get('button').contains('Connect Database').click();
|
||||
cy.getBySel('database-display-name').type('testDB1');
|
||||
cy.getBySel('database-url').type(dbUrl);
|
||||
cy.getBySel('save-database').click();
|
||||
cy.getBySel('connect-database-btn').click();
|
||||
cy.get('.notification-error')
|
||||
.should('be.visible')
|
||||
.and('contain', 'Add data source failed')
|
||||
|
@ -4,7 +4,7 @@
|
||||
"description": "Console for Hasura GraphQL Engine",
|
||||
"author": "Hasura (https://github.com/hasura/graphql-engine)",
|
||||
"license": "Apache 2.0",
|
||||
"version": "2.0.1-5",
|
||||
"version": "2.0.1-7",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/hasura/graphql-engine"
|
||||
@ -224,4 +224,4 @@
|
||||
"engines": {
|
||||
"node": ">=8.9.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1490,7 +1490,6 @@ code {
|
||||
|
||||
.connect_db_radios {
|
||||
display: flex;
|
||||
width: 70%;
|
||||
align-items: center;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
@ -1507,7 +1506,7 @@ code {
|
||||
}
|
||||
|
||||
.connect_form_layout {
|
||||
width: 50%;
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -1552,7 +1551,7 @@ code {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.connnection_settings_form_input_layout {
|
||||
.connection_settings_form_input_layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@ -1566,6 +1565,10 @@ code {
|
||||
width: 65%;
|
||||
}
|
||||
|
||||
.connect_form_width {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
/* container height subtracting top header and bottom scroll bar */
|
||||
$mainContainerHeight: calc(100vh - 50px - 20px);
|
||||
|
||||
|
@ -3,13 +3,13 @@ import styles from '../../Common/Common.scss';
|
||||
|
||||
interface LabeledInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string;
|
||||
labelInBold?: boolean;
|
||||
boldlabel?: boolean;
|
||||
}
|
||||
|
||||
export const LabeledInput: React.FC<LabeledInputProps> = props => (
|
||||
<>
|
||||
<label className={props.labelInBold ? '' : styles.connect_db_input_label}>
|
||||
{props?.labelInBold ? <b>{props.label}</b> : props.label}
|
||||
<label className={props.boldlabel ? '' : styles.connect_db_input_label}>
|
||||
{props?.boldlabel ? <b>{props.label}</b> : props.label}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -0,0 +1,325 @@
|
||||
import React, { ChangeEvent, Dispatch, useState } from 'react';
|
||||
|
||||
import { ConnectDBActions, ConnectDBState, connectionTypes } from './state';
|
||||
import { LabeledInput } from '../../../Common/LabeledInput';
|
||||
import { Driver } from '../../../../dataSources';
|
||||
|
||||
import styles from './DataSources.scss';
|
||||
|
||||
type ConnectDatabaseFormProps = {
|
||||
// Connect DB State Props
|
||||
connectionDBState: ConnectDBState;
|
||||
connectionDBStateDispatch: Dispatch<ConnectDBActions>;
|
||||
// Connection Type Props - for the Radio buttons
|
||||
updateConnectionTypeRadio: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
connectionTypeState: string;
|
||||
// Other Props
|
||||
isreadreplica?: 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,
|
||||
},
|
||||
];
|
||||
|
||||
const dbTypePlaceholders: Record<Driver, string> = {
|
||||
postgres: 'postgresql://username:password@hostname:5432/database',
|
||||
mssql:
|
||||
'Driver={ODBC Driver 17 for SQL Server};Server=serveraddress;Database=dbname;Uid=username;Pwd=password;',
|
||||
mysql: 'MySQL connection string',
|
||||
};
|
||||
|
||||
const defaultTitle = 'Connect Database Via';
|
||||
|
||||
const ConnectDatabaseForm: React.FC<ConnectDatabaseFormProps> = ({
|
||||
connectionDBState,
|
||||
connectionDBStateDispatch,
|
||||
updateConnectionTypeRadio,
|
||||
connectionTypeState,
|
||||
isreadreplica = false,
|
||||
title,
|
||||
}) => {
|
||||
const [currentConnectionParamState, toggleConnectionParamState] = useState(
|
||||
false
|
||||
);
|
||||
const toggleConnectionParams = (value: boolean) => () => {
|
||||
toggleConnectionParamState(value);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<h4 className={`${styles.remove_pad_bottom} ${styles.connect_db_header}`}>
|
||||
{title ?? defaultTitle}
|
||||
</h4>
|
||||
<div
|
||||
className={styles.connect_db_radios}
|
||||
onChange={updateConnectionTypeRadio}
|
||||
>
|
||||
{connectionRadios.map(radioBtn => (
|
||||
<label
|
||||
key={`label-${radioBtn.title}`}
|
||||
className={styles.connect_db_radio_label}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={radioBtn.value}
|
||||
name={
|
||||
!isreadreplica
|
||||
? 'connection-type'
|
||||
: 'connection-type-read-replica'
|
||||
}
|
||||
checked={connectionTypeState.includes(radioBtn.value)}
|
||||
defaultChecked={
|
||||
connectionTypeState === connectionTypes.DATABASE_URL
|
||||
}
|
||||
// disabled={
|
||||
// isEditState === radioBtn.disableOnEdit && isEditState
|
||||
// }
|
||||
/>
|
||||
{radioBtn.title}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.connect_form_layout}>
|
||||
{!isreadreplica && (
|
||||
<>
|
||||
<LabeledInput
|
||||
onChange={e =>
|
||||
connectionDBStateDispatch({
|
||||
type: 'UPDATE_DISPLAY_NAME',
|
||||
data: e.target.value,
|
||||
})
|
||||
}
|
||||
value={connectionDBState.displayName}
|
||||
label="Database Display Name"
|
||||
placeholder="database name"
|
||||
data-test="database-display-name"
|
||||
/>
|
||||
<label
|
||||
key="Data Source Driver"
|
||||
className={styles.connect_db_input_label}
|
||||
>
|
||||
Data Source Driver
|
||||
</label>
|
||||
<select
|
||||
key="connect-db-type"
|
||||
value={connectionDBState.dbType}
|
||||
onChange={e =>
|
||||
connectionDBStateDispatch({
|
||||
type: 'UPDATE_DB_DRIVER',
|
||||
data: e.target.value as Driver,
|
||||
})
|
||||
}
|
||||
className={`form-control ${styles.connect_db_input_pad}`}
|
||||
data-test="database-type"
|
||||
>
|
||||
<option key="postgres" value="postgres">
|
||||
Postgres
|
||||
</option>
|
||||
<option key="mssql" value="mssql">
|
||||
MS Server
|
||||
</option>
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
{connectionTypeState.includes(connectionTypes.DATABASE_URL) ||
|
||||
(connectionTypeState.includes(connectionTypes.CONNECTION_PARAMS) &&
|
||||
connectionDBState.dbType === 'mssql') ? (
|
||||
<LabeledInput
|
||||
label="Database URL"
|
||||
onChange={e =>
|
||||
connectionDBStateDispatch({
|
||||
type: 'UPDATE_DB_URL',
|
||||
data: e.target.value,
|
||||
})
|
||||
}
|
||||
value={connectionDBState.databaseURLState.dbURL}
|
||||
placeholder={dbTypePlaceholders[connectionDBState.dbType]}
|
||||
data-test="database-url"
|
||||
// disabled={isEditState}
|
||||
/>
|
||||
) : null}
|
||||
{connectionTypeState.includes(connectionTypes.ENV_VAR) ? (
|
||||
<LabeledInput
|
||||
label="Environment Variable"
|
||||
placeholder="HASURA_GRAPHQL_DB_URL_FROM_ENV"
|
||||
onChange={e =>
|
||||
connectionDBStateDispatch({
|
||||
type: 'UPDATE_DB_URL_ENV_VAR',
|
||||
data: e.target.value,
|
||||
})
|
||||
}
|
||||
value={connectionDBState.envVarURLState.envVarURL}
|
||||
data-test="database-url-env"
|
||||
/>
|
||||
) : null}
|
||||
{connectionTypeState.includes(connectionTypes.CONNECTION_PARAMS) &&
|
||||
connectionDBState.dbType !== 'mssql' ? (
|
||||
<>
|
||||
<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>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectDatabaseForm;
|
@ -1,4 +1,11 @@
|
||||
import React, { ChangeEvent, FormEvent } from 'react';
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
FormEvent,
|
||||
useReducer,
|
||||
useState,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import Tabbed from './TabbedDataSourceConnection';
|
||||
@ -7,65 +14,52 @@ import { mapDispatchToPropsEmpty } from '../../../Common/utils/reactUtils';
|
||||
import Button from '../../../Common/Button';
|
||||
import { showErrorNotification } from '../../Common/Notification';
|
||||
import _push from '../push';
|
||||
|
||||
import styles from '../../../Common/Common.scss';
|
||||
import {
|
||||
connectDataSource,
|
||||
connectDBReducer,
|
||||
connectionTypes,
|
||||
getDefaultState,
|
||||
readReplicaReducer,
|
||||
defaultState,
|
||||
makeReadReplicaConnectionObject,
|
||||
} from './state';
|
||||
import { getDatasourceURL, getErrorMessageFromMissingFields } from './utils';
|
||||
import { LabeledInput } from '../../../Common/LabeledInput';
|
||||
import { Driver } from '../../../../dataSources';
|
||||
import ConnectDatabaseForm from './ConnectDBForm';
|
||||
import ReadReplicaForm from './ReadReplicaForm';
|
||||
|
||||
import styles from './DataSources.scss';
|
||||
|
||||
interface ConnectDatabaseProps extends InjectedProps {}
|
||||
|
||||
const connectionRadioName = 'connection-type';
|
||||
const dbTypePlaceholders: Record<Driver, string> = {
|
||||
postgres: 'postgresql://username:password@hostname:5432/database',
|
||||
mssql:
|
||||
'Driver={ODBC Driver 17 for SQL Server};Server=serveraddress;Database=dbname;Uid=username;Pwd=password;',
|
||||
mysql: 'MySQL connection string',
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
|
||||
const { dispatch } = props;
|
||||
|
||||
const [connectDBInputState, connectDBDispatch] = React.useReducer(
|
||||
const [connectDBInputState, connectDBDispatch] = useReducer(
|
||||
connectDBReducer,
|
||||
getDefaultState(props)
|
||||
);
|
||||
|
||||
const [connectionType, changeConnectionType] = React.useState(
|
||||
const [connectionType, changeConnectionType] = useState(
|
||||
props.dbConnection.envVar
|
||||
? connectionTypes.ENV_VAR
|
||||
: connectionTypes.DATABASE_URL
|
||||
);
|
||||
const [openConnectionSettings, changeConnectionsParamState] = React.useState(
|
||||
false
|
||||
);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const { sources = [], pathname = '' } = props;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [readReplicasState, readReplicaDispatch] = useReducer(
|
||||
readReplicaReducer,
|
||||
[]
|
||||
);
|
||||
const [
|
||||
connectDBStateForReadReplica,
|
||||
connectDBReadReplicaDispatch,
|
||||
] = useReducer(connectDBReducer, defaultState);
|
||||
const [readReplicaConnectionType, updateReadReplicaConnectionType] = useState(
|
||||
connectionTypes.DATABASE_URL
|
||||
);
|
||||
|
||||
const { sources = [], pathname = '' } = props;
|
||||
const isEditState =
|
||||
pathname.includes('edit') || pathname.indexOf('edit') !== -1;
|
||||
const paths = pathname.split('/');
|
||||
@ -73,7 +67,8 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
|
||||
const currentSourceInfo = sources.find(
|
||||
source => source.name === editSourceName
|
||||
);
|
||||
React.useEffect(() => {
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditState && currentSourceInfo) {
|
||||
connectDBDispatch({
|
||||
type: 'INIT',
|
||||
@ -91,9 +86,8 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
|
||||
}
|
||||
}, [isEditState, currentSourceInfo]);
|
||||
|
||||
const onChangeConnectionType = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const onChangeConnectionType = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
changeConnectionType(e.target.value);
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
connectDBDispatch({
|
||||
@ -124,6 +118,11 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
|
||||
// TODO: server to provide API
|
||||
}
|
||||
|
||||
// TODO: check if permitted, if not pass undefined
|
||||
const read_replicas = readReplicasState.map(replica =>
|
||||
makeReadReplicaConnectionObject(replica)
|
||||
);
|
||||
|
||||
if (
|
||||
connectionType === connectionTypes.DATABASE_URL ||
|
||||
(connectionType === connectionTypes.CONNECTION_PARAMS &&
|
||||
@ -144,7 +143,8 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
|
||||
dispatch,
|
||||
connectionTypes.DATABASE_URL,
|
||||
connectDBInputState,
|
||||
onSuccessConnectDBCb
|
||||
onSuccessConnectDBCb,
|
||||
read_replicas
|
||||
)
|
||||
.then(() => setLoading(false))
|
||||
.catch(() => setLoading(false));
|
||||
@ -165,9 +165,10 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
|
||||
setLoading(true);
|
||||
connectDataSource(
|
||||
dispatch,
|
||||
connectionType,
|
||||
connectionTypes.ENV_VAR,
|
||||
connectDBInputState,
|
||||
onSuccessConnectDBCb
|
||||
onSuccessConnectDBCb,
|
||||
read_replicas
|
||||
)
|
||||
.then(() => setLoading(false))
|
||||
.catch(() => setLoading(false));
|
||||
@ -198,292 +199,95 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
|
||||
setLoading(true);
|
||||
connectDataSource(
|
||||
dispatch,
|
||||
connectionType,
|
||||
connectionTypes.CONNECTION_PARAMS,
|
||||
connectDBInputState,
|
||||
onSuccessConnectDBCb
|
||||
onSuccessConnectDBCb,
|
||||
read_replicas
|
||||
)
|
||||
.then(() => setLoading(false))
|
||||
.catch(() => setLoading(false));
|
||||
};
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
onConnectDatabase();
|
||||
};
|
||||
|
||||
const updateRadioForReadReplica = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
updateReadReplicaConnectionType(e.target.value);
|
||||
|
||||
const onClickAddReadReplica = () => {
|
||||
connectDBReadReplicaDispatch({
|
||||
type: 'RESET_INPUT_STATE',
|
||||
});
|
||||
updateReadReplicaConnectionType(connectionTypes.DATABASE_URL);
|
||||
const indexForName = readReplicasState.length;
|
||||
connectDBReadReplicaDispatch({
|
||||
type: 'UPDATE_DISPLAY_NAME',
|
||||
data: `read-replica-${indexForName}`,
|
||||
});
|
||||
};
|
||||
|
||||
const onClickCancelOnReadReplicaForm = () =>
|
||||
connectDBReadReplicaDispatch({
|
||||
type: 'RESET_INPUT_STATE',
|
||||
});
|
||||
|
||||
const onClickSaveReadReplicaForm = () => {
|
||||
readReplicaDispatch({
|
||||
type: 'ADD_READ_REPLICA',
|
||||
data: {
|
||||
...connectDBStateForReadReplica,
|
||||
chosenConnectionType: readReplicaConnectionType,
|
||||
},
|
||||
});
|
||||
connectDBReadReplicaDispatch({
|
||||
type: 'RESET_INPUT_STATE',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabbed tabName="connect">
|
||||
<form onSubmit={onSubmit} className={styles.connect_db_content}>
|
||||
<h4
|
||||
className={`${styles.remove_pad_bottom} ${styles.connect_db_header}`}
|
||||
>
|
||||
Connect Database Via
|
||||
</h4>
|
||||
<div
|
||||
className={styles.connect_db_radios}
|
||||
onChange={onChangeConnectionType}
|
||||
>
|
||||
{connectionRadios.map(
|
||||
(radioBtn: {
|
||||
value: string;
|
||||
title: string;
|
||||
disableOnEdit: boolean;
|
||||
}) => (
|
||||
<label
|
||||
key={`label-${radioBtn.title}`}
|
||||
className={styles.connect_db_radio_label}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={radioBtn.value}
|
||||
name={connectionRadioName}
|
||||
checked={connectionType === radioBtn.value}
|
||||
disabled={
|
||||
isEditState === radioBtn.disableOnEdit && isEditState
|
||||
}
|
||||
/>
|
||||
{radioBtn.title}
|
||||
</label>
|
||||
)
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className={`${styles.connect_db_content} ${styles.connect_form_width}`}
|
||||
>
|
||||
<ConnectDatabaseForm
|
||||
connectionDBState={connectDBInputState}
|
||||
connectionDBStateDispatch={connectDBDispatch}
|
||||
connectionTypeState={connectionType}
|
||||
updateConnectionTypeRadio={onChangeConnectionType}
|
||||
/>
|
||||
{/* Should be rendered only on Pro and Cloud Console */}
|
||||
{connectDBInputState.dbType !== 'mssql' &&
|
||||
(window.__env.consoleId || window.__env.userRole) && (
|
||||
<ReadReplicaForm
|
||||
readReplicaState={readReplicasState}
|
||||
readReplicaDispatch={readReplicaDispatch}
|
||||
connectDBState={connectDBStateForReadReplica}
|
||||
connectDBStateDispatch={connectDBReadReplicaDispatch}
|
||||
readReplicaConnectionType={readReplicaConnectionType}
|
||||
updateReadReplicaConnectionType={updateRadioForReadReplica}
|
||||
onClickAddReadReplicaCb={onClickAddReadReplica}
|
||||
onClickCancelOnReadReplicaCb={onClickCancelOnReadReplicaForm}
|
||||
onClickSaveReadReplicaCb={onClickSaveReadReplicaForm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.connect_form_layout}>
|
||||
<LabeledInput
|
||||
onChange={e =>
|
||||
connectDBDispatch({
|
||||
type: 'UPDATE_DISPLAY_NAME',
|
||||
data: e.target.value,
|
||||
})
|
||||
}
|
||||
value={connectDBInputState.displayName}
|
||||
label="Database Display Name"
|
||||
placeholder="database name"
|
||||
data-test="database-display-name"
|
||||
/>
|
||||
<label
|
||||
key="Data Source Driver"
|
||||
className={styles.connect_db_input_label}
|
||||
|
||||
<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"
|
||||
>
|
||||
Data Source Driver
|
||||
</label>
|
||||
<select
|
||||
key="connect-db-type"
|
||||
value={connectDBInputState.dbType}
|
||||
onChange={e =>
|
||||
connectDBDispatch({
|
||||
type: 'UPDATE_DB_DRIVER',
|
||||
data: e.target.value as Driver,
|
||||
})
|
||||
}
|
||||
className={`form-control ${styles.connect_db_input_pad}`}
|
||||
data-test="database-type"
|
||||
>
|
||||
<option key="postgres" value="postgres">
|
||||
Postgres
|
||||
</option>
|
||||
<option key="mssql" value="mssql">
|
||||
MS Server
|
||||
</option>
|
||||
</select>
|
||||
{connectionType === connectionTypes.DATABASE_URL ||
|
||||
(connectionType === connectionTypes.CONNECTION_PARAMS &&
|
||||
connectDBInputState.dbType === 'mssql') ? (
|
||||
<LabeledInput
|
||||
label="Database URL"
|
||||
onChange={e =>
|
||||
connectDBDispatch({
|
||||
type: 'UPDATE_DB_URL',
|
||||
data: e.target.value,
|
||||
})
|
||||
}
|
||||
value={connectDBInputState.databaseURLState.dbURL}
|
||||
placeholder={dbTypePlaceholders[connectDBInputState.dbType]}
|
||||
disabled={isEditState}
|
||||
data-test="database-url"
|
||||
/>
|
||||
) : null}
|
||||
{connectionType === connectionTypes.ENV_VAR ? (
|
||||
<LabeledInput
|
||||
label="Environment Variable"
|
||||
placeholder="HASURA_GRAPHQL_DB_URL_FROM_ENV"
|
||||
onChange={e =>
|
||||
connectDBDispatch({
|
||||
type: 'UPDATE_DB_URL_ENV_VAR',
|
||||
data: e.target.value,
|
||||
})
|
||||
}
|
||||
value={connectDBInputState.envVarURLState.envVarURL}
|
||||
data-test="database-url-env"
|
||||
/>
|
||||
) : null}
|
||||
{connectionType === connectionTypes.CONNECTION_PARAMS &&
|
||||
connectDBInputState.dbType !== 'mssql' ? (
|
||||
<>
|
||||
<LabeledInput
|
||||
label="Host"
|
||||
placeholder="localhost"
|
||||
onChange={e =>
|
||||
connectDBDispatch({
|
||||
type: 'UPDATE_DB_HOST',
|
||||
data: e.target.value,
|
||||
})
|
||||
}
|
||||
value={connectDBInputState.connectionParamState.host}
|
||||
data-test="host"
|
||||
/>
|
||||
<LabeledInput
|
||||
label="Port"
|
||||
placeholder="5432"
|
||||
onChange={e =>
|
||||
connectDBDispatch({
|
||||
type: 'UPDATE_DB_PORT',
|
||||
data: e.target.value,
|
||||
})
|
||||
}
|
||||
value={connectDBInputState.connectionParamState.port}
|
||||
data-test="port"
|
||||
/>
|
||||
<LabeledInput
|
||||
label="Username"
|
||||
placeholder="postgres_user"
|
||||
onChange={e =>
|
||||
connectDBDispatch({
|
||||
type: 'UPDATE_DB_USERNAME',
|
||||
data: e.target.value,
|
||||
})
|
||||
}
|
||||
value={connectDBInputState.connectionParamState.username}
|
||||
data-test="username"
|
||||
/>
|
||||
<LabeledInput
|
||||
label="Password"
|
||||
key="connect-db-password"
|
||||
type="password"
|
||||
placeholder="postgrespassword"
|
||||
onChange={e =>
|
||||
connectDBDispatch({
|
||||
type: 'UPDATE_DB_PASSWORD',
|
||||
data: e.target.value,
|
||||
})
|
||||
}
|
||||
value={connectDBInputState.connectionParamState.password}
|
||||
data-test="password"
|
||||
/>
|
||||
<LabeledInput
|
||||
key="connect-db-database-name"
|
||||
label="Database Name"
|
||||
placeholder="postgres"
|
||||
onChange={e =>
|
||||
connectDBDispatch({
|
||||
type: 'UPDATE_DB_DATABASE_NAME',
|
||||
data: e.target.value,
|
||||
})
|
||||
}
|
||||
value={connectDBInputState.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={() =>
|
||||
changeConnectionsParamState(!openConnectionSettings)
|
||||
}
|
||||
>
|
||||
{openConnectionSettings ? (
|
||||
<i className="fa fa-caret-down" />
|
||||
) : (
|
||||
<i className="fa fa-caret-right" />
|
||||
)}
|
||||
{' '}
|
||||
Connection Settings
|
||||
</a>
|
||||
</div>
|
||||
{openConnectionSettings ? (
|
||||
<div className={styles.connection_settings_form}>
|
||||
<div className={styles.connnection_settings_form_input_layout}>
|
||||
<LabeledInput
|
||||
label="Max Connections"
|
||||
type="number"
|
||||
className={`form-control ${styles.connnection_settings_form_input}`}
|
||||
placeholder="50"
|
||||
value={
|
||||
connectDBInputState.connectionSettings?.max_connections ??
|
||||
undefined
|
||||
}
|
||||
onChange={e =>
|
||||
connectDBDispatch({
|
||||
type: 'UPDATE_MAX_CONNECTIONS',
|
||||
data: e.target.value,
|
||||
})
|
||||
}
|
||||
min="0"
|
||||
labelInBold
|
||||
data-test="max-connections"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.connnection_settings_form_input_layout}>
|
||||
<LabeledInput
|
||||
label="Idle Timeout"
|
||||
type="number"
|
||||
className={`form-control ${styles.connnection_settings_form_input}`}
|
||||
placeholder="180"
|
||||
value={
|
||||
connectDBInputState.connectionSettings?.idle_timeout ??
|
||||
undefined
|
||||
}
|
||||
onChange={e =>
|
||||
connectDBDispatch({
|
||||
type: 'UPDATE_IDLE_TIMEOUT',
|
||||
data: e.target.value,
|
||||
})
|
||||
}
|
||||
min="0"
|
||||
labelInBold
|
||||
data-test="idle-timeout"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.connnection_settings_form_input_layout}>
|
||||
<LabeledInput
|
||||
label="Retries"
|
||||
type="number"
|
||||
className={`form-control ${styles.connnection_settings_form_input}`}
|
||||
placeholder="1"
|
||||
value={
|
||||
connectDBInputState.connectionSettings?.retries ??
|
||||
undefined
|
||||
}
|
||||
onChange={e =>
|
||||
connectDBDispatch({
|
||||
type: 'UPDATE_RETRIES',
|
||||
data: e.target.value,
|
||||
})
|
||||
}
|
||||
min="0"
|
||||
labelInBold
|
||||
data-test="retries"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={styles.add_button_layout}>
|
||||
<Button
|
||||
size="large"
|
||||
color="yellow"
|
||||
type="submit"
|
||||
style={{
|
||||
width: '70%',
|
||||
...(loading && { cursor: 'progress' }),
|
||||
}}
|
||||
disabled={loading}
|
||||
data-test="save-database"
|
||||
>
|
||||
{!isEditState ? 'Connect Database' : 'Edit Connection'}
|
||||
</Button>
|
||||
</div>
|
||||
{!isEditState ? 'Connect Database' : 'Edit Connection'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Tabbed>
|
||||
|
@ -11,3 +11,46 @@
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.read_replicas_heading {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
// hr doesn't work well within flexbox and it's a known issue
|
||||
// See https://dev.to/alvaromontoro/the-disappearing-line-a-css-mystery-3e35
|
||||
.line_width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add_button_styles {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.read_replica_add_form {
|
||||
width: 100%;
|
||||
padding: 18px 12px;
|
||||
background-color: white;
|
||||
border: 1px solid rgba(207, 205, 205, 0.6);
|
||||
}
|
||||
|
||||
.read_replica_action_bar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.read_replica_list_item {
|
||||
width: 125%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.remove_replica_btn {
|
||||
margin-right: 8px;
|
||||
color: rgb(214, 29, 29);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { DataSource } from '../../../../metadata/types';
|
||||
import { Driver } from '../../../../dataSources';
|
||||
|
||||
const parseURI = (url: string) => {
|
||||
export const parseURI = (url: string) => {
|
||||
try {
|
||||
const pattern = /^(?:([^:/?#\s]+):\/{2})?(?:([^@/?#\s]+)@)?([^/?#\s]+)?(?:\/([^?#\s]*))?(?:[?]([^#\s]+))?\S*$/;
|
||||
const matches = url.match(pattern);
|
||||
|
@ -0,0 +1,278 @@
|
||||
import React, { useState, Dispatch, ChangeEvent } from 'react';
|
||||
|
||||
import {
|
||||
ReadReplicaState,
|
||||
ReadReplicaActions,
|
||||
ConnectDBState,
|
||||
ConnectDBActions,
|
||||
ExtendedConnectDBState,
|
||||
connectionTypes,
|
||||
} from './state';
|
||||
import ConnectDatabaseForm from './ConnectDBForm';
|
||||
import Button from '../../../Common/Button';
|
||||
import ToolTip from '../../../Common/Tooltip/Tooltip';
|
||||
import {
|
||||
makeConnectionStringFromConnectionParams,
|
||||
parseURI,
|
||||
} from './ManageDBUtils';
|
||||
|
||||
import styles from './DataSources.scss';
|
||||
|
||||
const checkIfFieldsAreEmpty = (
|
||||
currentReadReplicaConnectionType: string,
|
||||
currentReadReplicaState: ConnectDBState
|
||||
) => {
|
||||
// split into 3 different conditions for better readability
|
||||
if (
|
||||
currentReadReplicaConnectionType === connectionTypes.DATABASE_URL &&
|
||||
!currentReadReplicaState?.databaseURLState?.dbURL
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
currentReadReplicaConnectionType === connectionTypes.ENV_VAR &&
|
||||
!currentReadReplicaState?.envVarURLState?.envVarURL
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
currentReadReplicaConnectionType === connectionTypes.CONNECTION_PARAMS &&
|
||||
!currentReadReplicaState?.connectionParamState?.database &&
|
||||
!currentReadReplicaState?.connectionParamState?.host &&
|
||||
!currentReadReplicaState?.connectionParamState?.port &&
|
||||
!currentReadReplicaState?.connectionParamState?.username
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
type ReadReplicaProps = {
|
||||
readReplicaState: ReadReplicaState;
|
||||
readReplicaDispatch: Dispatch<ReadReplicaActions>;
|
||||
connectDBState: ConnectDBState;
|
||||
connectDBStateDispatch: Dispatch<ConnectDBActions>;
|
||||
readReplicaConnectionType: string;
|
||||
updateReadReplicaConnectionType: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onClickAddReadReplicaCb?: () => void;
|
||||
onClickCancelOnReadReplicaCb?: () => void;
|
||||
onClickSaveReadReplicaCb?: () => void;
|
||||
};
|
||||
|
||||
interface FormProps
|
||||
extends Pick<
|
||||
ReadReplicaProps,
|
||||
| 'connectDBState'
|
||||
| 'connectDBStateDispatch'
|
||||
| 'readReplicaConnectionType'
|
||||
| 'updateReadReplicaConnectionType'
|
||||
> {
|
||||
onClickCancel: () => void;
|
||||
onClickSave: () => void;
|
||||
}
|
||||
|
||||
const Form: React.FC<FormProps> = ({
|
||||
connectDBState,
|
||||
connectDBStateDispatch,
|
||||
onClickCancel,
|
||||
onClickSave,
|
||||
readReplicaConnectionType,
|
||||
updateReadReplicaConnectionType,
|
||||
}) => {
|
||||
const areFieldsEmpty = checkIfFieldsAreEmpty(
|
||||
readReplicaConnectionType,
|
||||
connectDBState
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.read_replica_add_form}>
|
||||
<ConnectDatabaseForm
|
||||
connectionDBState={connectDBState}
|
||||
connectionDBStateDispatch={connectDBStateDispatch}
|
||||
updateConnectionTypeRadio={updateReadReplicaConnectionType}
|
||||
connectionTypeState={readReplicaConnectionType}
|
||||
isreadreplica
|
||||
title="Connect Read Replica via"
|
||||
/>
|
||||
<div className={styles.read_replica_action_bar}>
|
||||
<Button size="sm" color="white" onClick={onClickCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="yellow"
|
||||
onClick={onClickSave}
|
||||
disabled={areFieldsEmpty}
|
||||
>
|
||||
Add Read Replica
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ReadReplicaListItemProps = {
|
||||
currentState: ExtendedConnectDBState;
|
||||
onClickRemove: () => void;
|
||||
};
|
||||
|
||||
const ReadReplicaListItem: React.FC<ReadReplicaListItemProps> = ({
|
||||
currentState,
|
||||
onClickRemove,
|
||||
}) => {
|
||||
const [showUrl, setShowUrl] = useState(false);
|
||||
const connectionType = currentState.chosenConnectionType;
|
||||
const isFromEnvVar = connectionType === connectionTypes.ENV_VAR;
|
||||
let connectionString = '';
|
||||
if (!isFromEnvVar) {
|
||||
if (connectionType === connectionTypes.DATABASE_URL) {
|
||||
connectionString = currentState?.databaseURLState?.dbURL?.trim() ?? '';
|
||||
} else {
|
||||
connectionString = makeConnectionStringFromConnectionParams({
|
||||
dbType: 'postgres',
|
||||
...currentState.connectionParamState,
|
||||
});
|
||||
}
|
||||
}
|
||||
const host = isFromEnvVar ? '' : parseURI(connectionString)?.host ?? '';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.read_replica_list_item}
|
||||
key={`read-replica-item-${currentState.displayName}`}
|
||||
>
|
||||
<Button
|
||||
color="white"
|
||||
size="xs"
|
||||
onClick={onClickRemove}
|
||||
className={styles.remove_replica_btn}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<p>{isFromEnvVar ? currentState.envVarURLState.envVarURL : host}</p>
|
||||
{/* The connection string is redundant if it's provided via ENV VAR */}
|
||||
{!isFromEnvVar && (
|
||||
<span
|
||||
className={`${styles.db_large_string_break_words} ${styles.add_pad_top_10}`}
|
||||
>
|
||||
{showUrl ? (
|
||||
connectionString
|
||||
) : (
|
||||
<span
|
||||
className={styles.show_connection_string}
|
||||
onClick={() => setShowUrl(true)}
|
||||
>
|
||||
<i
|
||||
className={`${styles.showAdminSecret} fa fa-eye`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p style={{ marginLeft: 6 }}>Show Connection String</p>
|
||||
</span>
|
||||
)}
|
||||
{showUrl && (
|
||||
<ToolTip
|
||||
id="connection-string-hide"
|
||||
placement="right"
|
||||
message="Hide connection string"
|
||||
>
|
||||
<i
|
||||
className={`${styles.closeHeader} fa fa-times`}
|
||||
aria-hidden="true"
|
||||
onClick={() => setShowUrl(false)}
|
||||
style={{ paddingLeft: 10 }}
|
||||
/>
|
||||
</ToolTip>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ReadReplicaForm: React.FC<ReadReplicaProps> = ({
|
||||
connectDBState,
|
||||
connectDBStateDispatch,
|
||||
readReplicaState,
|
||||
readReplicaDispatch,
|
||||
onClickAddReadReplicaCb,
|
||||
onClickCancelOnReadReplicaCb,
|
||||
onClickSaveReadReplicaCb,
|
||||
readReplicaConnectionType,
|
||||
updateReadReplicaConnectionType,
|
||||
}) => {
|
||||
const [isReadReplicaButtonClicked, updateClickState] = useState(false);
|
||||
|
||||
const onClickAddReadReplica = () => {
|
||||
updateClickState(true);
|
||||
if (onClickAddReadReplicaCb) {
|
||||
onClickAddReadReplicaCb();
|
||||
}
|
||||
};
|
||||
|
||||
const onClickCancelOnReadReplica = () => {
|
||||
updateClickState(false);
|
||||
if (onClickCancelOnReadReplicaCb) {
|
||||
onClickCancelOnReadReplicaCb();
|
||||
}
|
||||
};
|
||||
|
||||
const onClickSaveReadReplica = () => {
|
||||
updateClickState(false);
|
||||
if (onClickSaveReadReplicaCb) {
|
||||
onClickSaveReadReplicaCb();
|
||||
}
|
||||
};
|
||||
|
||||
const onClickRemoveReadReplica = (replicaDBName: string) => () =>
|
||||
readReplicaDispatch({
|
||||
type: 'REMOVE_READ_REPLICA',
|
||||
data: replicaDBName,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr className={styles.line_width} />
|
||||
<div className={styles.flexColumn}>
|
||||
<h5 className={styles.read_replicas_heading}>Read Replicas</h5>
|
||||
<p>
|
||||
Hasura Cloud can load balance queries and subscriptions across read
|
||||
replicas while sending all mutations and metadata API calls to the
|
||||
master.
|
||||
<a
|
||||
href="https://hasura.io/docs/latest/graphql/cloud/read-replicas.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i>(Read More)</i>
|
||||
</a>
|
||||
</p>
|
||||
{readReplicaState.map(stateVar => (
|
||||
<ReadReplicaListItem
|
||||
currentState={stateVar}
|
||||
onClickRemove={onClickRemoveReadReplica(stateVar.displayName)}
|
||||
/>
|
||||
))}
|
||||
{!isReadReplicaButtonClicked ? (
|
||||
<Button
|
||||
onClick={onClickAddReadReplica}
|
||||
className={styles.add_button_styles}
|
||||
>
|
||||
Add Read Replica
|
||||
</Button>
|
||||
) : (
|
||||
<Form
|
||||
connectDBState={connectDBState}
|
||||
connectDBStateDispatch={connectDBStateDispatch}
|
||||
onClickCancel={onClickCancelOnReadReplica}
|
||||
onClickSave={onClickSaveReadReplica}
|
||||
readReplicaConnectionType={readReplicaConnectionType}
|
||||
updateReadReplicaConnectionType={updateReadReplicaConnectionType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<hr className={styles.line_width} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReadReplicaForm;
|
@ -2,6 +2,7 @@ import { Driver } from '../../../../dataSources';
|
||||
import { makeConnectionStringFromConnectionParams } from './ManageDBUtils';
|
||||
import { addDataSource } from '../../../../metadata/actions';
|
||||
import { Dispatch } from '../../../../types';
|
||||
import { SourceConnectionInfo } from '../../../../metadata/types';
|
||||
|
||||
export const connectionTypes = {
|
||||
DATABASE_URL: 'DATABASE_URL',
|
||||
@ -84,7 +85,8 @@ export const connectDataSource = (
|
||||
dispatch: Dispatch,
|
||||
typeConnection: string,
|
||||
currentState: ConnectDBState,
|
||||
cb: () => void
|
||||
cb: () => void,
|
||||
replicas?: Omit<SourceConnectionInfo, 'connection_string'>[]
|
||||
) => {
|
||||
let databaseURL:
|
||||
| string
|
||||
@ -108,7 +110,8 @@ export const connectDataSource = (
|
||||
connection_pool_settings: currentState.connectionSettings,
|
||||
},
|
||||
},
|
||||
cb
|
||||
cb,
|
||||
replicas
|
||||
)
|
||||
);
|
||||
};
|
||||
@ -254,3 +257,81 @@ export const connectDBReducer = (
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ExtendedConnectDBState extends ConnectDBState {
|
||||
chosenConnectionType: string;
|
||||
}
|
||||
|
||||
const defaultReadReplicasState: ExtendedConnectDBState[] = [];
|
||||
export type ReadReplicaState = ExtendedConnectDBState[];
|
||||
|
||||
export interface AddReadReplicaToState {
|
||||
type: 'ADD_READ_REPLICA';
|
||||
data: ExtendedConnectDBState;
|
||||
}
|
||||
|
||||
export interface RemoveReadReplicaFromState {
|
||||
type: 'REMOVE_READ_REPLICA';
|
||||
// the default name is set using index `read-replica-${index}`
|
||||
// we can use that to simplify removal
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface ResetReadReplicaState {
|
||||
type: 'RESET_READ_REPLICA_STATE';
|
||||
}
|
||||
|
||||
export type ReadReplicaActions =
|
||||
| AddReadReplicaToState
|
||||
| RemoveReadReplicaFromState
|
||||
| ResetReadReplicaState;
|
||||
|
||||
export const readReplicaReducer = (
|
||||
state: ReadReplicaState,
|
||||
action: ReadReplicaActions
|
||||
): ReadReplicaState => {
|
||||
switch (action.type) {
|
||||
case 'ADD_READ_REPLICA':
|
||||
return [...state, action.data];
|
||||
case 'REMOVE_READ_REPLICA':
|
||||
return state.filter(st => st.displayName !== action.data);
|
||||
case 'RESET_READ_REPLICA_STATE':
|
||||
return defaultReadReplicasState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const makeReadReplicaConnectionObject = (
|
||||
stateVal: ExtendedConnectDBState
|
||||
) => {
|
||||
let database_url;
|
||||
if (stateVal.chosenConnectionType === connectionTypes.DATABASE_URL) {
|
||||
database_url = stateVal.databaseURLState?.dbURL?.trim() ?? '';
|
||||
} else if (stateVal.chosenConnectionType === connectionTypes.ENV_VAR) {
|
||||
database_url = {
|
||||
from_env: stateVal.envVarURLState?.envVarURL?.trim() ?? '',
|
||||
};
|
||||
} else {
|
||||
database_url = makeConnectionStringFromConnectionParams({
|
||||
dbType: 'postgres',
|
||||
...stateVal.connectionParamState,
|
||||
});
|
||||
}
|
||||
|
||||
const pool_settings: any = {};
|
||||
if (stateVal.connectionSettings.max_connections) {
|
||||
pool_settings.max_connections = stateVal.connectionSettings.max_connections;
|
||||
}
|
||||
if (stateVal.connectionSettings.idle_timeout) {
|
||||
pool_settings.idle_timeout = stateVal.connectionSettings.idle_timeout;
|
||||
}
|
||||
if (stateVal.connectionSettings.retries) {
|
||||
pool_settings.retries = stateVal.connectionSettings.retries;
|
||||
}
|
||||
|
||||
return {
|
||||
database_url,
|
||||
pool_settings,
|
||||
};
|
||||
};
|
||||
|
@ -18,7 +18,6 @@ import ToolTip from '../../../Common/Tooltip/Tooltip';
|
||||
import { getConfirmation } from '../../../Common/utils/jsUtils';
|
||||
import { mapDispatchToPropsEmpty } from '../../../Common/utils/reactUtils';
|
||||
import _push from '../push';
|
||||
import { getHostFromConnectionString } from '../DataSources/ManageDBUtils';
|
||||
import { isInconsistentSource } from '../utils';
|
||||
|
||||
const driverToLabel: Record<Driver, string> = {
|
||||
@ -96,15 +95,19 @@ const DatabaseListItem: React.FC<DatabaseListItemProps> = ({
|
||||
>
|
||||
{removing ? 'Removing...' : 'Remove'}
|
||||
</Button>
|
||||
<div
|
||||
className={`${styles.displayFlexContainer} ${styles.add_pad_left} ${styles.add_pad_top_10}`}
|
||||
>
|
||||
<b>{dataSource.name}</b>
|
||||
<p>({driverToLabel[dataSource.driver]})</p>
|
||||
<div className={styles.flexColumn}>
|
||||
<div
|
||||
className={`${styles.displayFlexContainer} ${styles.add_pad_left} ${styles.add_pad_top_10}`}
|
||||
>
|
||||
<b>{dataSource.name}</b>
|
||||
<p>({driverToLabel[dataSource.driver]})</p>
|
||||
{!!dataSource?.read_replicas?.length && (
|
||||
<div className={styles.replica_badge}>
|
||||
{dataSource.read_replicas.length} Replicas
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className={`${styles.add_pad_top_10} ${styles.add_pad_left}`}>
|
||||
{getHostFromConnectionString(dataSource)}
|
||||
</p>
|
||||
{isInconsistentDataSource && (
|
||||
<ToolTip
|
||||
id={`inconsistent-source-${dataSource.name}`}
|
||||
@ -142,7 +145,7 @@ const DatabaseListItem: React.FC<DatabaseListItemProps> = ({
|
||||
{showUrl && (
|
||||
<ToolTip
|
||||
id="connection-string-hide"
|
||||
placement="right"
|
||||
placement="top"
|
||||
message="Hide connection string"
|
||||
>
|
||||
<i
|
||||
|
@ -52,3 +52,18 @@
|
||||
color: #c02020;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.replica_badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #ccc;
|
||||
width: auto;
|
||||
border-radius: 10px;
|
||||
height: 20px;
|
||||
padding: 8px;
|
||||
font-size: 13px;
|
||||
color: black;
|
||||
margin: 0px 4px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
@ -1,6 +1,11 @@
|
||||
import requestAction from '../utils/requestAction';
|
||||
import Endpoints, { globalCookiePolicy } from '../Endpoints';
|
||||
import { HasuraMetadataV2, HasuraMetadataV3, RestEndpointEntry } from './types';
|
||||
import {
|
||||
HasuraMetadataV2,
|
||||
HasuraMetadataV3,
|
||||
RestEndpointEntry,
|
||||
SourceConnectionInfo,
|
||||
} from './types';
|
||||
import {
|
||||
showSuccessNotification,
|
||||
showErrorNotification,
|
||||
@ -227,6 +232,7 @@ export const exportMetadata = (
|
||||
export const addDataSource = (
|
||||
data: AddDataSourceRequest['data'],
|
||||
successCb: () => void,
|
||||
replicas?: Omit<SourceConnectionInfo, 'connection_string'>[],
|
||||
skipNotification = false
|
||||
): Thunk<Promise<void | ReduxState>, MetadataActions> => (
|
||||
dispatch,
|
||||
@ -234,7 +240,7 @@ export const addDataSource = (
|
||||
) => {
|
||||
const { dataHeaders } = getState().tables;
|
||||
|
||||
const query = addSource(data.driver, data.payload);
|
||||
const query = addSource(data.driver, data.payload, replicas);
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
@ -350,6 +356,7 @@ export const editDataSource = (
|
||||
);
|
||||
onSuccessCb();
|
||||
},
|
||||
[],
|
||||
true
|
||||
)
|
||||
).catch(err => {
|
||||
|
@ -361,6 +361,7 @@ export const getDataSources = createSelector(getMetadata, metadata => {
|
||||
max_connections: 50,
|
||||
},
|
||||
driver: source.kind || 'postgres',
|
||||
read_replicas: source.configuration?.read_replicas ?? undefined,
|
||||
});
|
||||
});
|
||||
return sources;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Driver } from '../dataSources';
|
||||
import { SourceConnectionInfo } from './types';
|
||||
|
||||
export const addSource = (
|
||||
driver: Driver,
|
||||
@ -10,7 +11,9 @@ export const addSource = (
|
||||
idle_timeout?: number;
|
||||
retries?: number;
|
||||
};
|
||||
}
|
||||
},
|
||||
// supported only for PG sources at the moment
|
||||
replicas?: Omit<SourceConnectionInfo, 'connection_string'>[]
|
||||
) => {
|
||||
if (driver === 'mssql') {
|
||||
return {
|
||||
@ -36,6 +39,7 @@ export const addSource = (
|
||||
database_url: payload.dbUrl,
|
||||
pool_settings: payload.connection_pool_settings,
|
||||
},
|
||||
read_replicas: replicas?.length ? replicas : null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -10,6 +10,7 @@ export type DataSource = {
|
||||
idle_timeout?: number;
|
||||
retries?: number;
|
||||
};
|
||||
read_replicas?: Omit<SourceConnectionInfo, 'connection_string'>[];
|
||||
};
|
||||
|
||||
// GENERATED
|
||||
@ -865,6 +866,22 @@ export interface RestEndpointEntry {
|
||||
// #endregion REST ENDPOINT
|
||||
// /////////////////////////////
|
||||
|
||||
/**
|
||||
* Docs for type: https://hasura.io/docs/latest/graphql/core/api-reference/syntax-defs.html#pgsourceconnectioninfo
|
||||
*/
|
||||
|
||||
export interface SourceConnectionInfo {
|
||||
// used for SQL Server
|
||||
connection_string: string;
|
||||
// used for Postgres
|
||||
database_url: string | { from_env: string };
|
||||
pool_settings: {
|
||||
max_connections: number;
|
||||
idle_timeout: number;
|
||||
retries: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Type used in exported 'metadata.json' and replace metadata endpoint
|
||||
* https://hasura.io/docs/latest/graphql/core/api-reference/schema-metadata-api/manage-metadata.html#replace-metadata
|
||||
@ -886,17 +903,11 @@ export interface HasuraMetadataV2 {
|
||||
|
||||
export interface MetadataDataSource {
|
||||
name: string;
|
||||
kind?: 'postgres' | 'mysql';
|
||||
kind?: 'postgres' | 'mysql' | 'mssql';
|
||||
configuration?: {
|
||||
connection_info?: {
|
||||
connection_string?: string;
|
||||
database_url?: string | { from_env: string };
|
||||
pool_settings?: {
|
||||
max_connections: number;
|
||||
idle_timeout: number;
|
||||
retries: number;
|
||||
};
|
||||
};
|
||||
connection_info?: SourceConnectionInfo;
|
||||
// pro-only feature
|
||||
read_replicas?: SourceConnectionInfo[];
|
||||
};
|
||||
tables: TableEntry[];
|
||||
functions?: Array<{
|
||||
|
Loading…
Reference in New Issue
Block a user