console: support multi tenant connection pooling on cloud

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5950
Co-authored-by: Matthew Goodwin <49927862+m4ttheweric@users.noreply.github.com>
GitOrigin-RevId: 47db9dd70235242002533ce64ef2f987a816d4fe
This commit is contained in:
Sooraj 2022-09-27 17:52:02 +05:30 committed by hasura-bot
parent 2f86f71d4d
commit 265311a4cc
30 changed files with 686 additions and 375 deletions

View File

@ -70,6 +70,7 @@ Environment variables accepted in `server` mode:
- `SERVER_VERSION`: Hasura GraphQL Engine server version
- `CONSOLE_MODE`: In server mode, it should be `server`
- `IS_ADMIN_SECRET_SET`: Is GraphQl engine configured with an admin secret (`true`/`false`)
- `HASURA_CONSOLE_TYPE`: The environment where the console is running, this could be oss, pro or cloud
Here's an example `.env` file for `server` mode:
@ -84,6 +85,7 @@ URL_PREFIX=/
DATA_API_URL=http://localhost:8080
SERVER_VERSION=v1.0.0
CONSOLE_MODE=server
HASURA_CONSOLE_TYPE=oss
IS_ADMIN_SECRET_SET=true
```

View File

@ -80,7 +80,10 @@ export { ReactQueryProvider, reactQueryClient } from '../src/lib/reactQuery';
export { FeatureFlags } from '../src/features/FeatureFlags';
export { isMonitoringTabSupportedEnvironment } from '../src/utils/proConsole';
export {
isMonitoringTabSupportedEnvironment,
isEnvironmentSupportMultiTenantConnectionPooling,
} from '../src/utils/proConsole';
export {
SampleDBBanner,

View File

@ -29,10 +29,13 @@ module.exports = {
__DEVTOOLS__: true,
socket: true,
webpackIsomorphicTools: true,
__env: {
consoleType: 'oss',
},
window: {
__env: {
nodeEnv: 'development',
serverVersion: 'v1.0.0',
serverVersion: 'v1.0.0', // FIXME : moving this to the above __env block seem to be breaking some existing tests
},
},
},

View File

@ -6,8 +6,8 @@ import { isEmpty } from './components/Common/utils/jsUtils';
import { stripTrailingSlash } from './components/Common/utils/urlUtils';
import { SERVER_CONSOLE_MODE } from './constants';
import { parseConsoleType, ConsoleType } from './utils/envUtils';
type ConsoleType = 'oss' | 'cloud' | 'pro' | 'pro-lite';
export type LuxFeature =
| 'DatadogIntegration'
| 'ProUser'
@ -221,7 +221,9 @@ const globals = {
luxDataHost: window.__env?.luxDataHost,
userRole: window.__env?.userRole || undefined,
userId: window.__env?.userId || undefined,
consoleType: window.__env?.consoleType || '',
consoleType: window.__env?.consoleType // FIXME : this check can be removed when the all CLI environments are set with the console type, some CLI environments could have empty consoleType
? parseConsoleType(window.__env?.consoleType)
: ('' as ConsoleType),
eeMode: window.__env?.eeMode === 'true',
};
if (globals.consoleMode === SERVER_CONSOLE_MODE) {

View File

@ -10,7 +10,7 @@ import styles from './DataSources.module.scss';
import JSONEditor from '../TablePermissions/JSONEditor';
import { SupportedFeaturesType } from '../../../../dataSources/types';
import { Path } from '../../../Common/utils/tsUtils';
import ConnectionSettingsForm from './ConnectionSettingsForm';
import ConnectionSettingsForm from './ConnectionSettings/ConnectionSettingsForm';
import { GraphQLFieldCustomizationContainer } from './GraphQLFieldCustomization/GraphQLFieldCustomizationContainer';
import { SampleDBTrial } from './SampleDatabase';

View File

@ -0,0 +1,49 @@
import React, { Dispatch } from 'react';
import { ConnectDBActions, ConnectDBState } from '../state';
import { buildFormSettings } from './buildFormSettings';
import {
FormContainer,
ConnectionLifetime,
CumulativeMaxConnections,
IdleTimeout,
IsolationLevel,
MaxConnections,
PoolTimeout,
PreparedStatements,
Retries,
SSLCertificates,
} from './parts';
export interface ConnectionSettingsFormProps {
connectionDBState: ConnectDBState;
connectionDBStateDispatch: Dispatch<ConnectDBActions>;
}
const ConnectionSettingsForm: React.FC<ConnectionSettingsFormProps> = props => {
const { connectionDBState } = props;
const formSettings = React.useMemo(
() => buildFormSettings(connectionDBState.dbType),
[connectionDBState.dbType]
);
if (!formSettings.connectionSettings) return null;
return (
<FormContainer>
<MaxConnections {...props} />
{formSettings.cumulativeMaxConnections && (
<CumulativeMaxConnections {...props} />
)}
<IdleTimeout {...props} />
{formSettings.retries && <Retries {...props} />}
{formSettings.pool_timeout && <PoolTimeout {...props} />}
{formSettings.connection_lifetime && <ConnectionLifetime {...props} />}
{formSettings.isolation_level && <IsolationLevel {...props} />}
{formSettings.prepared_statements && <PreparedStatements {...props} />}
{formSettings.ssl_certificates && <SSLCertificates {...props} />}
</FormContainer>
);
};
export default ConnectionSettingsForm;

View File

@ -0,0 +1,28 @@
import { getSupportedDrivers } from '@/dataSources';
import { DbConnectionSettings } from '@/dataSources/types';
import { ConnectDBState } from '../state';
export const buildFormSettings = (
dbType: ConnectDBState['dbType']
): DbConnectionSettings => {
const settings: DbConnectionSettings = {
connectionSettings: false,
cumulativeMaxConnections: false,
retries: false,
pool_timeout: false,
connection_lifetime: false,
isolation_level: false,
prepared_statements: false,
ssl_certificates: false,
};
let setting: keyof DbConnectionSettings;
// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (setting in settings) {
settings[setting] = getSupportedDrivers(
`connectDbForm.${setting}`
).includes(dbType);
}
return settings;
};

View File

@ -0,0 +1,31 @@
import { LabeledInput } from '@/components/Common/LabeledInput';
import { ConnectionSettingsFormProps } from '@/components/Services/Data/DataSources/ConnectionSettings/ConnectionSettingsForm';
import React from 'react';
import styles from '../../DataSources.module.scss';
export const ConnectionLifetime: React.VFC<ConnectionSettingsFormProps> = ({
connectionDBState,
connectionDBStateDispatch,
}) => (
<div className={styles.connection_settings_input_layout}>
<LabeledInput
label="Connection Lifetime"
tooltipText="Time (in seconds) from connection creation after which the connection should be destroyed and a new one created. A value of 0 indicates we should never destroy an active connection. If 0 is passed, memory from large query results may not be reclaimed."
type="number"
className={`form-control ${styles.connnection_settings_form_input}`}
placeholder="600"
value={
connectionDBState.connectionSettings?.connection_lifetime ?? undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_CONNECTION_LIFETIME',
data: e.target.value,
})
}
min="0"
boldlabel
data-test="connection-lifetime"
/>
</div>
);

View File

@ -0,0 +1,30 @@
import { LabeledInput } from '@/components/Common/LabeledInput';
import { ConnectionSettingsFormProps } from '@/components/Services/Data/DataSources/ConnectionSettings/ConnectionSettingsForm';
import React from 'react';
import styles from '../../DataSources.module.scss';
export const CumulativeMaxConnections: React.VFC<ConnectionSettingsFormProps> =
({ connectionDBState, connectionDBStateDispatch }) => (
<div className={styles.connection_settings_input_layout}>
<LabeledInput
label="Cumulative Max Connections"
tooltipText="Maximum number of database connections"
type="number"
className={`form-control ${styles.connnection_settings_form_input}`}
placeholder="50"
value={
connectionDBState.connectionSettings?.cumulative_max_connections ||
undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_CUMULATIVE_MAX_CONNECTIONS',
data: e.target.value,
})
}
min="0"
boldlabel
data-test="max-connections"
/>
</div>
);

View File

@ -0,0 +1,15 @@
import { Collapse } from '@/new-components/deprecated';
import React from 'react';
import styles from '../../DataSources.module.scss';
export const FormContainer: React.FC = ({ children }) => (
<div className="w-full mb-md">
<div className="cursor-pointer w-full flex-initial align-middle">
<Collapse title="Connection Settings" defaultOpen>
<Collapse.Content>
<div className={styles.connection_settings_form}>{children}</div>
</Collapse.Content>
</Collapse>
</div>
</div>
);

View File

@ -0,0 +1,29 @@
import { LabeledInput } from '@/components/Common/LabeledInput';
import { ConnectionSettingsFormProps } from '@/components/Services/Data/DataSources/ConnectionSettings/ConnectionSettingsForm';
import React from 'react';
import styles from '../../DataSources.module.scss';
export const IdleTimeout: React.VFC<ConnectionSettingsFormProps> = ({
connectionDBState,
connectionDBStateDispatch,
}) => (
<div className={styles.connection_settings_input_layout}>
<LabeledInput
label="Idle Timeout"
tooltipText="The idle timeout (in seconds) per connection"
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>
);

View File

@ -0,0 +1,48 @@
import { ConnectionSettingsFormProps } from '@/components/Services/Data/DataSources/ConnectionSettings/ConnectionSettingsForm';
import { IsolationLevelOptions } from '@/metadata/types';
import { IconTooltip } from '@/new-components/Tooltip';
import React from 'react';
import styles from '../../DataSources.module.scss';
const ISOLATION_LEVEL_OPTIONS: readonly string[] = Object.freeze([
'read-committed',
'repeatable-read',
'serializable',
]);
const isIsolationLevelOption = (
value: string
): value is IsolationLevelOptions => {
return ['read-committed', 'repeatable-read', 'serializable'].includes(value);
};
export const IsolationLevel: React.VFC<ConnectionSettingsFormProps> = ({
connectionDBState,
connectionDBStateDispatch,
}) => (
<div className={styles.connection_settings_input_layout}>
<label className="flex items-center gap-1">
<b>Isolation Level</b>
<IconTooltip message="The transaction isolation level in which the queries made to the source will be run" />
</label>
<select
className={`form-control ${styles.connnection_settings_form_input} cursor-pointer`}
onChange={e => {
// any way to do this?
if (isIsolationLevelOption(e.target.value)) {
connectionDBStateDispatch({
type: 'UPDATE_ISOLATION_LEVEL',
data: e.target.value,
});
}
}}
value={connectionDBState.isolationLevel}
>
{ISOLATION_LEVEL_OPTIONS.map(o => (
<option key={o} value={o}>
{o}
</option>
))}
</select>
</div>
);

View File

@ -0,0 +1,29 @@
import { LabeledInput } from '@/components/Common/LabeledInput';
import { ConnectionSettingsFormProps } from '@/components/Services/Data/DataSources/ConnectionSettings/ConnectionSettingsForm';
import React from 'react';
import styles from '../../DataSources.module.scss';
export const MaxConnections: React.VFC<ConnectionSettingsFormProps> = ({
connectionDBState,
connectionDBStateDispatch,
}) => (
<div className={styles.connection_settings_input_layout}>
<LabeledInput
label="Max Connections"
tooltipText="Maximum number of database connections per instance"
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>
);

View File

@ -0,0 +1,29 @@
import { LabeledInput } from '@/components/Common/LabeledInput';
import { ConnectionSettingsFormProps } from '@/components/Services/Data/DataSources/ConnectionSettings/ConnectionSettingsForm';
import React from 'react';
import styles from '../../DataSources.module.scss';
export const PoolTimeout: React.VFC<ConnectionSettingsFormProps> = ({
connectionDBState,
connectionDBStateDispatch,
}) => (
<div className={styles.connection_settings_input_layout}>
<LabeledInput
label="Pool Timeout"
tooltipText="Maximum time (in seconds) to wait while acquiring a Postgres connection from the pool"
type="number"
className={`form-control ${styles.connnection_settings_form_input}`}
placeholder="360"
value={connectionDBState.connectionSettings?.pool_timeout ?? undefined}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_POOL_TIMEOUT',
data: e.target.value,
})
}
min="0"
boldlabel
data-test="pool-timeout"
/>
</div>
);

View File

@ -0,0 +1,28 @@
import { ConnectionSettingsFormProps } from '@/components/Services/Data/DataSources/ConnectionSettings/ConnectionSettingsForm';
import { IconTooltip } from '@/new-components/Tooltip';
import React from 'react';
import styles from '../../DataSources.module.scss';
export const PreparedStatements: React.VFC<ConnectionSettingsFormProps> = ({
connectionDBState,
connectionDBStateDispatch,
}) => (
<div className={`${styles.add_mar_bottom_mid} ${styles.checkbox_margin_top}`}>
<label className="inline-flex items-center">
<input
type="checkbox"
checked={connectionDBState.preparedStatements}
className="legacy-input-fix"
onChange={e => {
connectionDBStateDispatch({
type: 'UPDATE_PREPARED_STATEMENTS',
data: e.target.checked,
});
}}
/>{' '}
&nbsp;
<b>Use Prepared Statements</b>
<IconTooltip message="Prepared statements are disabled by default" />
</label>
</div>
);

View File

@ -0,0 +1,29 @@
import { LabeledInput } from '@/components/Common/LabeledInput';
import { ConnectionSettingsFormProps } from '@/components/Services/Data/DataSources/ConnectionSettings/ConnectionSettingsForm';
import React from 'react';
import styles from '../../DataSources.module.scss';
export const Retries: React.VFC<ConnectionSettingsFormProps> = ({
connectionDBState,
connectionDBStateDispatch,
}) => (
<div className={styles.connection_settings_input_layout}>
<LabeledInput
label="Retries"
tooltipText="Number of retries to perform"
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>
);

View File

@ -0,0 +1,123 @@
import { LabeledInput } from '@/components/Common/LabeledInput';
import { ConnectionSettingsFormProps } from '@/components/Services/Data/DataSources/ConnectionSettings/ConnectionSettingsForm';
import { SSLModeOptions } from '@/metadata/types';
import { IconTooltip } from '@/new-components/Tooltip';
import React from 'react';
import { FaCaretDown, FaCaretRight } from 'react-icons/fa';
import styles from '../../DataSources.module.scss';
export const SSLCertificates: React.VFC<ConnectionSettingsFormProps> = ({
connectionDBState,
connectionDBStateDispatch,
}) => {
const [showCertSettings, setShowCertSettings] = React.useState(false);
const handleCertificateSettingsClick = () =>
setShowCertSettings(!showCertSettings);
return (
<div className={styles.add_mar_top}>
<div
onClick={handleCertificateSettingsClick}
className={styles.connection_settings_header}
>
(showCertSettings ? <FaCaretDown /> : <FaCaretRight />
{` SSL Certificates Settings`}
</div>
<div className={styles.text_muted}>
Certificates will be loaded from{' '}
<a href="https://hasura.io/docs/latest/graphql/cloud/projects/create.html#existing-database">
environment variables
</a>
</div>
{showCertSettings && (
<div className="mt-xs">
<div className="mb-xs">
<label className="flex items-center gap-1">
<b>SSL Mode</b>
<IconTooltip message="SSL certificate verification mode" />
</label>
<select
className="form-control"
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_SSL_MODE',
data: (e.target.value as SSLModeOptions) || undefined,
})
}
value={connectionDBState.sslConfiguration?.sslmode}
>
<option value="">--</option>
<option value="disable">disable</option>
<option value="verify-ca">verify-ca</option>
<option value="verify-full">verify-full</option>
</select>
</div>
<LabeledInput
label="SSL Root Certificate"
type="text"
placeholder="SSL_ROOT_CERT"
tooltipText="Environment variable that stores trusted certificate authorities"
value={
connectionDBState.sslConfiguration?.sslrootcert?.from_env ??
undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_SSL_ROOT_CERT',
data: e.target.value,
})
}
/>
<LabeledInput
label="SSL Certificate"
type="text"
placeholder="SSL_CERT"
tooltipText="Environment variable that stores the client certificate (Optional)"
value={
connectionDBState.sslConfiguration?.sslcert?.from_env ?? undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_SSL_CERT',
data: e.target.value,
})
}
/>
<LabeledInput
label="SSL Key"
type="text"
placeholder="SSL_KEY"
tooltipText="Environment variable that stores the client private key (Optional)"
value={
connectionDBState.sslConfiguration?.sslkey?.from_env ?? undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_SSL_KEY',
data: e.target.value,
})
}
/>
<LabeledInput
label="SSL Password"
type="text"
className="form-control"
placeholder="SSL_PASSWORD"
tooltipText="Environment variable that stores the password if the client private key is encrypted (Optional)"
value={
connectionDBState.sslConfiguration?.sslpassword?.from_env ??
undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_SSL_PASSWORD',
data: e.target.value,
})
}
/>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,10 @@
export { FormContainer } from './FormContainer';
export { CumulativeMaxConnections } from './CumulativeMaxConnections';
export { MaxConnections } from './MaxConnections';
export { IdleTimeout } from './IdleTimeout';
export { Retries } from './Retries';
export { IsolationLevel } from './IsolationLevel';
export { PoolTimeout } from './PoolTimeout';
export { ConnectionLifetime } from './ConnectionLifetime';
export { PreparedStatements } from './PreparedStatements';
export { SSLCertificates } from './SSLCertificates';

View File

@ -1,340 +0,0 @@
import React, { Dispatch, useState } from 'react';
import { Collapse } from '@/new-components/deprecated';
import { IconTooltip } from '@/new-components/Tooltip';
import { FaCaretDown, FaCaretRight } from 'react-icons/fa';
import { ConnectDBActions, ConnectDBState } from './state';
import { LabeledInput } from '../../../Common/LabeledInput';
import { getSupportedDrivers } from '../../../../dataSources';
import styles from './DataSources.module.scss';
import {
SSLModeOptions,
IsolationLevelOptions,
} from '../../../../metadata/types';
export interface ConnectionSettingsFormProps {
// Connect DB State Props
connectionDBState: ConnectDBState;
connectionDBStateDispatch: Dispatch<ConnectDBActions>;
}
const ConnectionSettingsForm: React.FC<ConnectionSettingsFormProps> = ({
connectionDBState,
connectionDBStateDispatch,
}) => {
const [certificateSettingsState, setCertificateSettingsState] =
useState(false);
return (
<>
{getSupportedDrivers('connectDbForm.connectionSettings').includes(
connectionDBState.dbType
) ? (
<div className="w-full mb-md">
<div className="cursor-pointer w-full flex-initial align-middle">
<Collapse title="Connection Settings" defaultOpen={false}>
<Collapse.Content>
<div className={styles.connection_settings_form}>
<div className={styles.connection_settings_input_layout}>
<LabeledInput
label="Max Connections"
tooltipText="Maximum number of connections to be kept in the pool"
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_input_layout}>
<LabeledInput
label="Idle Timeout"
tooltipText="The idle timeout (in seconds) per connection"
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_input_layout}>
<LabeledInput
label="Retries"
tooltipText="Number of retries to perform"
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>
) : null}
{getSupportedDrivers('connectDbForm.pool_timeout').includes(
connectionDBState.dbType
) ? (
<div className={styles.connection_settings_input_layout}>
<LabeledInput
label="Pool Timeout"
tooltipText="Maximum time (in seconds) to wait while acquiring a Postgres connection from the pool"
type="number"
className={`form-control ${styles.connnection_settings_form_input}`}
placeholder="360"
value={
connectionDBState.connectionSettings?.pool_timeout ??
undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_POOL_TIMEOUT',
data: e.target.value,
})
}
min="0"
boldlabel
data-test="pool-timeout"
/>
</div>
) : null}
{getSupportedDrivers(
'connectDbForm.connection_lifetime'
).includes(connectionDBState.dbType) ? (
<div className={styles.connection_settings_input_layout}>
<LabeledInput
label="Connection Lifetime"
tooltipText="Time (in seconds) from connection creation after which the connection should be destroyed and a new one created. A value of 0 indicates we should never destroy an active connection. If 0 is passed, memory from large query results may not be reclaimed."
type="number"
className={`form-control ${styles.connnection_settings_form_input}`}
placeholder="600"
value={
connectionDBState.connectionSettings
?.connection_lifetime ?? undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_CONNECTION_LIFETIME',
data: e.target.value,
})
}
min="0"
boldlabel
data-test="connection-lifetime"
/>
</div>
) : null}
{getSupportedDrivers(
'connectDbForm.isolation_level'
).includes(connectionDBState.dbType) && (
<div className={styles.connection_settings_input_layout}>
<label className="flex items-center gap-1">
<b>Isolation Level</b>
<IconTooltip message="The transaction isolation level in which the queries made to the source will be run" />
</label>
<select
className={`form-control ${styles.connnection_settings_form_input} cursor-pointer`}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_ISOLATION_LEVEL',
data: e.target.value as IsolationLevelOptions,
})
}
value={connectionDBState.isolationLevel}
>
<option value="read-committed">read-committed</option>
<option value="repeatable-read">repeatable-read</option>
<option value="serializable">serializable</option>
</select>
</div>
)}
{getSupportedDrivers(
'connectDbForm.prepared_statements'
).includes(connectionDBState.dbType) ? (
<div
className={`${styles.add_mar_bottom_mid} ${styles.checkbox_margin_top}`}
>
<label className="inline-flex items-center">
<input
type="checkbox"
checked={connectionDBState.preparedStatements}
className="legacy-input-fix"
onChange={e => {
connectionDBStateDispatch({
type: 'UPDATE_PREPARED_STATEMENTS',
data: e.target.checked,
});
}}
/>{' '}
&nbsp;
<b>Use Prepared Statements</b>
<IconTooltip message="Prepared statements are disabled by default" />
</label>
</div>
) : null}
{getSupportedDrivers(
'connectDbForm.ssl_certificates'
).includes(connectionDBState.dbType) && (
<div className={styles.add_mar_top}>
<div
onClick={() =>
setCertificateSettingsState(!certificateSettingsState)
}
className={styles.connection_settings_header}
>
{certificateSettingsState ? (
<FaCaretDown />
) : (
<FaCaretRight />
)}
{' '}
SSL Certificates Settings
</div>
<div className={styles.text_muted}>
Certificates will be loaded from{' '}
<a href="https://hasura.io/docs/latest/graphql/cloud/projects/create.html#existing-database">
environment variables
</a>
</div>
{certificateSettingsState ? (
<div className="mt-xs">
<div className="mb-xs">
<label className="flex items-center gap-1">
<b>SSL Mode</b>
<IconTooltip message="SSL certificate verification mode" />
</label>
<select
className="form-control"
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_SSL_MODE',
data:
(e.target.value as SSLModeOptions) ||
undefined,
})
}
value={
connectionDBState.sslConfiguration?.sslmode
}
>
<option value="">--</option>
<option value="disable">disable</option>
<option value="verify-ca">verify-ca</option>
<option value="verify-full">verify-full</option>
</select>
</div>
<LabeledInput
label="SSL Root Certificate"
type="text"
placeholder="SSL_ROOT_CERT"
tooltipText="Environment variable that stores trusted certificate authorities"
value={
connectionDBState.sslConfiguration?.sslrootcert
?.from_env ?? undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_SSL_ROOT_CERT',
data: e.target.value,
})
}
/>
<LabeledInput
label="SSL Certificate"
type="text"
placeholder="SSL_CERT"
tooltipText="Environment variable that stores the client certificate (Optional)"
value={
connectionDBState.sslConfiguration?.sslcert
?.from_env ?? undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_SSL_CERT',
data: e.target.value,
})
}
/>
<LabeledInput
label="SSL Key"
type="text"
placeholder="SSL_KEY"
tooltipText="Environment variable that stores the client private key (Optional)"
value={
connectionDBState.sslConfiguration?.sslkey
?.from_env ?? undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_SSL_KEY',
data: e.target.value,
})
}
/>
<LabeledInput
label="SSL Password"
type="text"
className="form-control"
placeholder="SSL_PASSWORD"
tooltipText="Environment variable that stores the password if the client private key is encrypted (Optional)"
value={
connectionDBState.sslConfiguration?.sslpassword
?.from_env ?? undefined
}
onChange={e =>
connectionDBStateDispatch({
type: 'UPDATE_SSL_PASSWORD',
data: e.target.value,
})
}
/>
</div>
) : null}
</div>
)}
</div>
</Collapse.Content>
</Collapse>
</div>
</div>
) : null}
</>
);
};
export default ConnectionSettingsForm;

View File

@ -1,3 +1,4 @@
import { ConsoleType } from '../../../../../../utils/envUtils';
import {
checkNestedFieldValueInErrJson,
maskPostgresError,
@ -173,7 +174,7 @@ describe('Test checkNestedFieldValueInErrJson', () => {
describe('newSampleDBTrial test', () => {
test('consoleType==oss', () => {
const sampleDBTrialService = newSampleDBTrial({
consoleType: 'oss',
consoleType: 'oss' as ConsoleType,
hasuraCloudProjectId: 'project_id',
cohortConfig: {
databaseUrl: 'test_db_url',
@ -188,7 +189,7 @@ describe('newSampleDBTrial test', () => {
test('consoleType==cloud, null config', () => {
const sampleDBTrialService = newSampleDBTrial({
consoleType: 'cloud',
consoleType: 'cloud' as ConsoleType,
hasuraCloudProjectId: 'project_id',
cohortConfig: null,
});
@ -200,7 +201,7 @@ describe('newSampleDBTrial test', () => {
test('consoleType==cloud', () => {
const sampleDBTrialService = newSampleDBTrial({
consoleType: 'cloud',
consoleType: 'cloud' as ConsoleType,
hasuraCloudProjectId: 'project_id',
cohortConfig: {
databaseUrl: 'test_db_url',
@ -215,7 +216,7 @@ describe('newSampleDBTrial test', () => {
test('isExploringSampleDB', () => {
const sampleDBTrialService = newSampleDBTrial({
consoleType: 'cloud',
consoleType: 'cloud' as ConsoleType,
hasuraCloudProjectId: 'project_id',
cohortConfig: {
databaseUrl: 'test_db_url',
@ -322,7 +323,7 @@ describe('newSampleDBTrial test', () => {
test('hasAddedSampleDB', () => {
const sampleDBTrialService = newSampleDBTrial({
consoleType: 'cloud',
consoleType: 'cloud' as ConsoleType,
hasuraCloudProjectId: 'project_id',
cohortConfig: {
databaseUrl: 'test_db_url',
@ -441,7 +442,7 @@ describe('maskPostgresError tests', () => {
errorJson: {
status_code: '42501',
},
consoleType: 'cloud',
consoleType: 'cloud' as ConsoleType,
},
output: null,
},
@ -468,7 +469,7 @@ describe('maskPostgresError tests', () => {
errorJson: {
status_code: '42501',
},
consoleType: 'cloud',
consoleType: 'cloud' as ConsoleType,
},
output: maskedErrorMessage,
},
@ -500,7 +501,7 @@ describe('maskPostgresError tests', () => {
errorJson: {
status_code: '42501',
},
consoleType: 'cloud',
consoleType: 'cloud' as ConsoleType,
},
output: null,
},
@ -535,7 +536,7 @@ describe('maskPostgresError tests', () => {
errorJson: {
status_code: '42501',
},
consoleType: 'cloud',
consoleType: 'cloud' as ConsoleType,
},
output: null,
},
@ -570,7 +571,7 @@ describe('maskPostgresError tests', () => {
errorJson: {
status_code: '42501',
},
consoleType: 'cloud',
consoleType: 'cloud' as ConsoleType,
},
output: maskedErrorMessage,
},
@ -614,7 +615,7 @@ describe('maskPostgresError tests', () => {
},
],
},
consoleType: 'cloud',
consoleType: 'cloud' as ConsoleType,
},
output: maskedErrorMessage,
},
@ -658,7 +659,7 @@ describe('maskPostgresError tests', () => {
},
],
},
consoleType: 'oss',
consoleType: 'oss' as ConsoleType,
},
output: null,
},

View File

@ -1,4 +1,5 @@
import pickBy from 'lodash.pickby';
import produce from 'immer';
import { Driver, getSupportedDrivers } from '../../../../dataSources';
import { makeConnectionStringFromConnectionParams } from './ManageDBUtils';
import { addDataSource, renameDataSource } from '../../../../metadata/actions';
@ -256,6 +257,7 @@ export type ConnectDBActions =
| { type: 'UPDATE_DB_PASSWORD'; data: string }
| { type: 'UPDATE_DB_DATABASE_NAME'; data: string }
| { type: 'UPDATE_MAX_CONNECTIONS'; data: string }
| { type: 'UPDATE_CUMULATIVE_MAX_CONNECTIONS'; data: string }
| { type: 'UPDATE_RETRIES'; data: string }
| { type: 'UPDATE_IDLE_TIMEOUT'; data: string }
| { type: 'UPDATE_POOL_TIMEOUT'; data: string }
@ -385,6 +387,12 @@ export const connectDBReducer = (
max_connections: setNumberFromString(action.data),
},
};
case 'UPDATE_CUMULATIVE_MAX_CONNECTIONS':
return produce(state, (draft: ConnectDBState) => {
draft.connectionSettings = state.connectionSettings ?? {};
draft.connectionSettings.cumulative_max_connections =
setNumberFromString(action.data);
});
case 'UPDATE_RETRIES':
return {
...state,
@ -660,6 +668,10 @@ export const makeReadReplicaConnectionObject = (
if (stateVal.connectionSettings?.max_connections) {
pool_settings.max_connections = stateVal.connectionSettings.max_connections;
}
if (stateVal.connectionSettings?.cumulative_max_connections) {
pool_settings.cumulative_max_connections =
stateVal.connectionSettings.cumulative_max_connections;
}
if (stateVal.connectionSettings?.idle_timeout) {
pool_settings.idle_timeout = stateVal.connectionSettings.idle_timeout;
}

View File

@ -215,6 +215,7 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
isolation_level: false,
connectionSettings: false,
retries: false,
cumulativeMaxConnections: false,
extensions_schema: false,
pool_timeout: false,
connection_lifetime: false,

View File

@ -1,4 +1,6 @@
import { TriggerOperation } from '@/components/Common/FilterQuery/state';
import globals from '@/Globals';
import { isEnvironmentSupportMultiTenantConnectionPooling } from '@/utils/proConsole';
import React from 'react';
import { DeepRequired } from 'ts-essentials';
import { DataSourcesAPI } from '../..';
@ -234,6 +236,8 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
isolation_level: false,
connectionSettings: true,
retries: false,
cumulativeMaxConnections:
isEnvironmentSupportMultiTenantConnectionPooling(globals),
extensions_schema: false,
pool_timeout: false,
connection_lifetime: false,

View File

@ -1,4 +1,5 @@
import React from 'react';
import { isEnvironmentSupportMultiTenantConnectionPooling } from '@/utils/proConsole';
import { DeepRequired } from 'ts-essentials';
import {
@ -756,6 +757,8 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
prepared_statements: true,
isolation_level: true,
connectionSettings: true,
cumulativeMaxConnections:
isEnvironmentSupportMultiTenantConnectionPooling(globals),
retries: true,
extensions_schema: true,
pool_timeout: true,

View File

@ -407,25 +407,31 @@ export type SupportedFeaturesType = {
statementTimeout: boolean;
tracking: boolean;
};
connectDbForm: {
enabled: boolean;
connectionParameters: boolean;
databaseURL: boolean;
environmentVariable: boolean;
read_replicas: {
create: boolean;
edit: boolean;
};
prepared_statements: boolean;
isolation_level: boolean;
connectionSettings: boolean;
retries: boolean;
extensions_schema: boolean;
pool_timeout: boolean;
connection_lifetime: boolean;
ssl_certificates: boolean;
namingConvention: boolean;
connectDbForm: ConnectDbForm;
};
type ConnectDbForm = {
enabled: boolean;
connectionParameters: boolean;
databaseURL: boolean;
environmentVariable: boolean;
read_replicas: {
create: boolean;
edit: boolean;
};
extensions_schema: boolean;
namingConvention: boolean;
} & DbConnectionSettings;
export type DbConnectionSettings = {
connectionSettings: boolean;
cumulativeMaxConnections: boolean;
retries: boolean;
pool_timeout: boolean;
connection_lifetime: boolean;
isolation_level: boolean;
prepared_statements: boolean;
ssl_certificates: boolean;
};
type Tables = ReduxState['tables'];

View File

@ -1057,6 +1057,7 @@ export interface SSLConfigOptions {
export interface ConnectionPoolSettings {
max_connections?: number;
cumulative_max_connections?: number;
idle_timeout?: number;
retries?: number;
pool_timeout?: number;

View File

@ -0,0 +1,31 @@
import { parseConsoleType } from '../envUtils';
describe('parseConsoleType', () => {
describe('when parseConsoleType is called with env var "oss"', () => {
it('returns oss', () => {
expect(parseConsoleType('oss')).toBe('oss');
});
});
describe('when parseConsoleType is called with env var "cloud"', () => {
it('returns cloud', () => {
expect(parseConsoleType('cloud')).toBe('cloud');
});
});
describe('when parseConsoleType is called with env var "pro"', () => {
it('returns pro', () => {
expect(parseConsoleType('pro')).toBe('pro');
});
});
describe('when parseConsoleType is called with env var "pro-lite"', () => {
it('returns pro-lite', () => {
expect(parseConsoleType('pro-lite')).toBe('pro-lite');
});
});
describe('when parseConsoleType is called with env var "invalid"', () => {
it('returns invalid', () => {
expect(() => parseConsoleType('invalid')).toThrow(
'Unmanaged console type "invalid"'
);
});
});
});

View File

@ -1,4 +1,5 @@
import {
isEnvironmentSupportMultiTenantConnectionPooling,
isMonitoringTabSupportedEnvironment,
isProConsole,
ProConsoleEnv,
@ -142,3 +143,79 @@ describe('isMonitoringTabSupportedEnvironment', () => {
});
});
});
describe('isEnvironmentSupportMultiTenantConnectionPooling', () => {
// Server Runtimes
describe('when consoleMode is server and consoleType is cloud (ie. Production cloud runtime)', () => {
it('returns true', () => {
const env: ProConsoleEnv = {
consoleMode: 'server',
consoleType: 'cloud',
};
expect(isEnvironmentSupportMultiTenantConnectionPooling(env)).toBe(true);
});
});
describe('when consoleMode is server and consoleType is pro (ie. Self hosted runtime)', () => {
it('returns true', () => {
const env: ProConsoleEnv = {
consoleMode: 'server',
consoleType: 'pro',
};
expect(isEnvironmentSupportMultiTenantConnectionPooling(env)).toBe(false);
});
});
describe('when consoleMode is server and consoleType is pro-lite', () => {
it('returns false', () => {
const env: ProConsoleEnv = {
consoleMode: 'server',
consoleType: 'pro-lite',
};
expect(isEnvironmentSupportMultiTenantConnectionPooling(env)).toBe(false);
});
});
describe('when consoleMode is server and consoleType is oss', () => {
it('returns false', () => {
const env: ProConsoleEnv = {
consoleMode: 'server',
consoleType: 'oss',
};
expect(isEnvironmentSupportMultiTenantConnectionPooling(env)).toBe(false);
});
});
// CLI runtimes
// Cloud and Self hosted EE (with LUX)
describe('when consoleMode is cli and pro is true', () => {
it('returns true', () => {
const env: ProConsoleEnv = {
consoleMode: 'cli',
pro: true,
consoleType: undefined,
};
expect(isEnvironmentSupportMultiTenantConnectionPooling(env)).toBe(true);
});
});
// OSS console CLI
describe('when consoleMode is cli and consoleType is oss', () => {
it('returns false', () => {
const env: ProConsoleEnv = {
consoleMode: 'cli',
consoleType: 'oss',
};
expect(isEnvironmentSupportMultiTenantConnectionPooling(env)).toBe(false);
});
});
// EE lite CLI mode
describe('when consoleMode is cli and pro is undefined', () => {
it('returns false', () => {
const env: ProConsoleEnv = {
consoleMode: 'cli',
};
expect(isEnvironmentSupportMultiTenantConnectionPooling(env)).toBe(false);
});
});
});

View File

@ -0,0 +1,14 @@
export type ConsoleType = 'oss' | 'cloud' | 'pro' | 'pro-lite';
export function parseConsoleType(envConsoleType: unknown): ConsoleType {
switch (envConsoleType) {
case 'oss':
case 'cloud':
case 'pro':
case 'pro-lite':
return envConsoleType;
default:
throw new Error(`Unmanaged console type "${envConsoleType}"`);
}
}

View File

@ -33,3 +33,16 @@ export const isMonitoringTabSupportedEnvironment = (env: ProConsoleEnv) => {
// there should not be any other console modes
throw new Error(`Invalid consoleMode: ${env.consoleMode}`);
};
export const isEnvironmentSupportMultiTenantConnectionPooling = (
env: ProConsoleEnv
) => {
if (env.consoleMode === 'server') return env.consoleType === 'cloud';
// cloud and current self hosted setup will have pro:true
// FIX ME : currently in CLI mode there is no way to differentiate cloud and pro mode
// This can be added once the CLI adds support of consoleType in the env vars provided to console.
else if (env.consoleMode === 'cli') return env.pro === true;
// there should not be any other console modes
throw new Error(`Invalid consoleMode: ${env.consoleMode}`);
};