Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
seancolsen committed Dec 18, 2023
1 parent 97b24e3 commit 7c2e5ed
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 101 deletions.
2 changes: 1 addition & 1 deletion mathesar_ui/src/stores/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class ConnectionsStore {
/** TODO_3311: remove this or utilize it */
readonly requestStatus: Writable<RequestStatus> = writable();

/** TODO_3311: make this a Map */
/** TODO_3311: make this a Map and sort by nickname */
readonly connections = writable<ConnectionModel[]>([]);

/** TODO_3311: make this derived */
Expand Down
182 changes: 103 additions & 79 deletions mathesar_ui/src/systems/connections/AddConnection.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,33 @@
import { assertExhaustive } from '@mathesar/utils/typeUtils';
import GeneralConnection from './GeneralConnection.svelte';
import {
defaultGeneralConnection,
generalConnections,
getConnectionReference,
getUsername,
pickDefaultGeneralConnection,
} from './generalConnections';
const credentialsStrategyOptions = ['reuse', 'new'] as const;
type CredentialsStrategy = (typeof credentialsStrategyOptions)[number];
const credentialsStrategyLabels: Record<CredentialsStrategy, string> = {
reuse: $_('reuse_credentials_from_known_connection'),
new: $_('enter_new_credentials'),
};
interface CredentialsStrategy {
/** We want to reuse the credentials from a known connection */
reuse: boolean;
/** The user can modify the strategy */
modifiable: boolean;
}
function getCredentialsStrategyLabel(s: CredentialsStrategy) {
return s.reuse
? $_('reuse_credentials_from_known_connection')
: $_('enter_new_credentials');
}
const userTypeOptions = ['existing', 'create'] as const;
type UserTypeChoice = (typeof userTypeOptions)[number];
const userTypeLabels: Record<UserTypeChoice, string> = {
existing: $_('use_existing_pg_user'),
create: $_('create_new_pg_user'),
};
type UserType = UserTypeChoice | 'forceExisting';
interface UserStrategy {
/** We want to create a new PostgreSQL user */
create: boolean;
/** The UI user can modify the strategy */
modifiable: boolean;
}
function getUserStrategyLabel(s: UserStrategy) {
return s.create ? $_('create_new_pg_user') : $_('use_existing_pg_user');
}
type InstallationSchema = SampleDataSchemaIdentifier | 'internal';
const installationSchemaOptions: InstallationSchema[] = [
Expand All @@ -78,51 +85,53 @@
export let onExit: () => void;
$: availableConnections = $generalConnections;
$: availableConnectionsToReuse = $generalConnections;
$: defaultConnectionToReuse =
pickDefaultGeneralConnection($generalConnections);
$: namedConnections = connectionsStore.connections;
$: connectionNames = new Set($namedConnections.map((c) => c.nickname));
// Common fields
$: credentialsStrategy = requiredField<CredentialsStrategy>(
!!$defaultGeneralConnection ? 'reuse' : 'new',
$: someUserDbConnectionsExist = $generalConnections.some(
(c) => c.type === 'user_database',
);
$: userType = requiredField<UserType>(
$defaultGeneralConnection ? 'existing' : 'forceExisting',
$: defaultDatabaseName = someUserDbConnectionsExist ? '' : 'mathesar';
$: credentialsStrategy = requiredField<CredentialsStrategy>(
defaultConnectionToReuse
? { reuse: true, modifiable: true }
: { reuse: false, modifiable: false },
);
$: databaseName = requiredField('');
$: databaseName = requiredField(defaultDatabaseName);
$: createDatabase = requiredField(true);
$: installationSchemas = requiredField<InstallationSchema[]>(['internal']);
$: connectionNickname = requiredField('', [uniqueWith(connectionNames)]);
// Fields for the 'fromKnownConnection' strategy
$: connectionToReuse = requiredField($defaultGeneralConnection);
// Fields for the 'fromScratch' strategy
$: host = requiredField('');
$: connectionToReuse = requiredField(defaultConnectionToReuse);
$: host = requiredField('localhost');
$: port = requiredField(5432);
$: existingUserName = requiredField('');
$: existingPassword = requiredField('');
// Fields for the 'withNewUser' strategy
$: bootstrapConnection = requiredField($defaultGeneralConnection);
$: availableBootstrapConnections = $generalConnections.filter(
(c) => c.connection.host === $host && c.connection.port === $port,
);
$: defaultBootstrapConnection = pickDefaultGeneralConnection(
availableBootstrapConnections,
);
$: userStrategy = requiredField<UserStrategy>(
defaultBootstrapConnection
? { create: false, modifiable: true }
: { create: false, modifiable: false },
);
$: bootstrapConnection = requiredField(defaultBootstrapConnection);
$: newUserName = requiredField('');
$: newPassword = requiredField('');
$: confirmPassword = requiredField('');
$: overallStrategy = (() => {
if ($credentialsStrategy === 'reuse') {
if ($credentialsStrategy.reuse) {
return 'fromKnownConnection' as const;
}
if ($credentialsStrategy === 'new' || $credentialsStrategy === 'forceNew') {
if ($userType === 'existing' || $userType === 'forceExisting') {
return 'fromScratch' as const;
}
if ($userType === 'create') {
return 'withNewUser' as const;
}
return assertExhaustive($userType);
if ($userStrategy.create) {
return 'withNewUser' as const;
}
return assertExhaustive($credentialsStrategy);
return 'fromScratch' as const;
})();
$: form = (() => {
const commonFields = {
Expand Down Expand Up @@ -167,18 +176,18 @@
}
})();
$: canCreateDb = $credentialsStrategy === 'reuse' || $userType === 'create';
$: canCreateDb = $credentialsStrategy.reuse || $userStrategy.create;
$: databaseNameHelp = canCreateDb
? undefined
: $_('this_database_must_exist_already');
$: databaseCreationUser = (() => {
switch (overallStrategy) {
case 'fromKnownConnection':
return getUsername($connectionToReuse);
return $connectionToReuse && getUsername($connectionToReuse);
case 'fromScratch':
return undefined;
case 'withNewUser':
return getUsername($bootstrapConnection);
return $bootstrapConnection && getUsername($bootstrapConnection);
default:
return assertExhaustive(overallStrategy);
}
Expand All @@ -198,15 +207,20 @@
nickname: $connectionNickname,
};
switch (overallStrategy) {
case 'fromKnownConnection':
case 'fromKnownConnection': {
const connection = $connectionToReuse;
if (!connection) {
throw new Error('Bug: $connectionToReuse is undefined');
}
return connectionsStore.createFromKnownConnection({
...commonProps,
credentials: {
connection: getConnectionReference($connectionToReuse),
connection: getConnectionReference(connection),
},
create_database: $createDatabase,
});
case 'fromScratch':
}
case 'fromScratch': {
return connectionsStore.createFromScratch({
...commonProps,
credentials: {
Expand All @@ -216,16 +230,22 @@
password: $existingPassword,
},
});
case 'withNewUser':
}
case 'withNewUser': {
const connection = $bootstrapConnection;
if (!connection) {
throw new Error('Bug: $bootstrapConnection is undefined');
}
return connectionsStore.createWithNewUser({
...commonProps,
credentials: {
user: $newUserName,
password: $newPassword,
create_user_via: getConnectionReference($bootstrapConnection),
create_user_via: getConnectionReference(connection),
},
create_database: $createDatabase,
});
}
default:
return assertExhaustive(overallStrategy);
}
Expand All @@ -244,23 +264,27 @@

<div class="db-connection-form">
<GridForm>
{#if $credentialsStrategy !== 'forceNew'}
{#if $credentialsStrategy.modifiable}
<div>{$_('database_server_credentials')}</div>
<div>
<RadioGroup
bind:value={$credentialsStrategy}
ariaLabel={$_('database_server_credentials')}
options={credentialsStrategyOptions}
getRadioLabel={(o) => credentialsStrategyLabels[o]}
options={[
{ reuse: true, modifiable: true },
{ reuse: false, modifiable: true },
]}
valuesAreEqual={(a, b) => a?.reuse === b?.reuse}
getRadioLabel={getCredentialsStrategyLabel}
/>
</div>
{/if}

{#if $credentialsStrategy === 'reuse'}
{#if $credentialsStrategy.reuse}
<GridFormLabelRow label={$_('known_connection')}>
<Select
bind:value={$connectionToReuse}
options={availableConnections}
options={availableConnectionsToReuse}
getLabel={(generalConnection) => ({
component: GeneralConnection,
props: { generalConnection },
Expand All @@ -270,7 +294,7 @@
{$_('the_credentials_will_be_copied_from_this_connection')}
</FieldHelp>
</GridFormLabelRow>
{:else if $credentialsStrategy === 'new'}
{:else}
<GridFormLabelRow label={$_('host_name')}>
<div class="host-and-port">
<Field field={host} />
Expand All @@ -282,38 +306,27 @@
</div>
</GridFormLabelRow>

{#if $userType !== 'forceExisting'}
{#if $userStrategy.modifiable}
<div>{$_('user_type')}</div>
<div>
<RadioGroup
bind:value={$userType}
bind:value={$userStrategy}
ariaLabel={$_('user_type')}
options={userTypeOptions}
getRadioLabel={(o) => userTypeLabels[o]}
options={[
{ create: false, modifiable: true },
{ create: true, modifiable: true },
]}
valuesAreEqual={(a, b) => a?.create === b?.create}
getRadioLabel={getUserStrategyLabel}
/>
</div>
{/if}

{#if $userType === 'existing'}
<GridFormLabelRow label={$_('user_name')}>
<Field
field={existingUserName}
help={$_('existing_pg_user_privileges_help')}
/>
</GridFormLabelRow>

<GridFormLabelRow label={$_('password')}>
<Field
field={existingPassword}
input={{ component: PasswordInput }}
help={$_('password_encryption_help')}
/>
</GridFormLabelRow>
{:else if $userType === 'create'}
{#if $userStrategy.create}
<GridFormLabelRow label={$_('create_user_via')}>
<Select
bind:value={$bootstrapConnection}
options={availableConnections}
options={availableBootstrapConnections}
getLabel={(generalConnection) => ({
component: GeneralConnection,
props: { generalConnection },
Expand All @@ -338,10 +351,21 @@
/>
</GridFormLabelRow>
{:else}
{assertExhaustive($userType)}
<GridFormLabelRow label={$_('user_name')}>
<Field
field={existingUserName}
help={$_('existing_pg_user_privileges_help')}
/>
</GridFormLabelRow>

<GridFormLabelRow label={$_('password')}>
<Field
field={existingPassword}
input={{ component: PasswordInput }}
help={$_('password_encryption_help')}
/>
</GridFormLabelRow>
{/if}
{:else}
{assertExhaustive($credentialsStrategy)}
{/if}

<GridFormDivider />
Expand Down
56 changes: 35 additions & 21 deletions mathesar_ui/src/systems/connections/generalConnections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,28 +51,42 @@ export const generalConnections: Readable<GeneralConnection[]> = derived(
],
);

/**
* The connection to pre-select when asking the user what connection they want
* to utilize for managing other connections (e.g. creating a new connection).
*/
export const defaultGeneralConnection = derived(
generalConnections,
(connections) => {
if (connections.length === 0) return undefined;
const internalConnection = connections.find(
(connection) => connection.type === 'internal_database',
);
if (internalConnection) return internalConnection;
const userDatabaseConnections = connections.filter(
isUserDatabaseConnection,
);
// /**
// * The connection to pre-select when asking the user what connection they want
// * to utilize for managing other connections (e.g. creating a new connection).
// */
// export const defaultGeneralConnection = derived(
// generalConnections,
// (connections) => {
// if (connections.length === 0) return undefined;
// const internalConnection = connections.find(
// (connection) => connection.type === 'internal_database',
// );
// if (internalConnection) return internalConnection;
// const userDatabaseConnections = connections.filter(
// isUserDatabaseConnection,
// );

// Return the connection with the highest ID
return userDatabaseConnections.reduce((a, b) =>
a.connection.id > b.connection.id ? a : b,
);
},
);
// // Return the connection with the highest ID
// return userDatabaseConnections.reduce((a, b) =>
// a.connection.id > b.connection.id ? a : b,
// );
// },
// );

export function pickDefaultGeneralConnection(connections: GeneralConnection[]) {
if (connections.length === 0) return undefined;
const internalConnection = connections.find(
(connection) => connection.type === 'internal_database',
);
if (internalConnection) return internalConnection;
const userDatabaseConnections = connections.filter(isUserDatabaseConnection);

// Return the connection with the highest ID
return userDatabaseConnections.reduce((a, b) =>
a.connection.id > b.connection.id ? a : b,
);
}

export function getUsername({ connection }: GeneralConnection): string {
return 'user' in connection ? connection.user : connection.username;
Expand Down

0 comments on commit 7c2e5ed

Please sign in to comment.