diff --git a/graphql-server/schema.gql b/graphql-server/schema.gql index afe9458b..3978bb7c 100644 --- a/graphql-server/schema.gql +++ b/graphql-server/schema.gql @@ -279,6 +279,18 @@ type TagList { list: [Tag!]! } +type Remote { + _id: ID! + name: String! + url: String! + fetchSpecs: [String!] +} + +type RemoteList { + nextOffset: Int + list: [Remote!]! +} + type Query { branch(databaseName: String!, branchName: String!): Branch branchOrDefault(databaseName: String!, branchName: String): Branch @@ -298,6 +310,7 @@ type Query { docs(databaseName: String!, refName: String!): DocList! docOrDefaultDoc(refName: String!, databaseName: String!, docType: DocType): Doc pullWithDetails(databaseName: String!, fromBranchName: String!, toBranchName: String!): PullWithDetails! + remotes(databaseName: String!, offset: Int): RemoteList! rowDiffs(offset: Int, databaseName: String!, fromRefName: String!, toRefName: String!, refName: String, tableName: String!, filterByRowType: DiffRowType, type: CommitDiffType): RowDiffList! rows(schemaName: String, refName: String!, databaseName: String!, tableName: String!, offset: Int): RowList! schemaDiff(databaseName: String!, fromRefName: String!, toRefName: String!, refName: String, tableName: String!, type: CommitDiffType): SchemaDiff @@ -348,6 +361,8 @@ 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! + addRemote(databaseName: String!, remoteName: String!, remoteUrl: String!): String! + deleteRemote(databaseName: String!, remoteName: String!): 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/doltEntityManager.ts b/graphql-server/src/queryFactory/dolt/doltEntityManager.ts index a59acbb1..538893e5 100644 --- a/graphql-server/src/queryFactory/dolt/doltEntityManager.ts +++ b/graphql-server/src/queryFactory/dolt/doltEntityManager.ts @@ -138,3 +138,16 @@ export async function getDoltTags(em: EntityManager): t.PR { .orderBy("dolt_tags.date", "DESC") .getRawMany(); } + +export async function getDoltRemotesPaginated( + em: EntityManager, + args: t.ListRemotesArgs, +): t.PR { + return em + .createQueryBuilder() + .select("*") + .from("dolt_remotes", "") + .offset(args.offset) + .limit(ROW_LIMIT + 1) + .getRawMany(); +} diff --git a/graphql-server/src/queryFactory/dolt/index.ts b/graphql-server/src/queryFactory/dolt/index.ts index 97488ad1..234372e2 100644 --- a/graphql-server/src/queryFactory/dolt/index.ts +++ b/graphql-server/src/queryFactory/dolt/index.ts @@ -434,6 +434,29 @@ export class DoltQueryFactory args.refName, ); } + + async getRemotes(args: t.ListRemotesArgs): t.PR { + return this.queryForBuilder( + async em => dem.getDoltRemotesPaginated(em, args), + args.databaseName, + ); + } + + async addRemote(args: t.AddRemoteArgs): t.PR { + return this.query( + qh.callAddRemote, + [args.remoteName, args.remoteUrl], + args.databaseName, + ); + } + + async callDeleteRemote(args: t.RemoteArgs): t.PR { + return this.query( + qh.callDeleteRemote, + [args.remoteName], + args.databaseName, + ); + } } async function getTableInfoWithQR( diff --git a/graphql-server/src/queryFactory/dolt/queries.ts b/graphql-server/src/queryFactory/dolt/queries.ts index 3026a790..8eb926e2 100644 --- a/graphql-server/src/queryFactory/dolt/queries.ts +++ b/graphql-server/src/queryFactory/dolt/queries.ts @@ -74,6 +74,12 @@ export const threeDotSchemaDiffQuery = `SELECT * FROM DOLT_SCHEMA_DIFF(?, ?)`; export const getCallMerge = (hasAuthor = false) => `CALL DOLT_MERGE(?, "--no-ff", "-m", ?${getAuthorNameString(hasAuthor)})`; +// REMOTES + +export const callAddRemote = `CALL DOLT_REMOTE("add", ?, ?)`; + +export const callDeleteRemote = `CALL DOLT_REMOTE("remove", ?)`; + // TAGS export const callDeleteTag = `CALL DOLT_TAG("-d", ?)`; diff --git a/graphql-server/src/queryFactory/doltgres/index.ts b/graphql-server/src/queryFactory/doltgres/index.ts index 0ca7f043..bb841132 100644 --- a/graphql-server/src/queryFactory/doltgres/index.ts +++ b/graphql-server/src/queryFactory/doltgres/index.ts @@ -425,6 +425,29 @@ export class DoltgresQueryFactory args.refName, ); } + + async getRemotes(args: t.ListRemotesArgs): t.PR { + return this.queryForBuilder( + async em => dem.getDoltRemotesPaginated(em, args), + args.databaseName, + ); + } + + async addRemote(args: t.AddRemoteArgs): t.PR { + return this.query( + qh.callAddRemote, + [args.remoteName, args.remoteUrl], + args.databaseName, + ); + } + + async callDeleteRemote(args: t.RemoteArgs): t.PR { + return this.query( + qh.callDeleteRemote, + [args.remoteName], + args.databaseName, + ); + } } async function getTableInfoWithQR( diff --git a/graphql-server/src/queryFactory/doltgres/queries.ts b/graphql-server/src/queryFactory/doltgres/queries.ts index 00773529..85eefa7b 100644 --- a/graphql-server/src/queryFactory/doltgres/queries.ts +++ b/graphql-server/src/queryFactory/doltgres/queries.ts @@ -160,3 +160,9 @@ export function getOrderByFromDiffCols(cols: t.RawRows): string { export const callResetHard = `SELECT DOLT_RESET('--hard')`; export const callCheckoutTable = `SELECT DOLT_CHECKOUT($1::text)`; + +// REMOTES + +export const callAddRemote = `SELECT DOLT_REMOTE('add', $1::text, $2::text)`; + +export const callDeleteRemote = `SELECT DOLT_REMOTE('remove', $1::text)`; diff --git a/graphql-server/src/queryFactory/index.ts b/graphql-server/src/queryFactory/index.ts index 6879ebb5..0ce739b7 100644 --- a/graphql-server/src/queryFactory/index.ts +++ b/graphql-server/src/queryFactory/index.ts @@ -144,4 +144,10 @@ export declare class QueryFactory { getRowDiffs(args: t.RowDiffArgs): t.DiffRes; restoreAllTables(args: t.RefArgs): t.PR; + + getRemotes(args: t.ListRemotesArgs): t.PR; + + addRemote(args: t.AddRemoteArgs): t.PR; + + callDeleteRemote(args: t.RemoteArgs): t.PR; } diff --git a/graphql-server/src/queryFactory/mysql/index.ts b/graphql-server/src/queryFactory/mysql/index.ts index 6db114f8..9c8bcb81 100644 --- a/graphql-server/src/queryFactory/mysql/index.ts +++ b/graphql-server/src/queryFactory/mysql/index.ts @@ -301,4 +301,16 @@ export class MySQLQueryFactory async restoreAllTables(_args: t.RefArgs): t.PR { throw notDoltError("restore all tables"); } + + async getRemotes(_args: t.ListRemotesArgs): t.PR { + throw notDoltError("get remotes"); + } + + async addRemote(_: t.AddRemoteArgs): t.PR { + throw notDoltError("add remote"); + } + + async callDeleteRemote(_: t.RemoteArgs): t.PR { + throw notDoltError("delete remote"); + } } diff --git a/graphql-server/src/queryFactory/types.ts b/graphql-server/src/queryFactory/types.ts index ad151b1c..60db548b 100644 --- a/graphql-server/src/queryFactory/types.ts +++ b/graphql-server/src/queryFactory/types.ts @@ -7,6 +7,8 @@ export type RefArgs = DBArgs & { refName: string }; export type RefSchemaArgs = RefArgs & { schemaName: string }; export type RefMaybeSchemaArgs = RefArgs & { schemaName?: string }; export type BranchArgs = DBArgs & { branchName: string }; +export type RemoteArgs = DBArgs & { remoteName: string }; +export type AddRemoteArgs = RemoteArgs & { remoteUrl: string }; export type TagArgs = DBArgs & { tagName: string }; export type TableArgs = RefArgs & { tableName: string }; export type TableMaybeSchemaArgs = TableArgs & { schemaName?: string }; @@ -40,6 +42,10 @@ export type RowDiffArgs = DBArgs & { filterByRowType?: DiffRowType; }; +export type ListRemotesArgs = DBArgs & { + offset: number; +}; + export type RawRow = Record; export type RawRows = RawRow[]; export type PR = Promise; diff --git a/graphql-server/src/remotes/remote.model.ts b/graphql-server/src/remotes/remote.model.ts new file mode 100644 index 00000000..b9732505 --- /dev/null +++ b/graphql-server/src/remotes/remote.model.ts @@ -0,0 +1,47 @@ +import { Field, ID, ObjectType } from "@nestjs/graphql"; +import { __Type } from "graphql"; +import { getNextOffset, ROW_LIMIT } from "../utils"; +import { RawRow } from "../queryFactory/types"; +import { DBArgsWithOffset, ListOffsetRes } from "../utils/commonTypes"; + +@ObjectType() +export class Remote { + @Field(_type => ID) + _id: string; + + @Field() + name: string; + + @Field() + url: string; + + @Field(_type => [String], { nullable: true }) + fetchSpecs?: string[]; +} + +@ObjectType() +export class RemoteList extends ListOffsetRes { + @Field(_type => [Remote]) + list: Remote[]; +} + +export function fromDoltRemotesRow(databaseName: string, r: RawRow): Remote { + return { + _id: `databases/${databaseName}/remotes/${r.name}`, + name: r.name, + url: r.url, + fetchSpecs: r.fetch_specs, + }; +} + +export function getRemoteListRes( + remotes: RawRow[], + args: DBArgsWithOffset, +): RemoteList { + return { + list: remotes + .slice(0, ROW_LIMIT) + .map(l => fromDoltRemotesRow(args.databaseName, l)), + nextOffset: getNextOffset(remotes.length, args.offset ?? 0), + }; +} diff --git a/graphql-server/src/remotes/remote.resolver.ts b/graphql-server/src/remotes/remote.resolver.ts new file mode 100644 index 00000000..9184d747 --- /dev/null +++ b/graphql-server/src/remotes/remote.resolver.ts @@ -0,0 +1,47 @@ +import { + Args, + ArgsType, + Field, + Mutation, + Query, + Resolver, +} from "@nestjs/graphql"; +import { ConnectionProvider } from "../connections/connection.provider"; +import { DBArgs, DBArgsWithOffset, RemoteArgs } from "../utils/commonTypes"; +import { getRemoteListRes, Remote, RemoteList } from "./remote.model"; + +@ArgsType() +export class AddRemoteArgs extends DBArgs { + @Field() + remoteName: string; + + @Field() + remoteUrl: string; +} + +@Resolver(_of => Remote) +export class RemoteResolver { + constructor(private readonly conn: ConnectionProvider) {} + + @Query(_returns => RemoteList) + async remotes(@Args() args: DBArgsWithOffset): Promise { + const conn = this.conn.connection(); + + const res = await conn.getRemotes({ ...args, offset: args.offset ?? 0 }); + return getRemoteListRes(res, args); + } + + @Mutation(_returns => String) + async addRemote(@Args() args: AddRemoteArgs): Promise { + const conn = this.conn.connection(); + await conn.addRemote(args); + return args.remoteName; + } + + @Mutation(_returns => Boolean) + async deleteRemote(@Args() args: RemoteArgs): Promise { + const conn = this.conn.connection(); + await conn.callDeleteRemote(args); + return true; + } +} diff --git a/graphql-server/src/resolvers.ts b/graphql-server/src/resolvers.ts index f92ffd35..3c695d45 100644 --- a/graphql-server/src/resolvers.ts +++ b/graphql-server/src/resolvers.ts @@ -14,6 +14,7 @@ import { StatusResolver } from "./status/status.resolver"; import { TableResolver } from "./tables/table.resolver"; import { FileUploadResolver } from "./tables/upload.resolver"; import { TagResolver } from "./tags/tag.resolver"; +import { RemoteResolver } from "./remotes/remote.resolver"; const resolvers = [ BranchResolver, @@ -24,6 +25,7 @@ const resolvers = [ DocsResolver, FileUploadResolver, PullResolver, + RemoteResolver, RowDiffResolver, RowResolver, SchemaDiffResolver, diff --git a/graphql-server/src/utils/commonTypes.ts b/graphql-server/src/utils/commonTypes.ts index 0294dc7a..4bb68373 100644 --- a/graphql-server/src/utils/commonTypes.ts +++ b/graphql-server/src/utils/commonTypes.ts @@ -66,6 +66,12 @@ export class TagArgs extends DBArgs { tagName: string; } +@ArgsType() +export class RemoteArgs extends DBArgs { + @Field() + remoteName: string; +} + @InputType() export class AuthorInfo { @Field() diff --git a/web/renderer/components/DatabaseNav/tabs.ts b/web/renderer/components/DatabaseNav/tabs.ts index 004a634c..aeba46d9 100644 --- a/web/renderer/components/DatabaseNav/tabs.ts +++ b/web/renderer/components/DatabaseNav/tabs.ts @@ -4,4 +4,5 @@ export const tabs = [ "Commit Log", "Releases", "Pull Requests", + "Remotes", ]; diff --git a/web/renderer/components/DatabaseNav/utils.ts b/web/renderer/components/DatabaseNav/utils.ts index 5f6add81..4bd85986 100644 --- a/web/renderer/components/DatabaseNav/utils.ts +++ b/web/renderer/components/DatabaseNav/utils.ts @@ -9,6 +9,7 @@ import { ref, RefUrl, releases, + remotes, } from "@lib/urls"; function getUrlFromName(name: string): [DatabaseUrl, RefUrl?] { @@ -23,6 +24,8 @@ function getUrlFromName(name: string): [DatabaseUrl, RefUrl?] { return [releases]; case "Pull Requests": return [pulls]; + case "Remotes": + return [remotes]; default: return [database, ref]; } diff --git a/web/renderer/components/breadcrumbs/RemotesBreadcrumbs.tsx b/web/renderer/components/breadcrumbs/RemotesBreadcrumbs.tsx new file mode 100644 index 00000000..7d9b032b --- /dev/null +++ b/web/renderer/components/breadcrumbs/RemotesBreadcrumbs.tsx @@ -0,0 +1,19 @@ +import { DatabaseParams } from "@lib/params"; +import Breadcrumbs from "."; +import { remoteBreadcrumbDetails } from "./breadcrumbDetails"; + +type Props = { + params: DatabaseParams; + className?: string; +}; + +export default function RemotesBreadcrumbs(props: Props) { + return ( + + ); +} diff --git a/web/renderer/components/breadcrumbs/breadcrumbDetails.tsx b/web/renderer/components/breadcrumbs/breadcrumbDetails.tsx index 19644116..8f5260f3 100644 --- a/web/renderer/components/breadcrumbs/breadcrumbDetails.tsx +++ b/web/renderer/components/breadcrumbs/breadcrumbDetails.tsx @@ -113,6 +113,19 @@ export function commitLogBreadcrumbDetails( ]; } +export function remoteBreadcrumbDetails( + params: DatabaseParams, +): BreadcrumbDetails[] { + return [ + ...databaseBreadcrumbs(params), + { + child: remotes, + name: BreadcrumbName.DBRemote, + type: BreadcrumbType.Text, + }, + ]; +} + export function tableBreadcrumbsDetails( params: TableParams, ): BreadcrumbDetails[] { diff --git a/web/renderer/components/breadcrumbs/types.ts b/web/renderer/components/breadcrumbs/types.ts index badd8fd1..589ceca1 100644 --- a/web/renderer/components/breadcrumbs/types.ts +++ b/web/renderer/components/breadcrumbs/types.ts @@ -23,6 +23,7 @@ export enum BreadcrumbName { DBSchema = "db-schema", DBCommitGraph = "db-commit-graph", DBNew = "db-new", + DBRemote = "db-remote", } export enum BreadcrumbType { diff --git a/web/renderer/components/pageComponents/DatabasePage/ForRemotes/AddRemotePage/AddRemoteForm.tsx b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/AddRemotePage/AddRemoteForm.tsx new file mode 100644 index 00000000..7eaddec7 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/AddRemotePage/AddRemoteForm.tsx @@ -0,0 +1,129 @@ +import { + Button, + ButtonsWithError, + FormInput, + Loader, +} from "@dolthub/react-components"; +import { useReactiveWidth } from "@dolthub/react-hooks"; +import { + DoltDatabaseDetails, + useAddRemoteMutation, + useDoltDatabaseDetailsQuery, +} from "@gen/graphql-types"; +import useMutation from "@hooks/useMutation"; +import { DatabaseParams } from "@lib/params"; +import { refetchRemoteQueries } from "@lib/refetchQueries"; +import { remotes } from "@lib/urls"; +import { useRouter } from "next/router"; +import { SyntheticEvent, useState } from "react"; +import Link from "@components/links/Link"; +import { getDatabaseType } from "@components/DatabaseTypeLabel"; +import DoltLink from "@components/links/DoltLink"; +import DoltgresLink from "@components/links/DoltgresLink"; +import css from "./index.module.css"; + +type Props = { + params: DatabaseParams; +}; + +export default function AddRemoteForm(props: Props): JSX.Element { + const router = useRouter(); + const { data: databaseDetails, loading: databaseDetailsLoading } = + useDoltDatabaseDetailsQuery(); + const { dbLink, urlPlaceHolder } = getDbNameAndLink( + databaseDetails?.doltDatabaseDetails, + ); + const [remoteName, setRemoteName] = useState(""); + const [remoteUrl, setRemoteUrl] = useState(""); + const { + mutateFn: addRemote, + err, + loading, + } = useMutation({ + hook: useAddRemoteMutation, + refetchQueries: refetchRemoteQueries(props.params), + }); + const { isMobile } = useReactiveWidth(); + + const goToRemotesPage = () => { + const { href, as } = remotes(props.params); + router.push(href, as).catch(() => {}); + }; + + const onSubmit = async (e: SyntheticEvent) => { + e.preventDefault(); + if (!remoteName || !remoteUrl) return; + + const { data } = await addRemote({ + variables: { ...props.params, remoteName, remoteUrl }, + }); + if (!data) return; + const { href, as } = remotes(props.params); + router.push(href, as).catch(console.error); + }; + + return ( +
+
+
+ + + + + +

+ A remote is a {dbLink} database in another location, usually on a + different, network accessible host. To learn more about configuring + a remote for your database, see our{" "} + + documentation + +

+
+ + +
+ ); +} + +type ReturnType = { + dbLink: JSX.Element; + urlPlaceHolder: string; +}; + +function getDbNameAndLink(dbDetails?: DoltDatabaseDetails): ReturnType { + const type = getDatabaseType( + dbDetails?.type ?? undefined, + !!dbDetails?.isDolt, + ); + const universalUrl = "i.e. https://url-of-remote.com"; + if (type === "Dolt") { + return { + dbLink: , + urlPlaceHolder: "i.e. https://doltremoteapi.dolthub.com/owner/repo", + }; + } + if (type === "DoltgreSQL") { + return { dbLink: , urlPlaceHolder: universalUrl }; + } + return { dbLink: {type}, urlPlaceHolder: universalUrl }; +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForRemotes/AddRemotePage/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/AddRemotePage/index.module.css new file mode 100644 index 00000000..df39b339 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/AddRemotePage/index.module.css @@ -0,0 +1,22 @@ +.container { + @apply m-6; +} + +.remoteForm { + @apply max-w-2xl mx-auto; +} + +.input { + @apply mt-4 px-0; + input { + @apply text-sm rounded bg-stone-50 border border-stone-100; + + &:focus { + @apply bg-white; + } + } +} + +.text { + @apply text-sm text-stone-500 mb-5 max-w-xl mt-4; +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForRemotes/AddRemotePage/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/AddRemotePage/index.tsx new file mode 100644 index 00000000..3bbd582a --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/AddRemotePage/index.tsx @@ -0,0 +1,16 @@ +import { DatabaseParams } from "@lib/params"; +import AddRemoteForm from "./AddRemoteForm"; +import css from "./index.module.css"; + +type Props = { + params: DatabaseParams; +}; + +export default function AddRemotePage(props: Props): JSX.Element { + return ( +
+

New Remote

+ +
+ ); +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForRemotes/AddRemotePage/queries.ts b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/AddRemotePage/queries.ts new file mode 100644 index 00000000..343c8c44 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/AddRemotePage/queries.ts @@ -0,0 +1,15 @@ +import { gql } from "@apollo/client"; + +export const ADD_REMOTE = gql` + mutation AddRemote( + $databaseName: String! + $remoteName: String! + $remoteUrl: String! + ) { + addRemote( + databaseName: $databaseName + remoteName: $remoteName + remoteUrl: $remoteUrl + ) + } +`; diff --git a/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/Inner.tsx b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/Inner.tsx new file mode 100644 index 00000000..db7a0082 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/Inner.tsx @@ -0,0 +1,98 @@ +import { Button } from "@dolthub/react-components"; +import { RemoteFragment, useDeleteRemoteMutation } from "@gen/graphql-types"; + +import InfiniteScroll from "react-infinite-scroller"; +import HideForNoWritesWrapper from "@components/util/HideForNoWritesWrapper"; +import Link from "@components/links/Link"; +import { newRemote } from "@lib/urls"; +import { useState } from "react"; +import DeleteModal from "@components/DeleteModal"; +import { refetchRemoteQueries } from "@lib/refetchQueries"; +import { DatabaseParams } from "@lib/params"; +import RemoteRow from "./RemoteRow"; +import css from "./index.module.css"; + +type InnerProps = { + params: DatabaseParams; + remotes: RemoteFragment[]; + loadMore: () => Promise; + hasMore: boolean; +}; + +export default function Inner({ + remotes, + loadMore, + hasMore, + params, +}: InnerProps) { + const createUrl = newRemote(params); + const [isDeleteModalOpen, setDeleteModalOpen] = useState(false); + const [remoteNameToDelete, setRemoteNameToDelete] = useState(""); + const onDeleteClicked = (r: RemoteFragment) => { + setRemoteNameToDelete(r.name); + setDeleteModalOpen(true); + }; + return ( +
+
+

Remotes

+
+ + + + + +
+
+ {remotes.length ? ( +
+ Loading remotes ...
} + useWindow={false} + initialLoad={false} + getScrollParent={() => document.getElementById("main-content")} + > + + + + + + + + + + {remotes.map(r => ( + onDeleteClicked(r)} + params={params} + /> + ))} + +
NameUrlFetch Specs
+ +
+ ) : ( +

+ No remotes found +

+ )} + + + ); +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/RemoteRow.tsx b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/RemoteRow.tsx new file mode 100644 index 00000000..38b59b10 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/RemoteRow.tsx @@ -0,0 +1,34 @@ +import { RemoteFragment } from "@gen/graphql-types"; +import HideForNoWritesWrapper from "@components/util/HideForNoWritesWrapper"; +import { Button } from "@dolthub/react-components"; +import { FaRegTrashAlt } from "@react-icons/all-files/fa/FaRegTrashAlt"; +import { DatabaseParams } from "@lib/params"; +import css from "./index.module.css"; + +type Props = { + remote: RemoteFragment; + onDeleteClicked: () => void; + params: DatabaseParams; +}; + +export default function RemoteRow({ remote, onDeleteClicked, params }: Props) { + return ( + + {remote.name} + {remote.url} + {remote.fetchSpecs?.join(",")} + + + + + + + + + ); +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/index.module.css new file mode 100644 index 00000000..30c7b99f --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/index.module.css @@ -0,0 +1,55 @@ +.container { + @apply mx-6; +} + +.top { + @apply flex justify-between pt-6; +} + +.table { + @apply border border-stone-100 border-separate rounded-lg text-primary mx-auto my-10 overflow-x-auto block; + border-spacing: 0; + + @screen lg { + @apply min-w-[40rem] overflow-x-visible; + display: table; + } + + td, + th { + @apply px-6 text-left; + } + + th { + @apply pt-6 pb-2 bg-stone-50; + } + + td { + @apply border-t border-stone-100 py-3; + } + + tr:first-child { + th:first-child { + @apply rounded-tl-lg; + } + th:last-child { + @apply rounded-tr-lg; + } + } +} + +.noRemotes { + @apply text-center text-lg m-6; +} + +.loader { + @apply text-primary my-4 text-center; +} + +.trashColumn { + @apply flex justify-end; +} + +.icon { + @apply text-lg m-4; +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/index.tsx new file mode 100644 index 00000000..1d92200c --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/index.tsx @@ -0,0 +1,35 @@ +import { Loader, QueryHandler } from "@dolthub/react-components"; + +import { DatabaseParams } from "@lib/params"; +import { gqlDepNotFound } from "@lib/errors/graphql"; +import { errorMatches } from "@lib/errors/helpers"; +import Database404 from "@components/Database404"; + +import { useRemoteList } from "./useRemoteList"; +import Inner from "./Inner"; + +type Props = { + params: DatabaseParams; +}; + +export default function RemotesPage({ params }: Props) { + const res = useRemoteList(params); + if (res.loading) return ; + if (errorMatches(gqlDepNotFound, res.error)) { + return ; + } + + return ( + ( + + )} + /> + ); +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/queries.ts b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/queries.ts new file mode 100644 index 00000000..0c0f2f8f --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/queries.ts @@ -0,0 +1,24 @@ +import { gql } from "@apollo/client"; + +export const REMOTES_FOR_REMOTES_PAGE_QUERY = gql` + fragment Remote on Remote { + _id + name + url + fetchSpecs + } + query RemoteList($databaseName: String!, $offset: Int) { + remotes(databaseName: $databaseName, offset: $offset) { + list { + ...Remote + } + nextOffset + } + } +`; + +export const DELETE_REMOTE = gql` + mutation DeleteRemote($remoteName: String!, $databaseName: String!) { + deleteRemote(remoteName: $remoteName, databaseName: $databaseName) + } +`; diff --git a/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/useRemoteList.ts b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/useRemoteList.ts new file mode 100644 index 00000000..1626ecf7 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/RemotesPage/useRemoteList.ts @@ -0,0 +1,64 @@ +import { Maybe } from "@dolthub/web-utils"; +import { + RemoteFragment, + RemoteListDocument, + RemoteListQuery, + RemoteListQueryVariables, + useRemoteListQuery, +} from "@gen/graphql-types"; +import useApolloError from "@hooks/useApolloError"; +import { handleCaughtApolloError } from "@lib/errors/helpers"; +import { ApolloErrorType } from "@lib/errors/types"; +import { DatabaseParams } from "@lib/params"; +import { useEffect, useState } from "react"; + +type ReturnType = { + remotes: RemoteFragment[] | undefined; + loadMore: () => Promise; + error?: ApolloErrorType; + loading: boolean; + hasMore: boolean; +}; + +export function useRemoteList(params: DatabaseParams): ReturnType { + const { data, ...res } = useRemoteListQuery({ + variables: params, + fetchPolicy: "cache-and-network", + }); + const [err, setErr] = useApolloError(res.error); + const [remotes, setRemotes] = useState(data?.remotes.list); + const [offset, setOffset] = useState(data?.remotes.nextOffset); + const [lastOffset, setLastOffset] = useState>(undefined); + + useEffect(() => { + setRemotes(data?.remotes.list); + setOffset(data?.remotes.nextOffset); + }, [data, setRemotes]); + + const loadMore = async () => { + if (!offset) { + return; + } + setLastOffset(offset); + try { + const result = await res.client.query< + RemoteListQuery, + RemoteListQueryVariables + >({ + query: RemoteListDocument, + variables: { ...params, offset }, + }); + const newRemotes = result.data.remotes.list; + const newOffset = result.data.remotes.nextOffset; + setRemotes((remotes ?? []).concat(newRemotes)); + setOffset(newOffset); + } catch (e) { + handleCaughtApolloError(e, setErr); + } + }; + + const hasMore = + offset !== undefined && offset !== null && offset !== lastOffset; + + return { ...res, error: err, remotes, loadMore, hasMore }; +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForRemotes/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/index.tsx new file mode 100644 index 00000000..7ea410c3 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForRemotes/index.tsx @@ -0,0 +1,34 @@ +import RemotesBreadcrumbs from "@components/breadcrumbs/RemotesBreadcrumbs"; +import NotDoltWrapper from "@components/util/NotDoltWrapper"; +import { DatabaseParams } from "@lib/params"; +import { remotes } from "@lib/urls"; +import ForDefaultBranch from "../ForDefaultBranch"; +import AddRemotePage from "./AddRemotePage"; +import RemotesPage from "./RemotesPage"; + +type Props = { + params: DatabaseParams; + newRemote?: boolean; +}; + +export default function ForRemotes({ params, newRemote }: Props): JSX.Element { + const feature = newRemote ? "Creating remotes" : "Viewing remotes"; + return ( + } + title="remotes" + routeRefChangeTo={remotes} + > + + {newRemote ? ( + + ) : ( + + )} + + + ); +} diff --git a/web/renderer/components/pageComponents/DatabasePage/index.tsx b/web/renderer/components/pageComponents/DatabasePage/index.tsx index ed61fb11..dfc6ed79 100644 --- a/web/renderer/components/pageComponents/DatabasePage/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/index.tsx @@ -9,6 +9,7 @@ import ForPulls from "./ForPulls"; import ForQuery from "./ForQuery"; import ForRef from "./ForRef"; import ForReleases from "./ForReleases"; +import ForRemotes from "./ForRemotes"; import ForSchema from "./ForSchema"; import ForTable from "./ForTable"; import DatabasePage from "./component"; @@ -25,6 +26,7 @@ export default Object.assign(DatabasePage, { ForPullDiff, ForRef, ForReleases, + ForRemotes, ForSchema, ForTable, }); diff --git a/web/renderer/gen/graphql-types.tsx b/web/renderer/gen/graphql-types.tsx index 1df88039..4b757316 100644 --- a/web/renderer/gen/graphql-types.tsx +++ b/web/renderer/gen/graphql-types.tsx @@ -223,11 +223,13 @@ export enum LoadDataModifier { export type Mutation = { __typename?: 'Mutation'; addDatabaseConnection: CurrentDatabaseState; + addRemote: Scalars['String']['output']; createBranch: Scalars['String']['output']; createDatabase: Scalars['Boolean']['output']; createSchema: Scalars['Boolean']['output']; createTag: Scalars['String']['output']; deleteBranch: Scalars['Boolean']['output']; + deleteRemote: Scalars['Boolean']['output']; deleteTag: Scalars['Boolean']['output']; loadDataFile: Scalars['Boolean']['output']; mergePull: Scalars['Boolean']['output']; @@ -246,6 +248,13 @@ export type MutationAddDatabaseConnectionArgs = { }; +export type MutationAddRemoteArgs = { + databaseName: Scalars['String']['input']; + remoteName: Scalars['String']['input']; + remoteUrl: Scalars['String']['input']; +}; + + export type MutationCreateBranchArgs = { databaseName: Scalars['String']['input']; fromRefName: Scalars['String']['input']; @@ -280,6 +289,12 @@ export type MutationDeleteBranchArgs = { }; +export type MutationDeleteRemoteArgs = { + databaseName: Scalars['String']['input']; + remoteName: Scalars['String']['input']; +}; + + export type MutationDeleteTagArgs = { databaseName: Scalars['String']['input']; tagName: Scalars['String']['input']; @@ -381,6 +396,7 @@ export type Query = { doltProcedures: Array; doltSchemas: Array; pullWithDetails: PullWithDetails; + remotes: RemoteList; rowDiffs: RowDiffList; rows: RowList; schemaDiff?: Maybe; @@ -501,6 +517,12 @@ export type QueryPullWithDetailsArgs = { }; +export type QueryRemotesArgs = { + databaseName: Scalars['String']['input']; + offset?: InputMaybe; +}; + + export type QueryRowDiffsArgs = { databaseName: Scalars['String']['input']; filterByRowType?: InputMaybe; @@ -607,6 +629,20 @@ export enum QueryExecutionStatus { Timeout = 'Timeout' } +export type Remote = { + __typename?: 'Remote'; + _id: Scalars['ID']['output']; + fetchSpecs?: Maybe>; + name: Scalars['String']['output']; + url: Scalars['String']['output']; +}; + +export type RemoteList = { + __typename?: 'RemoteList'; + list: Array; + nextOffset?: Maybe; +}; + export type Row = { __typename?: 'Row'; columnValues: Array; @@ -1161,6 +1197,33 @@ export type DeleteTagMutationVariables = Exact<{ export type DeleteTagMutation = { __typename?: 'Mutation', deleteTag: boolean }; +export type AddRemoteMutationVariables = Exact<{ + databaseName: Scalars['String']['input']; + remoteName: Scalars['String']['input']; + remoteUrl: Scalars['String']['input']; +}>; + + +export type AddRemoteMutation = { __typename?: 'Mutation', addRemote: string }; + +export type RemoteFragment = { __typename?: 'Remote', _id: string, name: string, url: string, fetchSpecs?: Array | null }; + +export type RemoteListQueryVariables = Exact<{ + databaseName: Scalars['String']['input']; + offset?: InputMaybe; +}>; + + +export type RemoteListQuery = { __typename?: 'Query', remotes: { __typename?: 'RemoteList', nextOffset?: number | null, list: Array<{ __typename?: 'Remote', _id: string, name: string, url: string, fetchSpecs?: Array | null }> } }; + +export type DeleteRemoteMutationVariables = Exact<{ + remoteName: Scalars['String']['input']; + databaseName: Scalars['String']['input']; +}>; + + +export type DeleteRemoteMutation = { __typename?: 'Mutation', deleteRemote: boolean }; + export type LoadDataMutationVariables = Exact<{ databaseName: Scalars['String']['input']; refName: Scalars['String']['input']; @@ -1579,6 +1642,14 @@ export const PullDetailsFragmentDoc = gql` } } ${PullDetailsForPullDetailsFragmentDoc}`; +export const RemoteFragmentDoc = gql` + fragment Remote on Remote { + _id + name + url + fetchSpecs +} + `; export const ColumnForDataTableFragmentDoc = gql` fragment ColumnForDataTable on Column { name @@ -3447,6 +3518,119 @@ export function useDeleteTagMutation(baseOptions?: Apollo.MutationHookOptions; export type DeleteTagMutationResult = Apollo.MutationResult; export type DeleteTagMutationOptions = Apollo.BaseMutationOptions; +export const AddRemoteDocument = gql` + mutation AddRemote($databaseName: String!, $remoteName: String!, $remoteUrl: String!) { + addRemote( + databaseName: $databaseName + remoteName: $remoteName + remoteUrl: $remoteUrl + ) +} + `; +export type AddRemoteMutationFn = Apollo.MutationFunction; + +/** + * __useAddRemoteMutation__ + * + * To run a mutation, you first call `useAddRemoteMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useAddRemoteMutation` 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 [addRemoteMutation, { data, loading, error }] = useAddRemoteMutation({ + * variables: { + * databaseName: // value for 'databaseName' + * remoteName: // value for 'remoteName' + * remoteUrl: // value for 'remoteUrl' + * }, + * }); + */ +export function useAddRemoteMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(AddRemoteDocument, options); + } +export type AddRemoteMutationHookResult = ReturnType; +export type AddRemoteMutationResult = Apollo.MutationResult; +export type AddRemoteMutationOptions = Apollo.BaseMutationOptions; +export const RemoteListDocument = gql` + query RemoteList($databaseName: String!, $offset: Int) { + remotes(databaseName: $databaseName, offset: $offset) { + list { + ...Remote + } + nextOffset + } +} + ${RemoteFragmentDoc}`; + +/** + * __useRemoteListQuery__ + * + * To run a query within a React component, call `useRemoteListQuery` and pass it any options that fit your needs. + * When your component renders, `useRemoteListQuery` 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 } = useRemoteListQuery({ + * variables: { + * databaseName: // value for 'databaseName' + * offset: // value for 'offset' + * }, + * }); + */ +export function useRemoteListQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: RemoteListQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(RemoteListDocument, options); + } +export function useRemoteListLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(RemoteListDocument, options); + } +export function useRemoteListSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(RemoteListDocument, options); + } +export type RemoteListQueryHookResult = ReturnType; +export type RemoteListLazyQueryHookResult = ReturnType; +export type RemoteListSuspenseQueryHookResult = ReturnType; +export type RemoteListQueryResult = Apollo.QueryResult; +export const DeleteRemoteDocument = gql` + mutation DeleteRemote($remoteName: String!, $databaseName: String!) { + deleteRemote(remoteName: $remoteName, databaseName: $databaseName) +} + `; +export type DeleteRemoteMutationFn = Apollo.MutationFunction; + +/** + * __useDeleteRemoteMutation__ + * + * To run a mutation, you first call `useDeleteRemoteMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteRemoteMutation` 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 [deleteRemoteMutation, { data, loading, error }] = useDeleteRemoteMutation({ + * variables: { + * remoteName: // value for 'remoteName' + * databaseName: // value for 'databaseName' + * }, + * }); + */ +export function useDeleteRemoteMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(DeleteRemoteDocument, options); + } +export type DeleteRemoteMutationHookResult = ReturnType; +export type DeleteRemoteMutationResult = Apollo.MutationResult; +export type DeleteRemoteMutationOptions = Apollo.BaseMutationOptions; export const LoadDataDocument = gql` mutation LoadData($databaseName: String!, $refName: String!, $schemaName: String, $tableName: String!, $importOp: ImportOperation!, $fileType: FileType!, $file: Upload!, $modifier: LoadDataModifier) { loadDataFile( diff --git a/web/renderer/lib/refetchQueries.ts b/web/renderer/lib/refetchQueries.ts index 36789eed..3c1c8b68 100644 --- a/web/renderer/lib/refetchQueries.ts +++ b/web/renderer/lib/refetchQueries.ts @@ -109,6 +109,10 @@ export const refetchDeletedBranch = ( ...refetchBranchQueries(params), ]; +export const refetchRemoteQueries = ( + variables: DatabaseParams, +): RefetchQueries => [{ query: gen.RemoteListDocument, variables }]; + export const refetchSqlUpdateQueriesCacheEvict: RefetchOptions = { updateCache(cache: TCacheShape) { [ diff --git a/web/renderer/lib/urls.ts b/web/renderer/lib/urls.ts index a6aca2a3..12b422fa 100644 --- a/web/renderer/lib/urls.ts +++ b/web/renderer/lib/urls.ts @@ -96,6 +96,12 @@ function getDiffRange(p: ps.DiffParams): string { export const releases = (p: ps.OptionalRefParams): Route => database(p).addStatic("releases").withQuery({ refName: p.refName }); +export const remotes = (p: ps.DatabaseParams): Route => + database(p).addStatic("remotes"); + +export const newRemote = (p: ps.DatabaseParams): Route => + remotes(p).addStatic("new"); + const staticPulls = (p: ps.DatabaseParams) => database(p).addStatic("pulls"); export const pulls = (p: ps.PullParams): Route => diff --git a/web/renderer/pages/database/[databaseName]/remotes/index.tsx b/web/renderer/pages/database/[databaseName]/remotes/index.tsx new file mode 100644 index 00000000..961d0270 --- /dev/null +++ b/web/renderer/pages/database/[databaseName]/remotes/index.tsx @@ -0,0 +1,28 @@ +import Page from "@components/util/Page"; +import { DatabaseParams } from "@lib/params"; +import DatabasePage from "@pageComponents/DatabasePage"; +import { GetServerSideProps, NextPage } from "next"; + +type Props = { + params: DatabaseParams; +}; + +const DatabaseRemotesPage: NextPage = ({ params }) => ( + + + +); + +// #!if !isElectron +export const getServerSideProps: GetServerSideProps = async ({ + params, +}) => { + return { + props: { + params: params as DatabaseParams, + }, + }; +}; +// #!endif + +export default DatabaseRemotesPage; diff --git a/web/renderer/pages/database/[databaseName]/remotes/new.tsx b/web/renderer/pages/database/[databaseName]/remotes/new.tsx new file mode 100644 index 00000000..48f825f1 --- /dev/null +++ b/web/renderer/pages/database/[databaseName]/remotes/new.tsx @@ -0,0 +1,28 @@ +import Page from "@components/util/Page"; +import { DatabaseParams } from "@lib/params"; +import DatabasePage from "@pageComponents/DatabasePage"; +import { GetServerSideProps, NextPage } from "next"; + +type Props = { + params: DatabaseParams; +}; + +const DatabaseNewRemotePage: NextPage = ({ params }) => ( + + + +); + +// #!if !isElectron +export const getServerSideProps: GetServerSideProps = async ({ + params, +}) => { + return { + props: { + params: params as DatabaseParams, + }, + }; +}; +// #!endif + +export default DatabaseNewRemotePage;