From 8225cc5f7699665a2374fe304bedec2edf956654 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Tue, 19 Dec 2023 11:19:07 -0500 Subject: [PATCH] WIP --- mathesar_ui/src/AppTypes.ts | 2 +- .../common/utils/typeUtils.ts | 10 ++ .../breadcrumb/DatabaseSelector.svelte | 10 +- .../pages/connections/ConnectionsPage.svelte | 37 ++--- .../src/routes/AuthenticatedRoutes.svelte | 29 ++-- mathesar_ui/src/routes/DatabaseRoute.svelte | 2 +- mathesar_ui/src/stores/databases.ts | 155 ++++++------------ .../systems/connections/AddConnection.svelte | 3 +- .../connections/DeleteConnectionModal.svelte | 13 +- .../systems/connections/generalConnections.ts | 21 +-- mathesar_ui/src/utils/iterUtils.ts | 19 +++ 11 files changed, 136 insertions(+), 165 deletions(-) create mode 100644 mathesar_ui/src/utils/iterUtils.ts diff --git a/mathesar_ui/src/AppTypes.ts b/mathesar_ui/src/AppTypes.ts index 873006d747..168d51b58d 100644 --- a/mathesar_ui/src/AppTypes.ts +++ b/mathesar_ui/src/AppTypes.ts @@ -1,6 +1,6 @@ import type { TreeItem } from '@mathesar-component-library/types'; -/** @deprecated Use either Connection or ConnectionModel interface instead */ +/** @deprecated in favor of Connection */ export interface Database { id: number; nickname: string; diff --git a/mathesar_ui/src/component-library/common/utils/typeUtils.ts b/mathesar_ui/src/component-library/common/utils/typeUtils.ts index 882d01b29d..a0f13cd1ee 100644 --- a/mathesar_ui/src/component-library/common/utils/typeUtils.ts +++ b/mathesar_ui/src/component-library/common/utils/typeUtils.ts @@ -46,6 +46,16 @@ export function requiredNonNullable( return defaultValue; } +/** + * Used to make sure that a value is defined before using it. + */ +export function defined( + v: T | undefined, + f: (v: T) => U | undefined, +): U | undefined { + return v === undefined ? undefined : f(v); +} + /** * From https://stackoverflow.com/a/51365037/895563 */ diff --git a/mathesar_ui/src/components/breadcrumb/DatabaseSelector.svelte b/mathesar_ui/src/components/breadcrumb/DatabaseSelector.svelte index 99f5a667ee..8dffc34b7d 100644 --- a/mathesar_ui/src/components/breadcrumb/DatabaseSelector.svelte +++ b/mathesar_ui/src/components/breadcrumb/DatabaseSelector.svelte @@ -9,7 +9,7 @@ const { connections, currentConnectionId } = connectionsStore; - function makeBreadcrumbSelectorItem( + function makeBreadcrumbSelectorEntry( connection: Connection, ): BreadcrumbSelectorEntry { return { @@ -20,12 +20,14 @@ isActive: () => connection.id === $currentConnectionId, }; } + + $: breadcrumbEntries = [...$connections.values()].map( + makeBreadcrumbSelectorEntry, + ); { - if (query) { - const sanitizedQuery = query.trim().toLowerCase(); - return isMatch(connection, sanitizedQuery); - } - return true; - }); + function filterConnections(allConnections: Connection[], query: string) { + if (!query) return allConnections; + const sanitizedQuery = query.trim().toLowerCase(); + const match = (t: string) => t.toLowerCase().includes(sanitizedQuery); + return allConnections.filter((c) => match(c.nickname) || match(c.database)); } function handleClearFilterQuery() { filterQuery = ''; } - $: filteredConnections = filterConnections($connections ?? [], filterQuery); + $: filteredConnections = filterConnections( + [...$connections.values()], + filterQuery, + ); {makeSimplePageTitle($_('connections'))} - +
{$_('database_connections')} - {#if $connections.length}({$connections.length}){/if} + {#if $connections.size}({$connections.size}){/if}
{#if $connectionsRequestStatus.state === 'failure'} - {:else if $connections.length === 0} + {:else if $connections.size === 0} {:else} import { Route } from 'tinro'; - import { connectionsStore } from '@mathesar/stores/databases'; - import { getUserProfileStoreFromContext } from '@mathesar/stores/userProfile'; - import { getDatabasePageUrl, CONNECTIONS_URL } from '@mathesar/routes/urls'; + + import AppendBreadcrumb from '@mathesar/components/breadcrumb/AppendBreadcrumb.svelte'; import WelcomePage from '@mathesar/pages/WelcomePage.svelte'; import ConnectionsPage from '@mathesar/pages/connections/ConnectionsPage.svelte'; - import AppendBreadcrumb from '@mathesar/components/breadcrumb/AppendBreadcrumb.svelte'; + import { CONNECTIONS_URL, getDatabasePageUrl } from '@mathesar/routes/urls'; + import { connectionsStore } from '@mathesar/stores/databases'; + import { getUserProfileStoreFromContext } from '@mathesar/stores/userProfile'; + import { mapExactlyOne } from '@mathesar/utils/iterUtils'; + import AdminRoute from './AdminRoute.svelte'; import DatabaseRoute from './DatabaseRoute.svelte'; import UserProfileRoute from './UserProfileRoute.svelte'; - import AdminRoute from './AdminRoute.svelte'; const userProfileStore = getUserProfileStoreFromContext(); $: userProfile = $userProfileStore; $: ({ connections } = connectionsStore); - $: rootPathRedirectUrl = (() => { - const numberOfConnections = $connections?.length ?? 0; - if (numberOfConnections === 0) { - // There is no redirection when `redirect` is `undefined`. - return undefined; - } - if (numberOfConnections > 1) { - return CONNECTIONS_URL; - } - const firstConnection = $connections[0]; - return getDatabasePageUrl(firstConnection.id); - })(); + $: rootPathRedirectUrl = mapExactlyOne($connections, { + whenZero: undefined, + whenOne: ([id]) => getDatabasePageUrl(id), + whenMany: CONNECTIONS_URL, + }); diff --git a/mathesar_ui/src/routes/DatabaseRoute.svelte b/mathesar_ui/src/routes/DatabaseRoute.svelte index 16e4ead420..c7a82945a2 100644 --- a/mathesar_ui/src/routes/DatabaseRoute.svelte +++ b/mathesar_ui/src/routes/DatabaseRoute.svelte @@ -14,7 +14,7 @@ $: connectionsStore.setCurrentConnectionId(connectionId); $: ({ connections } = connectionsStore); - $: connection = $connections?.find((c) => c.id === connectionId); + $: connection = $connections.get(connectionId); function handleUnmount() { connectionsStore.clearCurrentConnectionId(); diff --git a/mathesar_ui/src/stores/databases.ts b/mathesar_ui/src/stores/databases.ts index 8bbb018f14..f586fb302b 100644 --- a/mathesar_ui/src/stores/databases.ts +++ b/mathesar_ui/src/stores/databases.ts @@ -1,5 +1,3 @@ -/* eslint-disable max-classes-per-file */ - import { derived, get, @@ -8,6 +6,11 @@ import { type Writable, } from 'svelte/store'; +import { + ImmutableMap, + WritableMap, + defined, +} from '@mathesar-component-library'; import connectionsApi, { type Connection, type CreateFromKnownConnectionProps, @@ -18,77 +21,61 @@ import connectionsApi, { import type { RequestStatus } from '@mathesar/api/utils/requestUtils'; import { preloadCommonData } from '@mathesar/utils/preloadData'; import type { MakeWritablePropertiesReadable } from '@mathesar/utils/typeUtils'; +import { some } from 'iter-tools'; const commonData = preloadCommonData(); -export class ConnectionModel { - readonly id: Connection['id']; - - readonly nickname: Connection['nickname']; - - readonly database: Connection['database']; - - readonly username: Connection['username']; - - readonly host: Connection['host']; - - readonly port: Connection['port']; - - constructor(connectionDetails: Connection) { - this.id = connectionDetails.id; - this.nickname = connectionDetails.nickname; - this.database = connectionDetails.database; - this.username = connectionDetails.username; - this.host = connectionDetails.host; - this.port = connectionDetails.port; - } - - getConnectionJson(): Connection { - return { - id: this.id, - nickname: this.nickname, - database: this.database, - username: this.username, - host: this.host, - port: this.port, - }; - } +function sortConnections(c: Iterable): Connection[] { + return [...c].sort((a, b) => a.nickname.localeCompare(b.nickname)); +} - with(connectionDetails: Partial): ConnectionModel { - return new ConnectionModel({ - ...this.getConnectionJson(), - ...connectionDetails, - }); - } +/** + * @returns true if the given connection is the only one that points to the same + * database among all the supplied connections. Connections with the same ids + * will not be compared. + */ +export function connectionHasUniqueDatabaseReference( + connection: Connection, + allConnections: Iterable, +): boolean { + return !some( + (c) => + c.id !== connection.id && + c.host === connection.host && + c.port === connection.port && + c.database === connection.database, + allConnections, + ); } class ConnectionsStore { /** TODO_3311: remove this or utilize it */ readonly requestStatus: Writable = writable(); - /** TODO_3311: make this a Map and sort by nickname */ - readonly connections = writable([]); + private readonly unsortedConnections = new WritableMap< + Connection['id'], + Connection + >(); - /** TODO_3311: make this derived */ - readonly count = writable(0); + readonly connections: Readable>; readonly currentConnectionId = writable(); - readonly currentConnection: Readable; + readonly currentConnection: Readable; constructor() { - this.requestStatus.set({ state: 'success' }); - this.connections.set( - commonData?.connections.map( - (connection) => new ConnectionModel(connection), - ) ?? [], + this.unsortedConnections.reconstruct( + (commonData?.connections ?? []).map((c) => [c.id, c]), + ); + this.connections = derived( + this.unsortedConnections, + (uc) => + new ImmutableMap(sortConnections(uc.values()).map((c) => [c.id, c])), ); - this.count.set(commonData?.connections.length ?? 0); this.currentConnectionId.set(commonData?.current_connection ?? undefined); this.currentConnection = derived( [this.connections, this.currentConnectionId], - ([connections, currentConnectionId]) => - connections.find((c) => c.id === currentConnectionId), + ([connections, id]) => defined(id, (v) => connections.get(v)), ); } @@ -101,10 +88,7 @@ class ConnectionsStore { } private addConnection(connection: Connection) { - this.connections.update((connections) => [ - ...connections, - new ConnectionModel(connection), - ]); + this.unsortedConnections.set(connection.id, connection); } async createFromKnownConnection(props: CreateFromKnownConnectionProps) { @@ -125,56 +109,25 @@ class ConnectionsStore { return connection; } - // async setupConnection(props: SetupConnectionProps) { - // const connection = await connectionsApi.setup(props); - // this.connections.update((connections) => [ - // ...connections, - // new ConnectionModel(connection), - // ]); - // return connection; - // } - async updateConnection( - connectionId: Connection['id'], + id: Connection['id'], properties: Partial, ): Promise { - const updatedConnection = await connectionsApi.update( - connectionId, - properties, - ); - const newConnectionModel = new ConnectionModel(updatedConnection); - this.connections.update((connections) => - connections.map((connection) => { - if (connection.id === connectionId) { - return newConnectionModel; - } - return connection; - }), - ); - return newConnectionModel; + const connection = await connectionsApi.update(id, properties); + this.unsortedConnections.set(id, connection); + return connection; } - async deleteConnection( - connectionId: Connection['id'], - deleteMathesarSchemas = false, - ) { + async deleteConnection(id: Connection['id'], deleteMathesarSchemas = false) { const connections = get(this.connections); - const connectionToDelete = connections.find( - (conn) => conn.id === connectionId, - ); - const otherConnectionsUseSameDb = !!connections.find( - (conn) => - conn.id !== connectionId && - conn.database === connectionToDelete?.database, - ); - const mathesarSchemasShouldBeDeleted = - !otherConnectionsUseSameDb && deleteMathesarSchemas; - // TODO_3311: do this first so that if there's an error we don't clear the - // UI state - await connectionsApi.delete(connectionId, mathesarSchemasShouldBeDeleted); - this.connections.update((conns) => - conns.filter((conn) => conn.id !== connectionId), + const connection = connections.get(id); + if (!connection) return; + const databaseIsUnique = connectionHasUniqueDatabaseReference( + connection, + connections.values(), ); + await connectionsApi.delete(id, deleteMathesarSchemas && databaseIsUnique); + this.unsortedConnections.delete(id); } } @@ -183,5 +136,3 @@ export const connectionsStore: MakeWritablePropertiesReadable /** @deprecated Use connectionsStore.currentConnection instead */ export const currentDatabase = connectionsStore.currentConnection; - -/* eslint-enable max-classes-per-file */ diff --git a/mathesar_ui/src/systems/connections/AddConnection.svelte b/mathesar_ui/src/systems/connections/AddConnection.svelte index 710a700494..f63385403a 100644 --- a/mathesar_ui/src/systems/connections/AddConnection.svelte +++ b/mathesar_ui/src/systems/connections/AddConnection.svelte @@ -1,5 +1,6 @@