diff --git a/graphql-server/schema.gql b/graphql-server/schema.gql index 41a39dfa..b12e4c58 100644 --- a/graphql-server/schema.gql +++ b/graphql-server/schema.gql @@ -346,6 +346,7 @@ type Mutation { resetDatabase(newDatabase: String): Boolean! loadDataFile(schemaName: String, tableName: String!, refName: String!, databaseName: String!, importOp: ImportOperation!, fileType: FileType!, file: Upload!, modifier: LoadDataModifier): Boolean! mergePull(fromBranchName: String!, toBranchName: String!, databaseName: String!, author: AuthorInfo): Boolean! + restoreAllTables(databaseName: String!, refName: String!): Boolean! createTag(tagName: String!, databaseName: String!, message: String, fromRefName: String!, author: AuthorInfo): String! deleteTag(databaseName: String!, tagName: String!): Boolean! } diff --git a/graphql-server/src/queryFactory/dolt/index.ts b/graphql-server/src/queryFactory/dolt/index.ts index 905ffa1c..97488ad1 100644 --- a/graphql-server/src/queryFactory/dolt/index.ts +++ b/graphql-server/src/queryFactory/dolt/index.ts @@ -389,6 +389,51 @@ export class DoltQueryFactory args.refName, ); } + + async restoreAllTables(args: t.RefArgs): t.PR { + return this.queryQR( + async qr => { + console.log("[restore_all]: starting transaction"); + await qr.query("BEGIN"); + + console.log("[restore_all]: calling"); + const res = await qr.query(qh.callResetHard); + + if (res.length && res[0].status !== "0") { + console.log("[restore_all]: reset not successful, rolling back"); + await qr.query("ROLLBACK"); + throw new Error("Reset --hard not successful"); + } + + // Handles any new tables that weren't restored by dolt_reset(--hard) + const status = await dem.getDoltStatus(qr.manager); + + if (status.length) { + status.forEach(async r => { + console.log("[restore_all]: checking out new table", r.table_name); + const checkRes = await qr.query(qh.callCheckoutTable, [ + r.table_name, + ]); + if (checkRes.length && checkRes[0].status !== "0") { + console.log( + "[restore_all]: checkout not successful, rolling back", + ); + await qr.query("ROLLBACK"); + throw new Error( + `Checking out table not successful: ${checkRes[0].message}`, + ); + } + }); + } + + console.log("[restore_all]: committing"); + await qr.query("COMMIT"); + return res; + }, + args.databaseName, + args.refName, + ); + } } async function getTableInfoWithQR( diff --git a/graphql-server/src/queryFactory/dolt/queries.ts b/graphql-server/src/queryFactory/dolt/queries.ts index 9835cddb..3026a790 100644 --- a/graphql-server/src/queryFactory/dolt/queries.ts +++ b/graphql-server/src/queryFactory/dolt/queries.ts @@ -139,3 +139,7 @@ export function getOrderByFromDiffCols(cols: RawRows): string { const orderBy = diffCols.map(c => `\`${c}\` ASC`).join(", "); return orderBy === "" ? "" : `ORDER BY ${orderBy} `; } + +export const callResetHard = `CALL DOLT_RESET("--hard")`; + +export const callCheckoutTable = `CALL DOLT_CHECKOUT(?)`; diff --git a/graphql-server/src/queryFactory/doltgres/index.ts b/graphql-server/src/queryFactory/doltgres/index.ts index e70e2a5e..0ca7f043 100644 --- a/graphql-server/src/queryFactory/doltgres/index.ts +++ b/graphql-server/src/queryFactory/doltgres/index.ts @@ -11,7 +11,7 @@ import { handleTableNotFound } from "../../utils"; import * as dem from "../dolt/doltEntityManager"; import { getAuthorString, handleRefNotFound, unionCols } from "../dolt/utils"; import { PostgresQueryFactory } from "../postgres"; -import { getSchema } from "../postgres/utils"; +import { getSchema, tableWithoutSchema } from "../postgres/utils"; import * as t from "../types"; import * as qh from "./queries"; @@ -382,6 +382,49 @@ export class DoltgresQueryFactory args.refName, ); } + + async restoreAllTables(args: t.RefArgs): t.PR { + return this.queryQR( + async qr => { + console.log("[restore_all]: starting transaction"); + await qr.query("BEGIN"); + + console.log("[restore_all]: calling"); + const res = await qr.query(qh.callResetHard); + if (res.length && res[0].dolt_reset[0] !== "0") { + console.log("[restore_all]: reset not successful, rolling back"); + await qr.query("ROLLBACK"); + throw new Error("Reset --hard not successful"); + } + + // Handles any new tables that weren't restored by dolt_reset(--hard) + const status = await dem.getDoltStatus(qr.manager); + if (status.length) { + status.forEach(async r => { + console.log("[restore_all]: checking out new table", r.table_name); + const checkRes = await qr.query(qh.callCheckoutTable, [ + tableWithoutSchema(r.table_name), + ]); + if (checkRes.length && checkRes[0].dolt_checkout[0] !== "0") { + console.log( + "[restore_all]: checkout not successful, rolling back", + ); + await qr.query("ROLLBACK"); + throw new Error( + `Checking out table not successful: ${checkRes[0].message}`, + ); + } + }); + } + + console.log("[restore_all]: committing"); + await qr.query("COMMIT"); + return res; + }, + args.databaseName, + args.refName, + ); + } } async function getTableInfoWithQR( diff --git a/graphql-server/src/queryFactory/doltgres/queries.ts b/graphql-server/src/queryFactory/doltgres/queries.ts index 3da3019d..00773529 100644 --- a/graphql-server/src/queryFactory/doltgres/queries.ts +++ b/graphql-server/src/queryFactory/doltgres/queries.ts @@ -156,3 +156,7 @@ export function getOrderByFromDiffCols(cols: t.RawRows): string { const orderBy = diffCols.map(c => `"${c}" ASC`).join(", "); return orderBy === "" ? "" : `ORDER BY ${orderBy} `; } + +export const callResetHard = `SELECT DOLT_RESET('--hard')`; + +export const callCheckoutTable = `SELECT DOLT_CHECKOUT($1::text)`; diff --git a/graphql-server/src/queryFactory/index.ts b/graphql-server/src/queryFactory/index.ts index 3622cfa9..6879ebb5 100644 --- a/graphql-server/src/queryFactory/index.ts +++ b/graphql-server/src/queryFactory/index.ts @@ -142,4 +142,6 @@ export declare class QueryFactory { ): Promise<{ rows: t.RawRows; columns: t.RawRows }>; getRowDiffs(args: t.RowDiffArgs): t.DiffRes; + + restoreAllTables(args: t.RefArgs): t.PR; } diff --git a/graphql-server/src/queryFactory/mysql/index.ts b/graphql-server/src/queryFactory/mysql/index.ts index b523649c..6db114f8 100644 --- a/graphql-server/src/queryFactory/mysql/index.ts +++ b/graphql-server/src/queryFactory/mysql/index.ts @@ -297,4 +297,8 @@ export class MySQLQueryFactory async getRowDiffs(_args: t.RowDiffArgs): t.DiffRes { throw notDoltError("get row sided diffs"); } + + async restoreAllTables(_args: t.RefArgs): t.PR { + throw notDoltError("restore all tables"); + } } diff --git a/graphql-server/src/status/status.resolver.ts b/graphql-server/src/status/status.resolver.ts index 4bd76782..5f9ac38f 100644 --- a/graphql-server/src/status/status.resolver.ts +++ b/graphql-server/src/status/status.resolver.ts @@ -1,4 +1,4 @@ -import { Args, Query, Resolver } from "@nestjs/graphql"; +import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; import { ConnectionProvider } from "../connections/connection.provider"; import { RefArgs } from "../utils/commonTypes"; import { Status, fromStatusRows } from "./status.model"; @@ -13,4 +13,11 @@ export class StatusResolver { const res = await conn.getStatus(args); return fromStatusRows(res, args.databaseName, args.refName); } + + @Mutation(_returns => Boolean) + async restoreAllTables(@Args() args: RefArgs): Promise { + const conn = this.conn.connection(); + await conn.restoreAllTables(args); + return true; + } } diff --git a/web/.eslintrc.js b/web/.eslintrc.js index 8f0696b3..b8ccb685 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -13,6 +13,9 @@ module.exports = { extensions: [".js", ".jsx", ".ts", ".tsx"], }, }, + next: { + rootDir: "renderer", + }, }, env: { browser: true, diff --git a/web/renderer/components/DatabaseTableHeader/CreateViewButton/CreateViewModal.tsx b/web/renderer/components/DatabaseTableHeader/CreateViewButton/CreateViewModal.tsx index ca350757..ff8c626f 100644 --- a/web/renderer/components/DatabaseTableHeader/CreateViewButton/CreateViewModal.tsx +++ b/web/renderer/components/DatabaseTableHeader/CreateViewButton/CreateViewModal.tsx @@ -3,7 +3,7 @@ import { useSqlEditorContext } from "@contexts/sqleditor"; import { FormInput, FormModal, Loader } from "@dolthub/react-components"; import useSqlBuilder from "@hooks/useSqlBuilder"; import { ModalProps } from "@lib/modalProps"; -import { DatabaseParams } from "@lib/params"; +import { OptionalRefParams } from "@lib/params"; import dynamic from "next/dynamic"; import { SyntheticEvent, useState } from "react"; import css from "./index.module.css"; @@ -13,7 +13,7 @@ const AceEditor = dynamic(async () => import("@components/AceEditor"), { }); type Props = { - params: DatabaseParams & { refName?: string }; + params: OptionalRefParams; query: string; } & ModalProps; diff --git a/web/renderer/components/DatabaseTableHeader/CreateViewButton/index.tsx b/web/renderer/components/DatabaseTableHeader/CreateViewButton/index.tsx index cbb1bbb8..45034f3a 100644 --- a/web/renderer/components/DatabaseTableHeader/CreateViewButton/index.tsx +++ b/web/renderer/components/DatabaseTableHeader/CreateViewButton/index.tsx @@ -1,12 +1,12 @@ import HideForNoWritesWrapper from "@components/util/HideForNoWritesWrapper"; import { Button } from "@dolthub/react-components"; -import { DatabaseParams } from "@lib/params"; +import { OptionalRefParams } from "@lib/params"; import { useState } from "react"; import CreateViewModal from "./CreateViewModal"; import css from "./index.module.css"; type Props = { - params: DatabaseParams & { refName?: string }; + params: OptionalRefParams; query: string; }; diff --git a/web/renderer/components/FormSelectForRefs/useGetBranchOptionsForSelect.ts b/web/renderer/components/FormSelectForRefs/useGetBranchOptionsForSelect.ts index 559cb337..d7ed4766 100644 --- a/web/renderer/components/FormSelectForRefs/useGetBranchOptionsForSelect.ts +++ b/web/renderer/components/FormSelectForRefs/useGetBranchOptionsForSelect.ts @@ -1,6 +1,9 @@ import { FormSelectTypes } from "@dolthub/react-components"; import { excerpt } from "@dolthub/web-utils"; -import { useBranchesForSelectorQuery } from "@gen/graphql-types"; +import { + BranchForBranchSelectorFragment, + useBranchesForSelectorQuery, +} from "@gen/graphql-types"; import { ApolloErrorType } from "@lib/errors/types"; import { DatabaseParams } from "@lib/params"; @@ -8,6 +11,7 @@ type ReturnType = { branchOptions: Array>; error?: ApolloErrorType; loading: boolean; + defaultBranch?: string; }; export default function useGetBranchOptionsForSelect( @@ -26,5 +30,19 @@ export default function useGetBranchOptionsForSelect( }; }) ?? []; - return { branchOptions, error: branchRes.error, loading: branchRes.loading }; + return { + branchOptions, + error: branchRes.error, + loading: branchRes.loading, + defaultBranch: getDefaultBranch(branchRes.data?.allBranches), + }; +} + +function getDefaultBranch( + branches?: BranchForBranchSelectorFragment[], +): string | undefined { + if (!branches?.length) return undefined; + const mainBranch = branches.find(b => b.branchName === "main"); + if (mainBranch) return mainBranch.branchName; + return branches[0].branchName; } diff --git a/web/renderer/components/StatusWithOptions/ResetModal.tsx b/web/renderer/components/StatusWithOptions/ResetModal.tsx index a778396a..232d52d1 100644 --- a/web/renderer/components/StatusWithOptions/ResetModal.tsx +++ b/web/renderer/components/StatusWithOptions/ResetModal.tsx @@ -1,10 +1,12 @@ import Link from "@components/links/Link"; -import { Button, Modal } from "@dolthub/react-components"; -import { StatusFragment } from "@gen/graphql-types"; +import { Button, Loader, Modal } from "@dolthub/react-components"; +import { StatusFragment, useRestoreAllMutation } from "@gen/graphql-types"; +import useMutation from "@hooks/useMutation"; import useSqlBuilder from "@hooks/useSqlBuilder"; import { ModalProps } from "@lib/modalProps"; import { RefParams } from "@lib/params"; import { getPostgresTableName } from "@lib/postgres"; +import { refetchSqlUpdateQueriesCacheEvict } from "@lib/refetchQueries"; import { sqlQuery } from "@lib/urls"; import css from "./index.module.css"; @@ -16,6 +18,21 @@ type Props = ModalProps & { export default function ResetModal(props: Props) { const { getCallProcedure, isPostgres } = useSqlBuilder(); + const { mutateFn, loading, err, client } = useMutation({ + hook: useRestoreAllMutation, + }); + + const onRestoreAll = async () => { + try { + await mutateFn({ variables: props.params }); + props.setIsOpen(false); + client + .refetchQueries(refetchSqlUpdateQueriesCacheEvict) + .catch(console.error); + } catch (_) { + // Handled by useMutation + } + }; const getTableName = (tn: string): string => { if (isPostgres) { @@ -33,9 +50,15 @@ export default function ResetModal(props: Props) { title="Reset uncommitted changes" isOpen={props.isOpen} onRequestClose={onClose} - button={} + button={ + + } + err={err} >
+

Choose to unstage staged tables or restore tables to their current contents in the current HEAD. diff --git a/web/renderer/components/StatusWithOptions/queries.ts b/web/renderer/components/StatusWithOptions/queries.ts index bc84810f..8b687710 100644 --- a/web/renderer/components/StatusWithOptions/queries.ts +++ b/web/renderer/components/StatusWithOptions/queries.ts @@ -14,3 +14,9 @@ export const GET_STATUS = gql` } } `; + +export const RESTORE_ALL = gql` + mutation RestoreAll($databaseName: String!, $refName: String!) { + restoreAllTables(databaseName: $databaseName, refName: $refName) + } +`; diff --git a/web/renderer/components/pageComponents/FileUploadPage/FileInfo/index.tsx b/web/renderer/components/pageComponents/FileUploadPage/FileInfo/index.tsx index 9bda22a6..26b41e4e 100644 --- a/web/renderer/components/pageComponents/FileUploadPage/FileInfo/index.tsx +++ b/web/renderer/components/pageComponents/FileUploadPage/FileInfo/index.tsx @@ -1,4 +1,5 @@ import { Button } from "@dolthub/react-components"; +import { excerpt } from "@dolthub/web-utils"; import { FiFile } from "@react-icons/all-files/fi/FiFile"; import cx from "classnames"; import { ReactNode } from "react"; @@ -28,7 +29,9 @@ export default function FileInfo(props: Props) { > - {state.selectedFile.name} + + {excerpt(state.selectedFile.name, 30)} + {fileSize(state.selectedFile.size)} diff --git a/web/renderer/components/pageComponents/FileUploadPage/Steps/Branch/index.tsx b/web/renderer/components/pageComponents/FileUploadPage/Steps/Branch/index.tsx index a7273f7b..13a8db11 100644 --- a/web/renderer/components/pageComponents/FileUploadPage/Steps/Branch/index.tsx +++ b/web/renderer/components/pageComponents/FileUploadPage/Steps/Branch/index.tsx @@ -1,6 +1,7 @@ import useGetBranchOptionsForSelect from "@components/FormSelectForRefs/useGetBranchOptionsForSelect"; import DatabaseLink from "@components/links/DatabaseLink"; import { ErrorMsg, FormSelect, Loader } from "@dolthub/react-components"; +import { useEffect } from "react"; import StepLayout from "../../StepLayout"; import { useFileUploadContext } from "../../contexts/fileUploadLocalForage"; import { UploadStage } from "../../enums"; @@ -16,7 +17,14 @@ export default function Branch() { function Inner() { const { state, error, updateLoad, setItem, dbParams, getUploadUrl } = useFileUploadContext(); - const { branchOptions } = useGetBranchOptionsForSelect(dbParams); + const { branchOptions, defaultBranch } = + useGetBranchOptionsForSelect(dbParams); + + useEffect(() => { + if (defaultBranch && !state.branchName) { + setItem("branchName", defaultBranch); + } + }, [defaultBranch, state.branchName, setItem]); return ( - +

diff --git a/web/renderer/components/pageComponents/FileUploadPage/Steps/Upload/index.tsx b/web/renderer/components/pageComponents/FileUploadPage/Steps/Upload/index.tsx index 22cfca88..c559fed9 100644 --- a/web/renderer/components/pageComponents/FileUploadPage/Steps/Upload/index.tsx +++ b/web/renderer/components/pageComponents/FileUploadPage/Steps/Upload/index.tsx @@ -51,7 +51,10 @@ function Inner() { - + diff --git a/web/renderer/components/pageComponents/FileUploadPage/UploadQueryInfo.tsx b/web/renderer/components/pageComponents/FileUploadPage/UploadQueryInfo.tsx index cfdeb01f..4cf3ca95 100644 --- a/web/renderer/components/pageComponents/FileUploadPage/UploadQueryInfo.tsx +++ b/web/renderer/components/pageComponents/FileUploadPage/UploadQueryInfo.tsx @@ -6,6 +6,7 @@ import css from "./index.module.css"; type Props = { forSpreadsheet?: boolean; + hideModifierOptions?: boolean; tableName: string; }; @@ -42,7 +43,7 @@ export default function UploadQueryInfo(props: Props) { - {!isPostgres && } + {!isPostgres && !props.hideModifierOptions && } ); } diff --git a/web/renderer/gen/graphql-types.tsx b/web/renderer/gen/graphql-types.tsx index c3440729..f53bc639 100644 --- a/web/renderer/gen/graphql-types.tsx +++ b/web/renderer/gen/graphql-types.tsx @@ -233,6 +233,7 @@ export type Mutation = { mergePull: Scalars['Boolean']['output']; removeDatabaseConnection: Scalars['Boolean']['output']; resetDatabase: Scalars['Boolean']['output']; + restoreAllTables: Scalars['Boolean']['output']; }; @@ -314,6 +315,12 @@ export type MutationResetDatabaseArgs = { newDatabase?: InputMaybe; }; + +export type MutationRestoreAllTablesArgs = { + databaseName: Scalars['String']['input']; + refName: Scalars['String']['input']; +}; + export type PullDetailCommit = { __typename?: 'PullDetailCommit'; _id: Scalars['ID']['output']; @@ -924,6 +931,14 @@ export type GetStatusQueryVariables = Exact<{ export type GetStatusQuery = { __typename?: 'Query', status: Array<{ __typename?: 'Status', _id: string, refName: string, tableName: string, staged: boolean, status: string }> }; +export type RestoreAllMutationVariables = Exact<{ + databaseName: Scalars['String']['input']; + refName: Scalars['String']['input']; +}>; + + +export type RestoreAllMutation = { __typename?: 'Mutation', restoreAllTables: boolean }; + export type ColumnForTableListFragment = { __typename?: 'Column', name: string, type: string, isPrimaryKey: boolean, constraints?: Array<{ __typename?: 'ColConstraint', notNull: boolean }> | null }; export type TableWithColumnsFragment = { __typename?: 'Table', _id: string, tableName: string, columns: Array<{ __typename?: 'Column', name: string, type: string, isPrimaryKey: boolean, constraints?: Array<{ __typename?: 'ColConstraint', notNull: boolean }> | null }> }; @@ -2497,6 +2512,38 @@ export type GetStatusQueryHookResult = ReturnType; export type GetStatusLazyQueryHookResult = ReturnType; export type GetStatusSuspenseQueryHookResult = ReturnType; export type GetStatusQueryResult = Apollo.QueryResult; +export const RestoreAllDocument = gql` + mutation RestoreAll($databaseName: String!, $refName: String!) { + restoreAllTables(databaseName: $databaseName, refName: $refName) +} + `; +export type RestoreAllMutationFn = Apollo.MutationFunction; + +/** + * __useRestoreAllMutation__ + * + * To run a mutation, you first call `useRestoreAllMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRestoreAllMutation` 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 [restoreAllMutation, { data, loading, error }] = useRestoreAllMutation({ + * variables: { + * databaseName: // value for 'databaseName' + * refName: // value for 'refName' + * }, + * }); + */ +export function useRestoreAllMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(RestoreAllDocument, options); + } +export type RestoreAllMutationHookResult = ReturnType; +export type RestoreAllMutationResult = Apollo.MutationResult; +export type RestoreAllMutationOptions = Apollo.BaseMutationOptions; export const TableForBranchDocument = gql` query TableForBranch($databaseName: String!, $refName: String!, $tableName: String!, $schemaName: String) { table( diff --git a/web/renderer/lib/urls.ts b/web/renderer/lib/urls.ts index 913c5bd0..a6aca2a3 100644 --- a/web/renderer/lib/urls.ts +++ b/web/renderer/lib/urls.ts @@ -113,8 +113,10 @@ export const pullDiff = (p: ps.PullDiffParams): Route => export const newRelease = (p: ps.OptionalRefParams): Route => releases(p).addStatic("new").withQuery({ refName: p.refName }); -export const upload = (p: ps.DatabaseParams & { schemaName?: string }): Route => - database(p).addStatic("upload").withQuery({ schemaName: p.schemaName }); +export const upload = (p: ps.OptionalRefAndSchemaParams): Route => + database(p) + .addStatic("upload") + .withQuery({ schemaName: p.schemaName, branchName: p.refName }); export const uploadStage = ( p: ps.UploadParamsWithOptions & {