From f253548404d9e3259b8f4bb01cf5ca4861ded026 Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Mon, 27 Nov 2023 10:45:03 -0800 Subject: [PATCH 1/7] Save database info --- .dockerignore | 3 +- graphql-server/.gitignore | 1 + graphql-server/schema.gql | 15 ++++- .../src/dataSources/dataSource.service.ts | 2 +- .../src/databases/database.resolver.ts | 67 ++++++++++++++++--- .../AddConnectionOptions.tsx | 6 +- .../ConfigurationPage/queries.ts | 13 +++- .../pageComponents/HomePage/index.tsx | 12 ++-- web/gen/graphql-types.tsx | 66 ++++++++++++------ 9 files changed, 143 insertions(+), 42 deletions(-) diff --git a/.dockerignore b/.dockerignore index 413a0310..82e3e6f7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -26,4 +26,5 @@ images/ **/*.test.tsx **/.yarn/cache -**/.yarn/install-state.gz \ No newline at end of file +**/.yarn/install-state.gz +**/store.json \ No newline at end of file diff --git a/graphql-server/.gitignore b/graphql-server/.gitignore index 8b50d0d1..ed08a45c 100644 --- a/graphql-server/.gitignore +++ b/graphql-server/.gitignore @@ -45,3 +45,4 @@ lerna-debug.log* !.yarn/sdks !.yarn/versions node_modules +store.json diff --git a/graphql-server/schema.gql b/graphql-server/schema.gql index 2ed8c8c8..89b8d96d 100644 --- a/graphql-server/schema.gql +++ b/graphql-server/schema.gql @@ -99,6 +99,17 @@ type DoltDatabaseDetails { hideDoltFeatures: Boolean! } +type StoredState { + connectionUrl: String! + useSSL: Boolean + hideDoltFeatures: Boolean +} + +type DatabaseState { + hasEnv: Boolean! + storedState: StoredState +} + type DiffStat { rowsUnmodified: Float! rowsAdded: Float! @@ -266,7 +277,7 @@ type Query { defaultBranch(databaseName: String!): Branch commits(offset: Int, databaseName: String!, refName: String, afterCommitId: String, twoDot: Boolean, excludingCommitsFromRefName: String): CommitList! currentDatabase: String - hasDatabaseEnv: Boolean! + databaseState: DatabaseState! databases: [String!]! doltDatabaseDetails: DoltDatabaseDetails! diffStat(databaseName: String!, fromRefName: String!, toRefName: String!, refName: String, type: CommitDiffType, tableName: String): DiffStat! @@ -317,7 +328,7 @@ enum DiffRowType { type Mutation { createBranch(databaseName: String!, newBranchName: String!, fromRefName: String!): Branch! deleteBranch(databaseName: String!, branchName: String!): Boolean! - addDatabaseConnection(url: String, useEnv: Boolean, hideDoltFeatures: Boolean, useSSL: Boolean): String + addDatabaseConnection(url: String, useEnv: Boolean, hideDoltFeatures: Boolean, useSSL: Boolean, shouldStore: Boolean): String createDatabase(databaseName: String!): Boolean! resetDatabase: Boolean! loadDataFile(tableName: String!, refName: String!, databaseName: String!, importOp: ImportOperation!, fileType: FileType!, file: Upload!, modifier: LoadDataModifier): Boolean! diff --git a/graphql-server/src/dataSources/dataSource.service.ts b/graphql-server/src/dataSources/dataSource.service.ts index 6ac3b585..bada23f0 100644 --- a/graphql-server/src/dataSources/dataSource.service.ts +++ b/graphql-server/src/dataSources/dataSource.service.ts @@ -6,7 +6,7 @@ import { RawRows } from "../utils/commonTypes"; export const dbNotFoundErr = "Database connection not found"; export type ParQuery = (q: string, p?: any[] | undefined) => Promise; -class WorkbenchConfig { +export class WorkbenchConfig { hideDoltFeatures: boolean; connectionUrl: string; diff --git a/graphql-server/src/databases/database.resolver.ts b/graphql-server/src/databases/database.resolver.ts index 664d6ba0..a251c9a1 100644 --- a/graphql-server/src/databases/database.resolver.ts +++ b/graphql-server/src/databases/database.resolver.ts @@ -8,7 +8,11 @@ import { Query, Resolver, } from "@nestjs/graphql"; -import { DataSourceService } from "../dataSources/dataSource.service"; +import { readFileSync, writeFileSync } from "fs"; +import { + DataSourceService, + WorkbenchConfig, +} from "../dataSources/dataSource.service"; import { DBArgs } from "../utils/commonTypes"; @ArgsType() @@ -24,6 +28,9 @@ class AddDatabaseConnectionArgs { @Field({ nullable: true }) useSSL?: boolean; + + @Field({ nullable: true }) + shouldStore?: boolean; } @ObjectType() @@ -35,6 +42,27 @@ class DoltDatabaseDetails { hideDoltFeatures: boolean; } +@ObjectType() +class StoredState { + @Field() + connectionUrl: string; + + @Field({ nullable: true }) + useSSL?: boolean; + + @Field({ nullable: true }) + hideDoltFeatures?: boolean; +} + +@ObjectType() +class DatabaseState { + @Field() + hasEnv: boolean; + + @Field(_type => StoredState, { nullable: true }) + storedState?: StoredState; +} + @Resolver() export class DatabaseResolver { constructor( @@ -53,9 +81,24 @@ export class DatabaseResolver { } } - @Query(_returns => Boolean) - async hasDatabaseEnv(): Promise { - return !!this.configService.get("DATABASE_URL"); + @Query(_returns => DatabaseState) + async databaseState(): Promise { + const hasEnv = !!this.configService.get("DATABASE_URL"); + const file = readFileSync("store.json", { encoding: "utf8", flag: "r" }); + if (!file) { + return { hasEnv }; + } + try { + const parsed = JSON.parse(file); + console.log(parsed); + return { + hasEnv, + storedState: parsed, + }; + } catch (err) { + console.error("Error parsing store json:", err); + return { hasEnv }; + } } @Query(_returns => [String]) @@ -93,26 +136,34 @@ export class DatabaseResolver { async addDatabaseConnection( @Args() args: AddDatabaseConnectionArgs, ): Promise { + let workbenchConfig: WorkbenchConfig; if (args.useEnv) { const url = this.configService.get("DATABASE_URL"); if (!url) throw new Error("DATABASE_URL not found in env"); const hideDoltFeatures = this.configService.get("HIDE_DOLT_FEATURES"); const useSSL = this.configService.get("USE_SSL"); - await this.dss.addDS({ + workbenchConfig = { connectionUrl: url, hideDoltFeatures: !!hideDoltFeatures && hideDoltFeatures === "true", useSSL: useSSL !== undefined ? useSSL === "true" : true, - }); + }; + await this.dss.addDS(workbenchConfig); } else if (args.url) { - await this.dss.addDS({ + workbenchConfig = { connectionUrl: args.url, hideDoltFeatures: !!args.hideDoltFeatures, useSSL: !!args.useSSL, - }); + }; + await this.dss.addDS(workbenchConfig); } else { throw new Error("database url not provided"); } + if (args.shouldStore) { + const stringified = JSON.stringify(workbenchConfig); + writeFileSync("store.json", stringified); + } + const db = await this.currentDatabase(); if (!db) return undefined; return db; diff --git a/web/components/pageComponents/ConfigurationPage/AddConnectionOptions.tsx b/web/components/pageComponents/ConfigurationPage/AddConnectionOptions.tsx index aa97abf3..8edf6d3b 100644 --- a/web/components/pageComponents/ConfigurationPage/AddConnectionOptions.tsx +++ b/web/components/pageComponents/ConfigurationPage/AddConnectionOptions.tsx @@ -2,7 +2,7 @@ import ErrorMsg from "@components/ErrorMsg"; import QueryHandler from "@components/util/QueryHandler"; import { useAddDatabaseConnectionMutation, - useHasDatabaseEnvQuery, + useDatabaseStateQuery, } from "@gen/graphql-types"; import { maybeDatabase } from "@lib/urls"; import { useRouter } from "next/router"; @@ -60,11 +60,11 @@ function Inner(props: InnerProps) { } export default function AddConnectionOptions() { - const res = useHasDatabaseEnvQuery(); + const res = useDatabaseStateQuery(); return ( } + render={data => } /> ); } diff --git a/web/components/pageComponents/ConfigurationPage/queries.ts b/web/components/pageComponents/ConfigurationPage/queries.ts index 5a3cb121..93236774 100644 --- a/web/components/pageComponents/ConfigurationPage/queries.ts +++ b/web/components/pageComponents/ConfigurationPage/queries.ts @@ -6,18 +6,27 @@ export const ADD_DATABASE_CONNECTION = gql` $useEnv: Boolean $hideDoltFeatures: Boolean $useSSL: Boolean + $shouldStore: Boolean ) { addDatabaseConnection( url: $url useEnv: $useEnv hideDoltFeatures: $hideDoltFeatures useSSL: $useSSL + shouldStore: $shouldStore ) } `; export const HAS_DB_ENV = gql` - query HasDatabaseEnv { - hasDatabaseEnv + query DatabaseState { + databaseState { + hasEnv + storedState { + connectionUrl + useSSL + hideDoltFeatures + } + } } `; diff --git a/web/components/pageComponents/HomePage/index.tsx b/web/components/pageComponents/HomePage/index.tsx index 6e470abc..3bcae1f4 100644 --- a/web/components/pageComponents/HomePage/index.tsx +++ b/web/components/pageComponents/HomePage/index.tsx @@ -1,7 +1,7 @@ import Loader from "@components/Loader"; import { useAddDatabaseConnectionMutation, - useHasDatabaseEnvQuery, + useDatabaseStateQuery, } from "@gen/graphql-types"; import useEffectAsync from "@hooks/useEffectAsync"; import { maybeDatabase } from "@lib/urls"; @@ -11,13 +11,17 @@ import ConfigurationPage from "../ConfigurationPage"; export default function HomePage() { const router = useRouter(); - const res = useHasDatabaseEnvQuery(); + const res = useDatabaseStateQuery(); const [addDb] = useAddDatabaseConnectionMutation(); const [loading, setLoading] = useState(true); useEffectAsync(async () => { + if (res.error) { + setLoading(false); + return; + } if (!res.data) return; - if (!res.data.hasDatabaseEnv) { + if (!res.data.databaseState.hasEnv) { setLoading(false); return; } @@ -34,7 +38,7 @@ export default function HomePage() { // Handled by res.error setLoading(false); } - }, [res.data?.hasDatabaseEnv]); + }, [res.data?.databaseState.hasEnv, res.error]); if (loading) return ; return ; diff --git a/web/gen/graphql-types.tsx b/web/gen/graphql-types.tsx index 4cf8b936..8b795484 100644 --- a/web/gen/graphql-types.tsx +++ b/web/gen/graphql-types.tsx @@ -88,6 +88,12 @@ export type CommitList = { nextOffset?: Maybe; }; +export type DatabaseState = { + __typename?: 'DatabaseState'; + hasEnv: Scalars['Boolean']['output']; + storedState?: Maybe; +}; + export enum DiffRowType { Added = 'Added', All = 'All', @@ -207,6 +213,7 @@ export type Mutation = { export type MutationAddDatabaseConnectionArgs = { hideDoltFeatures?: InputMaybe; + shouldStore?: InputMaybe; url?: InputMaybe; useEnv?: InputMaybe; useSSL?: InputMaybe; @@ -309,6 +316,7 @@ export type Query = { branches: BranchNamesList; commits: CommitList; currentDatabase?: Maybe; + databaseState: DatabaseState; databases: Array; defaultBranch?: Maybe; diffStat: DiffStat; @@ -318,7 +326,6 @@ export type Query = { doltDatabaseDetails: DoltDatabaseDetails; doltProcedures: Array; doltSchemas: Array; - hasDatabaseEnv: Scalars['Boolean']['output']; pullWithDetails: PullWithDetails; rowDiffs: RowDiffList; rows: RowList; @@ -584,6 +591,13 @@ export type Status = { tableName: Scalars['String']['output']; }; +export type StoredState = { + __typename?: 'StoredState'; + connectionUrl: Scalars['String']['output']; + hideDoltFeatures?: Maybe; + useSSL?: Maybe; +}; + export type Table = { __typename?: 'Table'; _id: Scalars['ID']['output']; @@ -843,15 +857,16 @@ export type AddDatabaseConnectionMutationVariables = Exact<{ useEnv?: InputMaybe; hideDoltFeatures?: InputMaybe; useSSL?: InputMaybe; + shouldStore?: InputMaybe; }>; export type AddDatabaseConnectionMutation = { __typename?: 'Mutation', addDatabaseConnection?: string | null }; -export type HasDatabaseEnvQueryVariables = Exact<{ [key: string]: never; }>; +export type DatabaseStateQueryVariables = Exact<{ [key: string]: never; }>; -export type HasDatabaseEnvQuery = { __typename?: 'Query', hasDatabaseEnv: boolean }; +export type DatabaseStateQuery = { __typename?: 'Query', databaseState: { __typename?: 'DatabaseState', hasEnv: boolean, storedState?: { __typename?: 'StoredState', connectionUrl: string, useSSL?: boolean | null, hideDoltFeatures?: boolean | null } | null } }; export type BranchFragment = { __typename?: 'Branch', _id: string, branchName: string, databaseName: string, lastUpdated: any, lastCommitter: string }; @@ -2315,12 +2330,13 @@ export type RowsForViewsLazyQueryHookResult = ReturnType; export type RowsForViewsQueryResult = Apollo.QueryResult; export const AddDatabaseConnectionDocument = gql` - mutation AddDatabaseConnection($url: String, $useEnv: Boolean, $hideDoltFeatures: Boolean, $useSSL: Boolean) { + mutation AddDatabaseConnection($url: String, $useEnv: Boolean, $hideDoltFeatures: Boolean, $useSSL: Boolean, $shouldStore: Boolean) { addDatabaseConnection( url: $url useEnv: $useEnv hideDoltFeatures: $hideDoltFeatures useSSL: $useSSL + shouldStore: $shouldStore ) } `; @@ -2343,6 +2359,7 @@ export type AddDatabaseConnectionMutationFn = Apollo.MutationFunction; export type AddDatabaseConnectionMutationResult = Apollo.MutationResult; export type AddDatabaseConnectionMutationOptions = Apollo.BaseMutationOptions; -export const HasDatabaseEnvDocument = gql` - query HasDatabaseEnv { - hasDatabaseEnv +export const DatabaseStateDocument = gql` + query DatabaseState { + databaseState { + hasEnv + storedState { + connectionUrl + useSSL + hideDoltFeatures + } + } } `; /** - * __useHasDatabaseEnvQuery__ + * __useDatabaseStateQuery__ * - * To run a query within a React component, call `useHasDatabaseEnvQuery` and pass it any options that fit your needs. - * When your component renders, `useHasDatabaseEnvQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `useDatabaseStateQuery` and pass it any options that fit your needs. + * When your component renders, `useDatabaseStateQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example - * const { data, loading, error } = useHasDatabaseEnvQuery({ + * const { data, loading, error } = useDatabaseStateQuery({ * variables: { * }, * }); */ -export function useHasDatabaseEnvQuery(baseOptions?: Apollo.QueryHookOptions) { +export function useDatabaseStateQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(HasDatabaseEnvDocument, options); + return Apollo.useQuery(DatabaseStateDocument, options); } -export function useHasDatabaseEnvLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { +export function useDatabaseStateLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(HasDatabaseEnvDocument, options); + return Apollo.useLazyQuery(DatabaseStateDocument, options); } -export function useHasDatabaseEnvSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { +export function useDatabaseStateSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useSuspenseQuery(HasDatabaseEnvDocument, options); + return Apollo.useSuspenseQuery(DatabaseStateDocument, options); } -export type HasDatabaseEnvQueryHookResult = ReturnType; -export type HasDatabaseEnvLazyQueryHookResult = ReturnType; -export type HasDatabaseEnvSuspenseQueryHookResult = ReturnType; -export type HasDatabaseEnvQueryResult = Apollo.QueryResult; +export type DatabaseStateQueryHookResult = ReturnType; +export type DatabaseStateLazyQueryHookResult = ReturnType; +export type DatabaseStateSuspenseQueryHookResult = ReturnType; +export type DatabaseStateQueryResult = Apollo.QueryResult; export const BranchListDocument = gql` query BranchList($databaseName: String!, $sortBy: SortBranchesBy) { branches(databaseName: $databaseName, sortBy: $sortBy) { From ed0c80b7c37a412e9f93916194c1a41b1ce7a490 Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Mon, 27 Nov 2023 11:35:29 -0800 Subject: [PATCH 2/7] Connect to existing db --- graphql-server/schema.gql | 2 +- .../src/databases/database.resolver.ts | 36 ++++++--------- .../AddConnectionOptions.tsx | 45 +++++++++++++++---- .../ConfigurationPage/queries.ts | 15 ++++--- .../ConfigurationPage/useConfig.ts | 28 ++---------- web/gen/graphql-types.tsx | 29 ++++++------ 6 files changed, 77 insertions(+), 78 deletions(-) diff --git a/graphql-server/schema.gql b/graphql-server/schema.gql index 89b8d96d..4bd1ab8d 100644 --- a/graphql-server/schema.gql +++ b/graphql-server/schema.gql @@ -328,7 +328,7 @@ enum DiffRowType { type Mutation { createBranch(databaseName: String!, newBranchName: String!, fromRefName: String!): Branch! deleteBranch(databaseName: String!, branchName: String!): Boolean! - addDatabaseConnection(url: String, useEnv: Boolean, hideDoltFeatures: Boolean, useSSL: Boolean, shouldStore: Boolean): String + addDatabaseConnection(connectionUrl: String, useEnv: Boolean, hideDoltFeatures: Boolean, useSSL: Boolean): String createDatabase(databaseName: String!): Boolean! resetDatabase: Boolean! loadDataFile(tableName: String!, refName: String!, databaseName: String!, importOp: ImportOperation!, fileType: FileType!, file: Upload!, modifier: LoadDataModifier): Boolean! diff --git a/graphql-server/src/databases/database.resolver.ts b/graphql-server/src/databases/database.resolver.ts index a251c9a1..a62c5aee 100644 --- a/graphql-server/src/databases/database.resolver.ts +++ b/graphql-server/src/databases/database.resolver.ts @@ -9,16 +9,13 @@ import { Resolver, } from "@nestjs/graphql"; import { readFileSync, writeFileSync } from "fs"; -import { - DataSourceService, - WorkbenchConfig, -} from "../dataSources/dataSource.service"; +import { DataSourceService } from "../dataSources/dataSource.service"; import { DBArgs } from "../utils/commonTypes"; @ArgsType() class AddDatabaseConnectionArgs { @Field({ nullable: true }) - url?: string; + connectionUrl?: string; @Field({ nullable: true }) useEnv?: boolean; @@ -28,9 +25,6 @@ class AddDatabaseConnectionArgs { @Field({ nullable: true }) useSSL?: boolean; - - @Field({ nullable: true }) - shouldStore?: boolean; } @ObjectType() @@ -84,19 +78,18 @@ export class DatabaseResolver { @Query(_returns => DatabaseState) async databaseState(): Promise { const hasEnv = !!this.configService.get("DATABASE_URL"); - const file = readFileSync("store.json", { encoding: "utf8", flag: "r" }); - if (!file) { - return { hasEnv }; - } try { + const file = readFileSync("store.json", { encoding: "utf8", flag: "r" }); + if (!file) { + return { hasEnv }; + } const parsed = JSON.parse(file); - console.log(parsed); return { hasEnv, storedState: parsed, }; } catch (err) { - console.error("Error parsing store json:", err); + console.error("Error reading store.json:", err); return { hasEnv }; } } @@ -136,32 +129,29 @@ export class DatabaseResolver { async addDatabaseConnection( @Args() args: AddDatabaseConnectionArgs, ): Promise { - let workbenchConfig: WorkbenchConfig; if (args.useEnv) { const url = this.configService.get("DATABASE_URL"); if (!url) throw new Error("DATABASE_URL not found in env"); const hideDoltFeatures = this.configService.get("HIDE_DOLT_FEATURES"); const useSSL = this.configService.get("USE_SSL"); - workbenchConfig = { + const workbenchConfig = { connectionUrl: url, hideDoltFeatures: !!hideDoltFeatures && hideDoltFeatures === "true", useSSL: useSSL !== undefined ? useSSL === "true" : true, }; await this.dss.addDS(workbenchConfig); - } else if (args.url) { - workbenchConfig = { - connectionUrl: args.url, + } else if (args.connectionUrl) { + const workbenchConfig = { + connectionUrl: args.connectionUrl, hideDoltFeatures: !!args.hideDoltFeatures, useSSL: !!args.useSSL, }; await this.dss.addDS(workbenchConfig); - } else { - throw new Error("database url not provided"); - } - if (args.shouldStore) { const stringified = JSON.stringify(workbenchConfig); writeFileSync("store.json", stringified); + } else { + throw new Error("database url not provided"); } const db = await this.currentDatabase(); diff --git a/web/components/pageComponents/ConfigurationPage/AddConnectionOptions.tsx b/web/components/pageComponents/ConfigurationPage/AddConnectionOptions.tsx index 8edf6d3b..e8098426 100644 --- a/web/components/pageComponents/ConfigurationPage/AddConnectionOptions.tsx +++ b/web/components/pageComponents/ConfigurationPage/AddConnectionOptions.tsx @@ -1,6 +1,8 @@ import ErrorMsg from "@components/ErrorMsg"; import QueryHandler from "@components/util/QueryHandler"; import { + AddDatabaseConnectionMutationVariables, + StoredStateFragment, useAddDatabaseConnectionMutation, useDatabaseStateQuery, } from "@gen/graphql-types"; @@ -12,17 +14,23 @@ import css from "./index.module.css"; type InnerProps = { hasDatabaseEnv: boolean; + storedState?: StoredStateFragment; }; -function Inner(props: InnerProps) { +function Inner({ storedState, ...props }: InnerProps) { const router = useRouter(); - const [showForm, setShowForm] = useState(!props.hasDatabaseEnv); + const [showForm, setShowForm] = useState( + !(props.hasDatabaseEnv || storedState), + ); const [addDb, res] = useAddDatabaseConnectionMutation(); - const onSubmit = async (e: SyntheticEvent) => { + const onSubmit = async ( + e: SyntheticEvent, + variables: AddDatabaseConnectionMutationVariables, + ) => { e.preventDefault(); try { - const db = await addDb({ variables: { useEnv: true } }); + const db = await addDb({ variables }); await res.client.clearStore(); if (!db.data) { return; @@ -39,16 +47,30 @@ function Inner(props: InnerProps) {

Choose how to connect

- + {storedState ? ( + + ) : ( + + )}
or
@@ -64,7 +86,12 @@ export default function AddConnectionOptions() { return ( } + render={data => ( + + )} /> ); } diff --git a/web/components/pageComponents/ConfigurationPage/queries.ts b/web/components/pageComponents/ConfigurationPage/queries.ts index 93236774..da6cf58e 100644 --- a/web/components/pageComponents/ConfigurationPage/queries.ts +++ b/web/components/pageComponents/ConfigurationPage/queries.ts @@ -2,30 +2,31 @@ import { gql } from "@apollo/client"; export const ADD_DATABASE_CONNECTION = gql` mutation AddDatabaseConnection( - $url: String + $connectionUrl: String $useEnv: Boolean $hideDoltFeatures: Boolean $useSSL: Boolean - $shouldStore: Boolean ) { addDatabaseConnection( - url: $url + connectionUrl: $connectionUrl useEnv: $useEnv hideDoltFeatures: $hideDoltFeatures useSSL: $useSSL - shouldStore: $shouldStore ) } `; export const HAS_DB_ENV = gql` + fragment StoredState on StoredState { + connectionUrl + useSSL + hideDoltFeatures + } query DatabaseState { databaseState { hasEnv storedState { - connectionUrl - useSSL - hideDoltFeatures + ...StoredState } } } diff --git a/web/components/pageComponents/ConfigurationPage/useConfig.ts b/web/components/pageComponents/ConfigurationPage/useConfig.ts index 9e6e18aa..908c178f 100644 --- a/web/components/pageComponents/ConfigurationPage/useConfig.ts +++ b/web/components/pageComponents/ConfigurationPage/useConfig.ts @@ -2,7 +2,7 @@ import { useAddDatabaseConnectionMutation } from "@gen/graphql-types"; import useSetState from "@hooks/useSetState"; import { maybeDatabase } from "@lib/urls"; import { useRouter } from "next/router"; -import { Dispatch, SyntheticEvent, useEffect } from "react"; +import { Dispatch, SyntheticEvent } from "react"; const defaultState = { host: "", @@ -31,21 +31,10 @@ type ReturnType = { export default function useConfig(): ReturnType { const router = useRouter(); const [state, setState] = useSetState(defaultState); - - useEffect(() => { - const config = sessionStorage.getItem("db-config"); - if (!config) { - return; - } - const parsed = JSON.parse(config); - setState(parsed); - }, []); - const [addDb, res] = useAddDatabaseConnectionMutation(); const clearState = () => { setState(defaultState); - sessionStorage.removeItem("db-config"); }; const onSubmit = async (e: SyntheticEvent) => { @@ -58,7 +47,7 @@ export default function useConfig(): ReturnType { try { const db = await addDb({ variables: { - url, + connectionUrl: url, hideDoltFeatures: state.hideDoltFeatures, useSSL: state.useSSL, }, @@ -67,18 +56,7 @@ export default function useConfig(): ReturnType { if (!db.data) { return; } - sessionStorage.setItem( - "db-config", - JSON.stringify({ - host: state.host, - port: state.port, - username: state.username, - database: state.database, - hideDoltFeatures: state.hideDoltFeatures, - connectionUrl: state.connectionUrl, - useSSL: state.useSSL, - }), - ); + const { href, as } = maybeDatabase(db.data.addDatabaseConnection); await router.push(href, as); } catch (_) { diff --git a/web/gen/graphql-types.tsx b/web/gen/graphql-types.tsx index 8b795484..41ad3298 100644 --- a/web/gen/graphql-types.tsx +++ b/web/gen/graphql-types.tsx @@ -212,9 +212,8 @@ export type Mutation = { export type MutationAddDatabaseConnectionArgs = { + connectionUrl?: InputMaybe; hideDoltFeatures?: InputMaybe; - shouldStore?: InputMaybe; - url?: InputMaybe; useEnv?: InputMaybe; useSSL?: InputMaybe; }; @@ -853,16 +852,17 @@ export type RowsForViewsQueryVariables = Exact<{ export type RowsForViewsQuery = { __typename?: 'Query', views: Array<{ __typename?: 'SchemaItem', name: string, type: SchemaType }> }; export type AddDatabaseConnectionMutationVariables = Exact<{ - url?: InputMaybe; + connectionUrl?: InputMaybe; useEnv?: InputMaybe; hideDoltFeatures?: InputMaybe; useSSL?: InputMaybe; - shouldStore?: InputMaybe; }>; export type AddDatabaseConnectionMutation = { __typename?: 'Mutation', addDatabaseConnection?: string | null }; +export type StoredStateFragment = { __typename?: 'StoredState', connectionUrl: string, useSSL?: boolean | null, hideDoltFeatures?: boolean | null }; + export type DatabaseStateQueryVariables = Exact<{ [key: string]: never; }>; @@ -1326,6 +1326,13 @@ export const TableWithColumnsFragmentDoc = gql` } } ${ColumnForTableListFragmentDoc}`; +export const StoredStateFragmentDoc = gql` + fragment StoredState on StoredState { + connectionUrl + useSSL + hideDoltFeatures +} + `; export const BranchFragmentDoc = gql` fragment Branch on Branch { _id @@ -2330,13 +2337,12 @@ export type RowsForViewsLazyQueryHookResult = ReturnType; export type RowsForViewsQueryResult = Apollo.QueryResult; export const AddDatabaseConnectionDocument = gql` - mutation AddDatabaseConnection($url: String, $useEnv: Boolean, $hideDoltFeatures: Boolean, $useSSL: Boolean, $shouldStore: Boolean) { + mutation AddDatabaseConnection($connectionUrl: String, $useEnv: Boolean, $hideDoltFeatures: Boolean, $useSSL: Boolean) { addDatabaseConnection( - url: $url + connectionUrl: $connectionUrl useEnv: $useEnv hideDoltFeatures: $hideDoltFeatures useSSL: $useSSL - shouldStore: $shouldStore ) } `; @@ -2355,11 +2361,10 @@ export type AddDatabaseConnectionMutationFn = Apollo.MutationFunction Date: Mon, 27 Nov 2023 11:37:28 -0800 Subject: [PATCH 3/7] Remove DockerREADME until releases are automated --- DockerREADME.md | 119 ------------------------------------------------ 1 file changed, 119 deletions(-) delete mode 100644 DockerREADME.md diff --git a/DockerREADME.md b/DockerREADME.md deleted file mode 100644 index 675e2c7c..00000000 --- a/DockerREADME.md +++ /dev/null @@ -1,119 +0,0 @@ -# dolt-workbench - -The Dolt Workbench can be connected to any MySQL-compatible database. Unlock extra version control features when you use [Dolt](https://doltdb.com). - -You can find the code for the Dolt Workbench in [this GitHub repository](https://github.com/dolthub/dolt-workbench). - -## Getting started - -```zsh -% docker pull dolthub/dolt-workbench:latest -% docker run -p 9002:9002 -p 3000:3000 dolthub/dolt-workbench:latest -``` - -Go to http://localhost:3000 and add your database information. - -You can also access the GraphQL Playground at http://localhost:9002/graphql. - -### Using environment variables - -If you'd like to specify your database information via environment variables rather than input through the UI, you can run the Docker image using a `DATABASE_URL` variable. - -```zsh -% docker run -p 9002:9002 -p 3000:3000 -e DATABASE_URL=mysql://username:password@host/defaultdb dolthub/dolt-workbench:latest -``` - -You can disable SSL by setting `USE_SSL=false` and hide Dolt features (for non-Dolt MySQL databases) using `HIDE_DOLT_FEATURES=true`. - -## Connecting to an internet accessible database - -If your database is already internet accessible (i.e. your database is hosted on a service like [AWS RDS](https://aws.amazon.com/rds/) or [Hosted Dolt](https://hosted.doltdb.com)), simply enter your database connection information through the UI or pass the database URL environment variable to `docker run`. - -## Connecting to a locally installed database - -If you'd like to connect to a MySQL server running on your local machine, there are some additional steps. Docker containers cannot use `localhost` or `127.0.0.1` to access services running on the host machine because they have their own local network. - -### MySQL - -You'll can configure MySQL running on your local machine to accept connections. - -By default, MySQL listens only to localhost. To allow connections from other hosts (like this Docker container), you can configure MySQL to listen to all IP addresses. This is done by setting the `bind-address` directive in the MySQL configuration file (`my.cnf` or `my.ini`) to `0.0.0.0`. Remember to restart the server after making this change (`brew services restart mysql`). - -On a Mac with MySQL installed via Homebrew, the location of this file is `/opt/homebrew/etc/my.cnf`. It can also be found at `/etc/mysql/my.cnf` or `/etc/mysql/mysql.conf.d/mysqld.cnf`. - -Docker containers cannot use `localhost` or `127.0.0.1` to access services running on the host machine because they have their own local network. Instead, you can use `host.docker.internal` as the host IP which Docker resolves to the internal IP address used by the host machine. - -### Dolt - -Once you have a running MySQL-compatible server, you need to open the port it is running on to the internet. To do this on a home or employer network, we recommend using [`ngrok`](https://ngrok.com/). `ngrok` is a hosted service that allows you to open a port on your computer to the internet fairly painlessly. First follow the [ngrok installation instructions](https://dashboard.ngrok.com/get-started/setup) for your operating system. Then run `ngrok tcp 3306` (or substitute 3306 for whichever port you started the server on). This will start a program with the following output in your terminal: - -```zsh -ngrok by @inconshreveable (Ctrl+C to quit) - -Session Status online -Account user@gmail.com (Plan: Free) -Version 2.3.35 -Region United States (us) -Web Interface http://127.0.0.1:4040 -Forwarding tcp://0.tcp.ngrok.io:15612 -> localhost:3306 - -Connections ttl opn rt1 rt5 p50 p90 - 78 0 0.00 0.01 0.05 0.40 -``` - -Now a port on an `ngrok.io` host is open and forwarding traffic through a secure tunnel to a host on your machine. In the above case the `ngrok` host is named `0.tcp.ngrok.io` and the port is `15612`. You can use this to connect to your MySQL server from the Docker container. - -## Connecting to a Docker installed database - -You can use Docker container networking to allow containers to connect to and communicate with each other, or to non-Docker workloads. This allows us to run a local MySQL server in the same network as the workbench. - -First, create the network: - -``` -% docker network create dolt-workbench -``` - -And then run the workbench in that network: - -``` -% docker run --network dolt-workbench -p 9002:9002 -p 3000:3000 dolthub/dolt-workbench:latest -``` - -### MySQL - -For MySQL databases, you can use the [`mysql/mysql-server`](https://hub.docker.com/r/mysql/mysql-server/) image to start a SQL server for a new database: - -``` -% docker run --network dolt-workbench --name mysql-db mysql/mysql-server:latest -``` - -Or an existing database on your local machine (see [MySQL documentation for more information](https://dev.mysql.com/doc/refman/8.0/en/docker-mysql-more-topics.html#docker-persisting-data-configuration)): - -``` -% docker run --name=mysql-db --network dolt-workbench \ ---mount type=bind,src=/path-on-host-machine/my.cnf,dst=/etc/my.cnf \ ---mount type=bind,src=/path-on-host-machine/datadir,dst=/var/lib/mysql \ --d mysql/mysql-server:latest -``` - -> Note: You can see at the bottom of [this article](https://dev.mysql.com/doc/refman/8.0/en/macos-installation-pkg.html) where the relevant MySQL directories are located on a Mac. This location is different if you installed MySQL via Homebrew. If you get an error about the bind source path not existing, you may need to add these paths to the [shared paths configuration from Docker](https://docs.docker.com/desktop/settings/mac/#file-sharing). - -> Note: Your MySQL version must be the same for your Docker container and source data directory. - -For new servers, follow the instructions in the [`mysql/mysql-server` README](https://hub.docker.com/r/mysql/mysql-server/) for setting a password for the `root` user. You may want to create a new user for this workbench that can be accessed from any host. When you enter your database configuration from the web UI, you can use `mysql-db` as the host name to connect to the database running in that container. - -### Dolt - -For Dolt databases, you can use the [`dolthub/dolt-sql-server`](https://hub.docker.com/repository/docker/dolthub/dolt-sql-server/general) image to start a SQL server for a new database: - -``` -% docker run --network dolt-workbench --name my-doltdb -p 3307:3306 dolthub/dolt-sql-server:latest -``` - -Or you can run a Dolt SQL server for an existing database on your local machine: - -``` -% docker run --network dolt-workbench --name my-doltdb -p 3307:3306 -v ~/path/to/parent/dir:/var/lib/dolt dolthub/dolt-sql-server:latest -``` - -When you enter your database configuration from the web UI, you can use `my-doltdb` as the host name to connect to the database running in that container. From d0476fcd55f645a4de955ae15eb313302c7c1106 Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Mon, 27 Nov 2023 12:54:25 -0800 Subject: [PATCH 4/7] Store volume for connection info --- .dockerignore | 2 +- Dockerfile | 2 ++ graphql-server/.gitignore | 2 +- .../src/databases/database.resolver.ts | 19 ++++++++++++++++--- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.dockerignore b/.dockerignore index 82e3e6f7..37828631 100644 --- a/.dockerignore +++ b/.dockerignore @@ -27,4 +27,4 @@ images/ **/.yarn/cache **/.yarn/install-state.gz -**/store.json \ No newline at end of file +**/store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index cb9252c8..7c059316 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,8 @@ FROM --platform=linux/amd64 node:18.13.0-alpine3.17 as graphql-base FROM graphql-base AS graphql-deps WORKDIR /app/graphql-server +VOLUME /app/graphql-server/store + # Copy the package.json and yarn files COPY --chown=node:node graphql-server/.yarn ./.yarn COPY --chown=node:node graphql-server/yarn.lock graphql-server/package.json graphql-server/.yarnrc.yml ./ diff --git a/graphql-server/.gitignore b/graphql-server/.gitignore index ed08a45c..39ca0e33 100644 --- a/graphql-server/.gitignore +++ b/graphql-server/.gitignore @@ -45,4 +45,4 @@ lerna-debug.log* !.yarn/sdks !.yarn/versions node_modules -store.json +store diff --git a/graphql-server/src/databases/database.resolver.ts b/graphql-server/src/databases/database.resolver.ts index a62c5aee..236c414a 100644 --- a/graphql-server/src/databases/database.resolver.ts +++ b/graphql-server/src/databases/database.resolver.ts @@ -8,7 +8,8 @@ import { Query, Resolver, } from "@nestjs/graphql"; -import { readFileSync, writeFileSync } from "fs"; +import * as fs from "fs"; +import { resolve } from "path"; import { DataSourceService } from "../dataSources/dataSource.service"; import { DBArgs } from "../utils/commonTypes"; @@ -79,7 +80,12 @@ export class DatabaseResolver { async databaseState(): Promise { const hasEnv = !!this.configService.get("DATABASE_URL"); try { - const file = readFileSync("store.json", { encoding: "utf8", flag: "r" }); + const file = fs.readFileSync( + resolve(__dirname, "../../store/store.json"), + { + encoding: "utf8", + }, + ); if (!file) { return { hasEnv }; } @@ -148,8 +154,15 @@ export class DatabaseResolver { }; await this.dss.addDS(workbenchConfig); + if (!fs.existsSync(resolve(__dirname, "../../store"))) { + fs.mkdirSync(resolve(__dirname, "../../store")); + } + const stringified = JSON.stringify(workbenchConfig); - writeFileSync("store.json", stringified); + fs.writeFileSync( + resolve(__dirname, "../../store/store.json"), + stringified, + ); } else { throw new Error("database url not provided"); } From 69e49994c14867a509572ef11476cddd62768d3f Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Mon, 27 Nov 2023 14:33:03 -0800 Subject: [PATCH 5/7] Save multiple connections --- graphql-server/schema.gql | 21 ++- graphql-server/src/app.module.ts | 4 +- .../src/dataSources/dataSource.service.ts | 2 +- .../src/databases/database.model.ts | 16 +++ .../src/databases/database.resolver.ts | 113 +++++---------- .../src/fileStore/fileStore.module.ts | 5 + .../src/fileStore/fileStore.service.ts | 64 +++++++++ web/components/Navbar/index.tsx | 4 +- .../AddConnectionOptions.tsx | 97 ------------- .../ConfigurationPage/index.module.css | 65 --------- .../ConfigurationPage/index.tsx | 21 --- .../ExistingConnections/Item.tsx | 46 +++++++ .../ExistingConnections/index.module.css | 19 +++ .../ExistingConnections/index.tsx | 61 +++++++++ .../NewConnection/index.module.css | 31 +++++ .../NewConnection/index.tsx} | 26 ++-- .../NewConnection}/useConfig.ts | 2 + .../ConnectionsPage/index.module.css | 19 +++ .../pageComponents/ConnectionsPage/index.tsx | 54 ++++++++ .../queries.ts | 26 ++-- .../pageComponents/HomePage/index.tsx | 42 +----- web/gen/graphql-types.tsx | 129 +++++++++++------- web/pages/configuration.tsx | 11 -- web/pages/connections.tsx | 11 ++ 24 files changed, 490 insertions(+), 399 deletions(-) create mode 100644 graphql-server/src/databases/database.model.ts create mode 100644 graphql-server/src/fileStore/fileStore.module.ts create mode 100644 graphql-server/src/fileStore/fileStore.service.ts delete mode 100644 web/components/pageComponents/ConfigurationPage/AddConnectionOptions.tsx delete mode 100644 web/components/pageComponents/ConfigurationPage/index.module.css delete mode 100644 web/components/pageComponents/ConfigurationPage/index.tsx create mode 100644 web/components/pageComponents/ConnectionsPage/ExistingConnections/Item.tsx create mode 100644 web/components/pageComponents/ConnectionsPage/ExistingConnections/index.module.css create mode 100644 web/components/pageComponents/ConnectionsPage/ExistingConnections/index.tsx create mode 100644 web/components/pageComponents/ConnectionsPage/NewConnection/index.module.css rename web/components/pageComponents/{ConfigurationPage/Form.tsx => ConnectionsPage/NewConnection/index.tsx} (85%) rename web/components/pageComponents/{ConfigurationPage => ConnectionsPage/NewConnection}/useConfig.ts (97%) create mode 100644 web/components/pageComponents/ConnectionsPage/index.module.css create mode 100644 web/components/pageComponents/ConnectionsPage/index.tsx rename web/components/pageComponents/{ConfigurationPage => ConnectionsPage}/queries.ts (50%) delete mode 100644 web/pages/configuration.tsx create mode 100644 web/pages/connections.tsx diff --git a/graphql-server/schema.gql b/graphql-server/schema.gql index 4bd1ab8d..04f86581 100644 --- a/graphql-server/schema.gql +++ b/graphql-server/schema.gql @@ -94,20 +94,16 @@ type CommitList { list: [Commit!]! } -type DoltDatabaseDetails { - isDolt: Boolean! - hideDoltFeatures: Boolean! -} - -type StoredState { +type DatabaseConnection { connectionUrl: String! - useSSL: Boolean + name: String! hideDoltFeatures: Boolean + useSSL: Boolean } -type DatabaseState { - hasEnv: Boolean! - storedState: StoredState +type DoltDatabaseDetails { + isDolt: Boolean! + hideDoltFeatures: Boolean! } type DiffStat { @@ -277,7 +273,7 @@ type Query { defaultBranch(databaseName: String!): Branch commits(offset: Int, databaseName: String!, refName: String, afterCommitId: String, twoDot: Boolean, excludingCommitsFromRefName: String): CommitList! currentDatabase: String - databaseState: DatabaseState! + storedConnections: [DatabaseConnection!]! databases: [String!]! doltDatabaseDetails: DoltDatabaseDetails! diffStat(databaseName: String!, fromRefName: String!, toRefName: String!, refName: String, type: CommitDiffType, tableName: String): DiffStat! @@ -328,7 +324,8 @@ enum DiffRowType { type Mutation { createBranch(databaseName: String!, newBranchName: String!, fromRefName: String!): Branch! deleteBranch(databaseName: String!, branchName: String!): Boolean! - addDatabaseConnection(connectionUrl: String, useEnv: Boolean, hideDoltFeatures: Boolean, useSSL: Boolean): String + addDatabaseConnection(connectionUrl: String!, name: String!, hideDoltFeatures: Boolean, useSSL: Boolean): String + removeDatabaseConnection(name: String!): Boolean! createDatabase(databaseName: String!): Boolean! resetDatabase: Boolean! loadDataFile(tableName: String!, refName: String!, databaseName: String!, importOp: ImportOperation!, fileType: FileType!, file: Upload!, modifier: LoadDataModifier): Boolean! diff --git a/graphql-server/src/app.module.ts b/graphql-server/src/app.module.ts index eace8c76..5a7d8934 100644 --- a/graphql-server/src/app.module.ts +++ b/graphql-server/src/app.module.ts @@ -1,20 +1,20 @@ import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo"; import { Module } from "@nestjs/common"; -import { ConfigModule } from "@nestjs/config"; import { GraphQLModule } from "@nestjs/graphql"; import { TerminusModule } from "@nestjs/terminus"; import { DataSourceModule } from "./dataSources/dataSource.module"; +import { FileStoreModule } from "./fileStore/fileStore.module"; import resolvers from "./resolvers"; @Module({ imports: [ - ConfigModule.forRoot({ envFilePath: ".development.env" }), GraphQLModule.forRoot({ autoSchemaFile: "schema.gql", context: ctx => ctx, driver: ApolloDriver, }), DataSourceModule, + FileStoreModule, TerminusModule, ], providers: resolvers, diff --git a/graphql-server/src/dataSources/dataSource.service.ts b/graphql-server/src/dataSources/dataSource.service.ts index bada23f0..6ac3b585 100644 --- a/graphql-server/src/dataSources/dataSource.service.ts +++ b/graphql-server/src/dataSources/dataSource.service.ts @@ -6,7 +6,7 @@ import { RawRows } from "../utils/commonTypes"; export const dbNotFoundErr = "Database connection not found"; export type ParQuery = (q: string, p?: any[] | undefined) => Promise; -export class WorkbenchConfig { +class WorkbenchConfig { hideDoltFeatures: boolean; connectionUrl: string; diff --git a/graphql-server/src/databases/database.model.ts b/graphql-server/src/databases/database.model.ts new file mode 100644 index 00000000..25f859f5 --- /dev/null +++ b/graphql-server/src/databases/database.model.ts @@ -0,0 +1,16 @@ +import { Field, ObjectType } from "@nestjs/graphql"; + +@ObjectType() +export class DatabaseConnection { + @Field() + connectionUrl: string; + + @Field() + name: string; + + @Field({ nullable: true }) + hideDoltFeatures?: boolean; + + @Field({ nullable: true }) + useSSL?: boolean; +} diff --git a/graphql-server/src/databases/database.resolver.ts b/graphql-server/src/databases/database.resolver.ts index 236c414a..eca8499b 100644 --- a/graphql-server/src/databases/database.resolver.ts +++ b/graphql-server/src/databases/database.resolver.ts @@ -1,4 +1,3 @@ -import { ConfigService } from "@nestjs/config"; import { Args, ArgsType, @@ -8,18 +7,18 @@ import { Query, Resolver, } from "@nestjs/graphql"; -import * as fs from "fs"; -import { resolve } from "path"; import { DataSourceService } from "../dataSources/dataSource.service"; +import { FileStoreService } from "../fileStore/fileStore.service"; import { DBArgs } from "../utils/commonTypes"; +import { DatabaseConnection } from "./database.model"; @ArgsType() class AddDatabaseConnectionArgs { - @Field({ nullable: true }) - connectionUrl?: string; + @Field() + connectionUrl: string; - @Field({ nullable: true }) - useEnv?: boolean; + @Field() + name: string; @Field({ nullable: true }) hideDoltFeatures?: boolean; @@ -37,32 +36,17 @@ class DoltDatabaseDetails { hideDoltFeatures: boolean; } -@ObjectType() -class StoredState { - @Field() - connectionUrl: string; - - @Field({ nullable: true }) - useSSL?: boolean; - - @Field({ nullable: true }) - hideDoltFeatures?: boolean; -} - -@ObjectType() -class DatabaseState { +@ArgsType() +class RemoveDatabaseConnectionArgs { @Field() - hasEnv: boolean; - - @Field(_type => StoredState, { nullable: true }) - storedState?: StoredState; + name: string; } -@Resolver() +@Resolver(_of => DatabaseConnection) export class DatabaseResolver { constructor( private readonly dss: DataSourceService, - private readonly configService: ConfigService, + private readonly fileStoreService: FileStoreService, ) {} @Query(_returns => String, { nullable: true }) @@ -76,28 +60,9 @@ export class DatabaseResolver { } } - @Query(_returns => DatabaseState) - async databaseState(): Promise { - const hasEnv = !!this.configService.get("DATABASE_URL"); - try { - const file = fs.readFileSync( - resolve(__dirname, "../../store/store.json"), - { - encoding: "utf8", - }, - ); - if (!file) { - return { hasEnv }; - } - const parsed = JSON.parse(file); - return { - hasEnv, - storedState: parsed, - }; - } catch (err) { - console.error("Error reading store.json:", err); - return { hasEnv }; - } + @Query(_returns => [DatabaseConnection]) + async storedConnections(): Promise { + return this.fileStoreService.getStore(); } @Query(_returns => [String]) @@ -135,43 +100,31 @@ export class DatabaseResolver { async addDatabaseConnection( @Args() args: AddDatabaseConnectionArgs, ): Promise { - if (args.useEnv) { - const url = this.configService.get("DATABASE_URL"); - if (!url) throw new Error("DATABASE_URL not found in env"); - const hideDoltFeatures = this.configService.get("HIDE_DOLT_FEATURES"); - const useSSL = this.configService.get("USE_SSL"); - const workbenchConfig = { - connectionUrl: url, - hideDoltFeatures: !!hideDoltFeatures && hideDoltFeatures === "true", - useSSL: useSSL !== undefined ? useSSL === "true" : true, - }; - await this.dss.addDS(workbenchConfig); - } else if (args.connectionUrl) { - const workbenchConfig = { - connectionUrl: args.connectionUrl, - hideDoltFeatures: !!args.hideDoltFeatures, - useSSL: !!args.useSSL, - }; - await this.dss.addDS(workbenchConfig); - - if (!fs.existsSync(resolve(__dirname, "../../store"))) { - fs.mkdirSync(resolve(__dirname, "../../store")); - } - - const stringified = JSON.stringify(workbenchConfig); - fs.writeFileSync( - resolve(__dirname, "../../store/store.json"), - stringified, - ); - } else { - throw new Error("database url not provided"); - } + const workbenchConfig = { + connectionUrl: args.connectionUrl, + hideDoltFeatures: !!args.hideDoltFeatures, + useSSL: !!args.useSSL, + }; + await this.dss.addDS(workbenchConfig); + + this.fileStoreService.addItemToStore({ + ...workbenchConfig, + name: args.name, + }); const db = await this.currentDatabase(); if (!db) return undefined; return db; } + @Mutation(_returns => Boolean) + async removeDatabaseConnection( + @Args() args: RemoveDatabaseConnectionArgs, + ): Promise { + this.fileStoreService.removeItemFromStore(args.name); + return true; + } + @Mutation(_returns => Boolean) async createDatabase(@Args() args: DBArgs): Promise { const qr = this.dss.getQR(); diff --git a/graphql-server/src/fileStore/fileStore.module.ts b/graphql-server/src/fileStore/fileStore.module.ts new file mode 100644 index 00000000..c1e92570 --- /dev/null +++ b/graphql-server/src/fileStore/fileStore.module.ts @@ -0,0 +1,5 @@ +import { Module } from "@nestjs/common"; +import { FileStoreService } from "./fileStore.service"; + +@Module({ providers: [FileStoreService], exports: [FileStoreService] }) +export class FileStoreModule {} diff --git a/graphql-server/src/fileStore/fileStore.service.ts b/graphql-server/src/fileStore/fileStore.service.ts new file mode 100644 index 00000000..17307617 --- /dev/null +++ b/graphql-server/src/fileStore/fileStore.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from "@nestjs/common"; +import * as fs from "fs"; +import { resolve } from "path"; +import { DatabaseConnection } from "../databases/database.model"; + +@Injectable() +export class FileStoreService { + constructor() {} + + getStore(): Array { + try { + const file = fs.readFileSync( + resolve(__dirname, "../../store/store.json"), + { + encoding: "utf8", + }, + ); + if (!file) { + return []; + } + const parsed = JSON.parse(file); + return parsed; + } catch (err) { + console.error("Error reading store.json:", err); + return []; + } + } + + addItemToStore(item: DatabaseConnection): void { + const store = this.getStore(); + + const existingItem = store.find(storeItem => storeItem.name === item.name); + if (existingItem) { + if (existingItem.connectionUrl === item.connectionUrl) return; + throw new Error("name already exists"); + } + + store.push(item); + + if (!fs.existsSync(resolve(__dirname, "../../store"))) { + fs.mkdirSync(resolve(__dirname, "../../store")); + } + + fs.writeFileSync( + resolve(__dirname, "../../store/store.json"), + JSON.stringify(store), + { + encoding: "utf8", + }, + ); + } + + removeItemFromStore(name: string): void { + const store = this.getStore(); + const newStore = store.filter(item => item.name !== name); + fs.writeFileSync( + resolve(__dirname, "../../store/store.json"), + JSON.stringify(newStore), + { + encoding: "utf8", + }, + ); + } +} diff --git a/web/components/Navbar/index.tsx b/web/components/Navbar/index.tsx index 8b51e4d9..ebbc5560 100644 --- a/web/components/Navbar/index.tsx +++ b/web/components/Navbar/index.tsx @@ -14,8 +14,8 @@ export default function Navbar(props: Props) {
Documentation - - Configuration + + Connections
diff --git a/web/components/pageComponents/ConfigurationPage/AddConnectionOptions.tsx b/web/components/pageComponents/ConfigurationPage/AddConnectionOptions.tsx deleted file mode 100644 index e8098426..00000000 --- a/web/components/pageComponents/ConfigurationPage/AddConnectionOptions.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import ErrorMsg from "@components/ErrorMsg"; -import QueryHandler from "@components/util/QueryHandler"; -import { - AddDatabaseConnectionMutationVariables, - StoredStateFragment, - useAddDatabaseConnectionMutation, - useDatabaseStateQuery, -} from "@gen/graphql-types"; -import { maybeDatabase } from "@lib/urls"; -import { useRouter } from "next/router"; -import { SyntheticEvent, useState } from "react"; -import Form from "./Form"; -import css from "./index.module.css"; - -type InnerProps = { - hasDatabaseEnv: boolean; - storedState?: StoredStateFragment; -}; - -function Inner({ storedState, ...props }: InnerProps) { - const router = useRouter(); - const [showForm, setShowForm] = useState( - !(props.hasDatabaseEnv || storedState), - ); - const [addDb, res] = useAddDatabaseConnectionMutation(); - - const onSubmit = async ( - e: SyntheticEvent, - variables: AddDatabaseConnectionMutationVariables, - ) => { - e.preventDefault(); - try { - const db = await addDb({ variables }); - await res.client.clearStore(); - if (!db.data) { - return; - } - const { href, as } = maybeDatabase(db.data.addDatabaseConnection); - router.push(href, as).catch(console.error); - } catch (_) { - // Handled by res.error - } - }; - - if (!showForm) { - return ( -
-

Choose how to connect

-
- {storedState ? ( - - ) : ( - - )} -
or
- -
- -
- ); - } - - return
; -} - -export default function AddConnectionOptions() { - const res = useDatabaseStateQuery(); - return ( - ( - - )} - /> - ); -} diff --git a/web/components/pageComponents/ConfigurationPage/index.module.css b/web/components/pageComponents/ConfigurationPage/index.module.css deleted file mode 100644 index 9666896a..00000000 --- a/web/components/pageComponents/ConfigurationPage/index.module.css +++ /dev/null @@ -1,65 +0,0 @@ -.container { - @apply max-w-6xl py-24 bg-ld-grey; -} - -.inner { - @apply flex justify-between; -} - -.top { - @apply mr-24 mt-10; - h1 { - @apply text-left; - } - p { - @apply text-xl max-w-lg mt-7 leading-8; - } -} - -.options { - @apply flex flex-col my-14 justify-between items-center max-w-2xl mx-auto; -} - -.option { - @apply w-72 h-24 bg-indigo-50 rounded border-2 border-acc-linkblue text-acc-linkblue px-4 py-3; - - h3 { - @apply text-lg; - } - - &:hover { - @apply border-acc-hoverlinkblue text-acc-hoverlinkblue bg-blue-50; - } -} - -.databaseForm { - @apply w-full max-w-xl; - - h3 { - @apply mb-8; - } -} - -.whiteContainer { - @apply max-w-xl w-full border rounded-lg py-10 px-16 bg-white mx-auto; -} - -.checkbox { - @apply mt-7 mb-10 text-sm; -} - -.advancedSettings { - @apply my-5 flex items-center; - - svg { - @apply mr-2; - } -} - -.or { - @apply font-semibold text-center w-full my-5 text-lg; -} - -.err { - @apply text-center; -} diff --git a/web/components/pageComponents/ConfigurationPage/index.tsx b/web/components/pageComponents/ConfigurationPage/index.tsx deleted file mode 100644 index ff84dbcc..00000000 --- a/web/components/pageComponents/ConfigurationPage/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import MainLayout from "@components/layouts/MainLayout"; -import DoltLink from "@components/links/DoltLink"; -import AddConnectionOptions from "./AddConnectionOptions"; -import css from "./index.module.css"; - -export default function ConfigurationPage() { - return ( - -
-
-

Configure your database

-

- Connect the workbench to any MySQL-compatible database. Use{" "} - Dolt to unlock version control features. -

-
- -
-
- ); -} diff --git a/web/components/pageComponents/ConnectionsPage/ExistingConnections/Item.tsx b/web/components/pageComponents/ConnectionsPage/ExistingConnections/Item.tsx new file mode 100644 index 00000000..5fe1a921 --- /dev/null +++ b/web/components/pageComponents/ConnectionsPage/ExistingConnections/Item.tsx @@ -0,0 +1,46 @@ +import Button from "@components/Button"; +import ErrorMsg from "@components/ErrorMsg"; +import { + DatabaseConnectionFragment, + useAddDatabaseConnectionMutation, +} from "@gen/graphql-types"; +import { maybeDatabase } from "@lib/urls"; +import { IoMdClose } from "@react-icons/all-files/io/IoMdClose"; +import { useRouter } from "next/router"; +import css from "./index.module.css"; + +type Props = { + conn: DatabaseConnectionFragment; + onDeleteClicked: (n: string) => void; +}; + +export default function Item({ conn, onDeleteClicked }: Props) { + const router = useRouter(); + const [addDb, res] = useAddDatabaseConnectionMutation(); + + const onClick = async () => { + try { + const db = await addDb({ variables: conn }); + await res.client.clearStore(); + if (!db.data) { + return; + } + const { href, as } = maybeDatabase(db.data.addDatabaseConnection); + router.push(href, as).catch(console.error); + } catch (_) { + // Handled by res.error + } + }; + + return ( + <> +
  • + {conn.name} + onDeleteClicked(conn.name)}> + + +
  • + + + ); +} diff --git a/web/components/pageComponents/ConnectionsPage/ExistingConnections/index.module.css b/web/components/pageComponents/ConnectionsPage/ExistingConnections/index.module.css new file mode 100644 index 00000000..bf07c2d3 --- /dev/null +++ b/web/components/pageComponents/ConnectionsPage/ExistingConnections/index.module.css @@ -0,0 +1,19 @@ +.whiteContainer { + @apply max-w-xl w-full border rounded-lg py-10 px-16 bg-white mx-auto; +} + +.options { + @apply my-6; + + ul { + @apply mb-8; + } +} + +.connection { + @apply my-3 border rounded py-1.5 px-3 flex justify-between; +} + +.err { + @apply text-center; +} diff --git a/web/components/pageComponents/ConnectionsPage/ExistingConnections/index.tsx b/web/components/pageComponents/ConnectionsPage/ExistingConnections/index.tsx new file mode 100644 index 00000000..b459f512 --- /dev/null +++ b/web/components/pageComponents/ConnectionsPage/ExistingConnections/index.tsx @@ -0,0 +1,61 @@ +import Button from "@components/Button"; +import DeleteModal from "@components/DeleteModal"; +import { + DatabaseConnectionFragment, + StoredConnectionsDocument, + useRemoveConnectionMutation, +} from "@gen/graphql-types"; +import { useState } from "react"; +import Item from "./Item"; +import css from "./index.module.css"; + +type Props = { + connections: DatabaseConnectionFragment[]; + setShowForm: (s: boolean) => void; +}; + +export default function ExistingConnections(props: Props) { + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [connectionNameToDelete, setConnectionNameToDelete] = useState(""); + + const onDeleteClicked = (name: string) => { + setConnectionNameToDelete(name); + setDeleteModalOpen(true); + }; + + return ( +
    +

    MySQL Connections

    +
    +
      + {props.connections.map(conn => ( + + ))} +
    + +

    + Are you sure you want to delete the "{connectionNameToDelete} + " connection? +

    +
    + +
    +
    + ); +} diff --git a/web/components/pageComponents/ConnectionsPage/NewConnection/index.module.css b/web/components/pageComponents/ConnectionsPage/NewConnection/index.module.css new file mode 100644 index 00000000..9192b7fa --- /dev/null +++ b/web/components/pageComponents/ConnectionsPage/NewConnection/index.module.css @@ -0,0 +1,31 @@ +.databaseForm { + @apply w-full max-w-xl; + + h3 { + @apply mb-8; + } +} + +.whiteContainer { + @apply max-w-xl w-full border rounded-lg py-10 px-16 bg-white mx-auto; +} + +.nameInput { + @apply pb-4 mb-8 border-b; +} + +.or { + @apply font-semibold text-center w-full my-5 text-lg; +} + +.advancedSettings { + @apply my-5 flex items-center; + + svg { + @apply mr-2; + } +} + +.checkbox { + @apply mt-7 mb-10 text-sm; +} diff --git a/web/components/pageComponents/ConfigurationPage/Form.tsx b/web/components/pageComponents/ConnectionsPage/NewConnection/index.tsx similarity index 85% rename from web/components/pageComponents/ConfigurationPage/Form.tsx rename to web/components/pageComponents/ConnectionsPage/NewConnection/index.tsx index 814eb1b9..46e0ec3f 100644 --- a/web/components/pageComponents/ConfigurationPage/Form.tsx +++ b/web/components/pageComponents/ConnectionsPage/NewConnection/index.tsx @@ -9,14 +9,16 @@ import css from "./index.module.css"; import useConfig from "./useConfig"; type Props = { - hasDatabaseEnv: boolean; + canGoBack: boolean; setShowForm: (s: boolean) => void; }; -export default function Form(props: Props) { +export default function NewConnection(props: Props) { const { onSubmit, state, setState, error, clearState } = useConfig(); + const canSubmit = + state.name && (state.connectionUrl || (state.host && state.username)); - const onCancel = props.hasDatabaseEnv + const onCancel = props.canGoBack ? () => { props.setShowForm(false); } @@ -28,6 +30,15 @@ export default function Form(props: Props) {

    Set up new connection

    +
    + setState({ name: n })} + label="Name" + placeholder="mydatabase (required)" + horizontal + /> +
    setState({ connectionUrl: c })} @@ -106,14 +117,9 @@ export default function Form(props: Props) { - diff --git a/web/components/pageComponents/ConfigurationPage/useConfig.ts b/web/components/pageComponents/ConnectionsPage/NewConnection/useConfig.ts similarity index 97% rename from web/components/pageComponents/ConfigurationPage/useConfig.ts rename to web/components/pageComponents/ConnectionsPage/NewConnection/useConfig.ts index 908c178f..8986f514 100644 --- a/web/components/pageComponents/ConfigurationPage/useConfig.ts +++ b/web/components/pageComponents/ConnectionsPage/NewConnection/useConfig.ts @@ -5,6 +5,7 @@ import { useRouter } from "next/router"; import { Dispatch, SyntheticEvent } from "react"; const defaultState = { + name: "", host: "", port: "3306", username: "root", @@ -47,6 +48,7 @@ export default function useConfig(): ReturnType { try { const db = await addDb({ variables: { + name: state.name, connectionUrl: url, hideDoltFeatures: state.hideDoltFeatures, useSSL: state.useSSL, diff --git a/web/components/pageComponents/ConnectionsPage/index.module.css b/web/components/pageComponents/ConnectionsPage/index.module.css new file mode 100644 index 00000000..66e7d0cf --- /dev/null +++ b/web/components/pageComponents/ConnectionsPage/index.module.css @@ -0,0 +1,19 @@ +.container { + @apply max-w-6xl py-24 bg-ld-grey; +} + +.inner { + @apply flex justify-between; +} + +.top { + @apply mr-24 mt-10; + + h1 { + @apply text-left; + } + + p { + @apply text-xl max-w-lg mt-7 leading-8; + } +} diff --git a/web/components/pageComponents/ConnectionsPage/index.tsx b/web/components/pageComponents/ConnectionsPage/index.tsx new file mode 100644 index 00000000..d83ff310 --- /dev/null +++ b/web/components/pageComponents/ConnectionsPage/index.tsx @@ -0,0 +1,54 @@ +import MainLayout from "@components/layouts/MainLayout"; +import DoltLink from "@components/links/DoltLink"; +import QueryHandler from "@components/util/QueryHandler"; +import { + DatabaseConnectionFragment, + useStoredConnectionsQuery, +} from "@gen/graphql-types"; +import { useEffect, useState } from "react"; +import ExistingConnections from "./ExistingConnections"; +import NewConnection from "./NewConnection"; +import css from "./index.module.css"; + +type InnerProps = { + connections: DatabaseConnectionFragment[]; +}; + +function Inner(props: InnerProps) { + const [showForm, setShowForm] = useState(!props.connections.length); + + useEffect(() => { + setShowForm(!props.connections.length); + }, [props.connections]); + + if (showForm) { + return ( + + ); + } + return ; +} + +export default function ConfigurationPage() { + const res = useStoredConnectionsQuery(); + return ( + +
    +
    +

    Welcome to the Dolt Workbench

    +

    + Connect the workbench to any MySQL-compatible database. Use{" "} + Dolt to unlock version control features. +

    +
    + } + /> +
    +
    + ); +} diff --git a/web/components/pageComponents/ConfigurationPage/queries.ts b/web/components/pageComponents/ConnectionsPage/queries.ts similarity index 50% rename from web/components/pageComponents/ConfigurationPage/queries.ts rename to web/components/pageComponents/ConnectionsPage/queries.ts index da6cf58e..35b55385 100644 --- a/web/components/pageComponents/ConfigurationPage/queries.ts +++ b/web/components/pageComponents/ConnectionsPage/queries.ts @@ -2,32 +2,36 @@ import { gql } from "@apollo/client"; export const ADD_DATABASE_CONNECTION = gql` mutation AddDatabaseConnection( - $connectionUrl: String - $useEnv: Boolean + $connectionUrl: String! + $name: String! $hideDoltFeatures: Boolean $useSSL: Boolean ) { addDatabaseConnection( connectionUrl: $connectionUrl - useEnv: $useEnv + name: $name hideDoltFeatures: $hideDoltFeatures useSSL: $useSSL ) } `; -export const HAS_DB_ENV = gql` - fragment StoredState on StoredState { +export const STORED_CONNECTIONS = gql` + fragment DatabaseConnection on DatabaseConnection { connectionUrl + name useSSL hideDoltFeatures } - query DatabaseState { - databaseState { - hasEnv - storedState { - ...StoredState - } + query StoredConnections { + storedConnections { + ...DatabaseConnection } } `; + +export const REMOVE_CONNECTION = gql` + mutation RemoveConnection($name: String!) { + removeDatabaseConnection(name: $name) + } +`; diff --git a/web/components/pageComponents/HomePage/index.tsx b/web/components/pageComponents/HomePage/index.tsx index 3bcae1f4..203f3ce7 100644 --- a/web/components/pageComponents/HomePage/index.tsx +++ b/web/components/pageComponents/HomePage/index.tsx @@ -1,45 +1,5 @@ -import Loader from "@components/Loader"; -import { - useAddDatabaseConnectionMutation, - useDatabaseStateQuery, -} from "@gen/graphql-types"; -import useEffectAsync from "@hooks/useEffectAsync"; -import { maybeDatabase } from "@lib/urls"; -import { useRouter } from "next/router"; -import { useState } from "react"; -import ConfigurationPage from "../ConfigurationPage"; +import ConfigurationPage from "../ConnectionsPage"; export default function HomePage() { - const router = useRouter(); - const res = useDatabaseStateQuery(); - const [addDb] = useAddDatabaseConnectionMutation(); - const [loading, setLoading] = useState(true); - - useEffectAsync(async () => { - if (res.error) { - setLoading(false); - return; - } - if (!res.data) return; - if (!res.data.databaseState.hasEnv) { - setLoading(false); - return; - } - try { - const db = await addDb({ variables: { useEnv: true } }); - await res.client.clearStore(); - if (!db.data) { - setLoading(false); - return; - } - const { href, as } = maybeDatabase(db.data.addDatabaseConnection); - router.push(href, as).catch(console.error); - } catch (_) { - // Handled by res.error - setLoading(false); - } - }, [res.data?.databaseState.hasEnv, res.error]); - - if (loading) return ; return ; } diff --git a/web/gen/graphql-types.tsx b/web/gen/graphql-types.tsx index 41ad3298..32dd09fd 100644 --- a/web/gen/graphql-types.tsx +++ b/web/gen/graphql-types.tsx @@ -88,10 +88,12 @@ export type CommitList = { nextOffset?: Maybe; }; -export type DatabaseState = { - __typename?: 'DatabaseState'; - hasEnv: Scalars['Boolean']['output']; - storedState?: Maybe; +export type DatabaseConnection = { + __typename?: 'DatabaseConnection'; + connectionUrl: Scalars['String']['output']; + hideDoltFeatures?: Maybe; + name: Scalars['String']['output']; + useSSL?: Maybe; }; export enum DiffRowType { @@ -207,14 +209,15 @@ export type Mutation = { deleteTag: Scalars['Boolean']['output']; loadDataFile: Scalars['Boolean']['output']; mergePull: Scalars['Boolean']['output']; + removeDatabaseConnection: Scalars['Boolean']['output']; resetDatabase: Scalars['Boolean']['output']; }; export type MutationAddDatabaseConnectionArgs = { - connectionUrl?: InputMaybe; + connectionUrl: Scalars['String']['input']; hideDoltFeatures?: InputMaybe; - useEnv?: InputMaybe; + name: Scalars['String']['input']; useSSL?: InputMaybe; }; @@ -268,6 +271,11 @@ export type MutationMergePullArgs = { toBranchName: Scalars['String']['input']; }; + +export type MutationRemoveDatabaseConnectionArgs = { + name: Scalars['String']['input']; +}; + export type PullDetailCommit = { __typename?: 'PullDetailCommit'; _id: Scalars['ID']['output']; @@ -315,7 +323,6 @@ export type Query = { branches: BranchNamesList; commits: CommitList; currentDatabase?: Maybe; - databaseState: DatabaseState; databases: Array; defaultBranch?: Maybe; diffStat: DiffStat; @@ -332,6 +339,7 @@ export type Query = { sqlSelect: SqlSelect; sqlSelectForCsvDownload: Scalars['String']['output']; status: Array; + storedConnections: Array; table: Table; tableNames: TableNames; tables: Array; @@ -590,13 +598,6 @@ export type Status = { tableName: Scalars['String']['output']; }; -export type StoredState = { - __typename?: 'StoredState'; - connectionUrl: Scalars['String']['output']; - hideDoltFeatures?: Maybe; - useSSL?: Maybe; -}; - export type Table = { __typename?: 'Table'; _id: Scalars['ID']['output']; @@ -852,8 +853,8 @@ export type RowsForViewsQueryVariables = Exact<{ export type RowsForViewsQuery = { __typename?: 'Query', views: Array<{ __typename?: 'SchemaItem', name: string, type: SchemaType }> }; export type AddDatabaseConnectionMutationVariables = Exact<{ - connectionUrl?: InputMaybe; - useEnv?: InputMaybe; + connectionUrl: Scalars['String']['input']; + name: Scalars['String']['input']; hideDoltFeatures?: InputMaybe; useSSL?: InputMaybe; }>; @@ -861,12 +862,19 @@ export type AddDatabaseConnectionMutationVariables = Exact<{ export type AddDatabaseConnectionMutation = { __typename?: 'Mutation', addDatabaseConnection?: string | null }; -export type StoredStateFragment = { __typename?: 'StoredState', connectionUrl: string, useSSL?: boolean | null, hideDoltFeatures?: boolean | null }; +export type DatabaseConnectionFragment = { __typename?: 'DatabaseConnection', connectionUrl: string, name: string, useSSL?: boolean | null, hideDoltFeatures?: boolean | null }; -export type DatabaseStateQueryVariables = Exact<{ [key: string]: never; }>; +export type StoredConnectionsQueryVariables = Exact<{ [key: string]: never; }>; -export type DatabaseStateQuery = { __typename?: 'Query', databaseState: { __typename?: 'DatabaseState', hasEnv: boolean, storedState?: { __typename?: 'StoredState', connectionUrl: string, useSSL?: boolean | null, hideDoltFeatures?: boolean | null } | null } }; +export type StoredConnectionsQuery = { __typename?: 'Query', storedConnections: Array<{ __typename?: 'DatabaseConnection', connectionUrl: string, name: string, useSSL?: boolean | null, hideDoltFeatures?: boolean | null }> }; + +export type RemoveConnectionMutationVariables = Exact<{ + name: Scalars['String']['input']; +}>; + + +export type RemoveConnectionMutation = { __typename?: 'Mutation', removeDatabaseConnection: boolean }; export type BranchFragment = { __typename?: 'Branch', _id: string, branchName: string, databaseName: string, lastUpdated: any, lastCommitter: string }; @@ -1326,9 +1334,10 @@ export const TableWithColumnsFragmentDoc = gql` } } ${ColumnForTableListFragmentDoc}`; -export const StoredStateFragmentDoc = gql` - fragment StoredState on StoredState { +export const DatabaseConnectionFragmentDoc = gql` + fragment DatabaseConnection on DatabaseConnection { connectionUrl + name useSSL hideDoltFeatures } @@ -2337,10 +2346,10 @@ export type RowsForViewsLazyQueryHookResult = ReturnType; export type RowsForViewsQueryResult = Apollo.QueryResult; export const AddDatabaseConnectionDocument = gql` - mutation AddDatabaseConnection($connectionUrl: String, $useEnv: Boolean, $hideDoltFeatures: Boolean, $useSSL: Boolean) { + mutation AddDatabaseConnection($connectionUrl: String!, $name: String!, $hideDoltFeatures: Boolean, $useSSL: Boolean) { addDatabaseConnection( connectionUrl: $connectionUrl - useEnv: $useEnv + name: $name hideDoltFeatures: $hideDoltFeatures useSSL: $useSSL ) @@ -2362,7 +2371,7 @@ export type AddDatabaseConnectionMutationFn = Apollo.MutationFunction; export type AddDatabaseConnectionMutationResult = Apollo.MutationResult; export type AddDatabaseConnectionMutationOptions = Apollo.BaseMutationOptions; -export const DatabaseStateDocument = gql` - query DatabaseState { - databaseState { - hasEnv - storedState { - ...StoredState - } +export const StoredConnectionsDocument = gql` + query StoredConnections { + storedConnections { + ...DatabaseConnection } } - ${StoredStateFragmentDoc}`; + ${DatabaseConnectionFragmentDoc}`; /** - * __useDatabaseStateQuery__ + * __useStoredConnectionsQuery__ * - * To run a query within a React component, call `useDatabaseStateQuery` and pass it any options that fit your needs. - * When your component renders, `useDatabaseStateQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `useStoredConnectionsQuery` and pass it any options that fit your needs. + * When your component renders, `useStoredConnectionsQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example - * const { data, loading, error } = useDatabaseStateQuery({ + * const { data, loading, error } = useStoredConnectionsQuery({ * variables: { * }, * }); */ -export function useDatabaseStateQuery(baseOptions?: Apollo.QueryHookOptions) { +export function useStoredConnectionsQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(DatabaseStateDocument, options); + return Apollo.useQuery(StoredConnectionsDocument, options); } -export function useDatabaseStateLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { +export function useStoredConnectionsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(DatabaseStateDocument, options); + return Apollo.useLazyQuery(StoredConnectionsDocument, options); } -export function useDatabaseStateSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { +export function useStoredConnectionsSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useSuspenseQuery(DatabaseStateDocument, options); + return Apollo.useSuspenseQuery(StoredConnectionsDocument, options); } -export type DatabaseStateQueryHookResult = ReturnType; -export type DatabaseStateLazyQueryHookResult = ReturnType; -export type DatabaseStateSuspenseQueryHookResult = ReturnType; -export type DatabaseStateQueryResult = Apollo.QueryResult; +export type StoredConnectionsQueryHookResult = ReturnType; +export type StoredConnectionsLazyQueryHookResult = ReturnType; +export type StoredConnectionsSuspenseQueryHookResult = ReturnType; +export type StoredConnectionsQueryResult = Apollo.QueryResult; +export const RemoveConnectionDocument = gql` + mutation RemoveConnection($name: String!) { + removeDatabaseConnection(name: $name) +} + `; +export type RemoveConnectionMutationFn = Apollo.MutationFunction; + +/** + * __useRemoveConnectionMutation__ + * + * To run a mutation, you first call `useRemoveConnectionMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRemoveConnectionMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [removeConnectionMutation, { data, loading, error }] = useRemoveConnectionMutation({ + * variables: { + * name: // value for 'name' + * }, + * }); + */ +export function useRemoveConnectionMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(RemoveConnectionDocument, options); + } +export type RemoveConnectionMutationHookResult = ReturnType; +export type RemoveConnectionMutationResult = Apollo.MutationResult; +export type RemoveConnectionMutationOptions = Apollo.BaseMutationOptions; export const BranchListDocument = gql` query BranchList($databaseName: String!, $sortBy: SortBranchesBy) { branches(databaseName: $databaseName, sortBy: $sortBy) { diff --git a/web/pages/configuration.tsx b/web/pages/configuration.tsx deleted file mode 100644 index 819c2471..00000000 --- a/web/pages/configuration.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import ConfigurationPage from "@components/pageComponents/ConfigurationPage"; -import Page from "@components/util/Page"; -import { NextPage } from "next"; - -const Configuration: NextPage = () => ( - - - -); - -export default Configuration; diff --git a/web/pages/connections.tsx b/web/pages/connections.tsx new file mode 100644 index 00000000..b13106a6 --- /dev/null +++ b/web/pages/connections.tsx @@ -0,0 +1,11 @@ +import ConnectionsPage from "@components/pageComponents/ConnectionsPage"; +import Page from "@components/util/Page"; +import { NextPage } from "next"; + +const Connections: NextPage = () => ( + + + +); + +export default Connections; From 29b6ebf984c9902e815b6694ec91b6492f85d35a Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Mon, 27 Nov 2023 14:44:44 -0800 Subject: [PATCH 6/7] graphql: Fix lint --- graphql-server/src/fileStore/fileStore.service.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/graphql-server/src/fileStore/fileStore.service.ts b/graphql-server/src/fileStore/fileStore.service.ts index 17307617..493c7137 100644 --- a/graphql-server/src/fileStore/fileStore.service.ts +++ b/graphql-server/src/fileStore/fileStore.service.ts @@ -5,9 +5,8 @@ import { DatabaseConnection } from "../databases/database.model"; @Injectable() export class FileStoreService { - constructor() {} - - getStore(): Array { + // eslint-disable-next-line class-methods-use-this + getStore(): DatabaseConnection[] { try { const file = fs.readFileSync( resolve(__dirname, "../../store/store.json"), From b05fd76cc6e8365e445ce071cb25e47bb8ca0a99 Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Mon, 27 Nov 2023 14:49:46 -0800 Subject: [PATCH 7/7] Update README --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 70f08925..3e3611a0 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,15 @@ Navigate to http://localhost:3000 to enter your database information. You can find more in-depth instructions on [DockerHub](https://hub.docker.com/r/dolthub/dolt-workbench). +### Saving connection information between runs + +If you want to save connection information between Docker runs, you can mount a local +directory to the `store` directory in `/app/graphql-server` in the container. + +``` +% docker run -p 9002:9002 -p 3000:3000 -v ~/path/to/store:/app/graphql-server/store dolthub/dolt-workbench:latest +``` + ## Getting started from source First, clone this repository.