diff --git a/packages/graphql-server/schema.gql b/packages/graphql-server/schema.gql index fce9b63f..17dc2772 100644 --- a/packages/graphql-server/schema.gql +++ b/packages/graphql-server/schema.gql @@ -98,6 +98,33 @@ type DoltDatabaseDetails { hideDoltFeatures: Boolean! } +type DiffStat { + rowsUnmodified: Float! + rowsAdded: Float! + rowsDeleted: Float! + rowsModified: Float! + cellsModified: Float! + rowCount: Float! + cellCount: Float! +} + +type DiffSummary { + _id: ID! + fromTableName: String! + toTableName: String! + tableName: String! + tableType: TableDiffType! + hasDataChanges: Boolean! + hasSchemaChanges: Boolean! +} + +enum TableDiffType { + Added + Dropped + Modified + Renamed +} + type ColumnValue { displayValue: String! } @@ -121,6 +148,28 @@ type DocList { list: [Doc!]! } +type RowDiff { + added: Row + deleted: Row +} + +type RowDiffList { + nextOffset: Int + list: [RowDiff!]! + columns: [Column!]! +} + +type TextDiff { + leftLines: String! + rightLines: String! +} + +type SchemaDiff { + schemaDiff: TextDiff + schemaPatch: [String!] + numChangedSchemas: Int +} + type SqlSelect { _id: ID! databaseName: String! @@ -166,15 +215,19 @@ type Query { branchOrDefault(databaseName: String!, branchName: String): Branch branches(databaseName: String!, sortBy: SortBranchesBy): BranchNamesList! defaultBranch(databaseName: String!): Branch - commits(offset: Int, databaseName: String!, refName: String!): CommitList! + commits(offset: Int, databaseName: String!, refName: String, afterCommitId: String): CommitList! currentDatabase: String hasDatabaseEnv: Boolean! databases: [String!]! doltDatabaseDetails: DoltDatabaseDetails! + diffStat(databaseName: String!, fromRefName: String!, toRefName: String!, refName: String, type: CommitDiffType, tableName: String): DiffStat! + diffSummaries(databaseName: String!, fromRefName: String!, toRefName: String!, refName: String, type: CommitDiffType, tableName: String): [DiffSummary!]! docs(databaseName: String!, refName: String!): DocList! docOrDefaultDoc(refName: String!, databaseName: String!, docType: DocType): Doc + rowDiffs(offset: Int, databaseName: String!, fromCommitId: String!, toCommitId: String!, refName: String, tableName: String!, filterByRowType: DiffRowType): RowDiffList! rows(refName: String!, databaseName: String!, tableName: String!, offset: Int): RowList! views(databaseName: String!, refName: String!): RowList! + schemaDiff(databaseName: String!, fromCommitId: String!, toCommitId: String!, refName: String, tableName: String!): SchemaDiff sqlSelect(refName: String!, databaseName: String!, queryString: String!): SqlSelect! sqlSelectForCsvDownload(refName: String!, databaseName: String!, queryString: String!): String! status(databaseName: String!, refName: String!): [Status!]! @@ -190,12 +243,25 @@ enum SortBranchesBy { LastUpdated } +enum CommitDiffType { + TwoDot + ThreeDot + Unspecified +} + enum DocType { Unspecified Readme License } +enum DiffRowType { + Added + Removed + Modified + All +} + type Mutation { createBranch(databaseName: String!, newBranchName: String!, fromRefName: String!): Branch! deleteBranch(databaseName: String!, branchName: String!): Boolean! diff --git a/packages/graphql-server/src/commits/commit.resolver.ts b/packages/graphql-server/src/commits/commit.resolver.ts index d3903e7f..9ce0aaf1 100644 --- a/packages/graphql-server/src/commits/commit.resolver.ts +++ b/packages/graphql-server/src/commits/commit.resolver.ts @@ -8,11 +8,11 @@ import { doltLogsQuery } from "./commit.queries"; @ArgsType() export class ListCommitsArgs extends DBArgsWithOffset { // either refName or afterCommitId must be set - @Field() - refName: string; + @Field({ nullable: true }) + refName?: string; - // @Field({ nullable: true }) - // afterCommitId?: string; + @Field({ nullable: true }) + afterCommitId?: string; // @Field(_type => Boolean, { nullable: true }) // twoDot?: boolean; @@ -33,13 +33,12 @@ export class CommitResolver { @Args() args: ListCommitsArgs, ): Promise { + const err = handleArgsErr(args); + if (err) throw err; + const refName = args.refName ?? args.afterCommitId ?? ""; const offset = args.offset ?? 0; return this.dss.query(async query => { - const logs = await query(doltLogsQuery, [ - args.refName, - ROW_LIMIT + 1, - offset, - ]); + const logs = await query(doltLogsQuery, [refName, ROW_LIMIT + 1, offset]); return getCommitListRes(logs, args); }, args.databaseName); } @@ -53,3 +52,27 @@ function getCommitListRes(logs: RawRow[], args: ListCommitsArgs): CommitList { nextOffset: getNextOffset(logs.length, args.offset ?? 0), }; } + +function handleArgsErr(args: ListCommitsArgs): Error | undefined { + if (!args.refName && !args.afterCommitId) { + return new Error( + "must supply either `refName` or `afterCommitId` to list commits", + ); + } + if (args.refName && args.afterCommitId) { + return new Error( + "cannot supply both `refName` and `afterCommitId` when listing commits", + ); + } + // if (args.twoDot && !args.excludingCommitsFromRefName) { + // return new Error( + // "must supply `excludingCommitsFromRefName` if twoDot is true", + // ); + // } + // if (!args.twoDot && args.excludingCommitsFromRefName) { + // return new Error( + // "cannot supply `excludingCommitsFromRefName` if twoDot is not provided or false", + // ); + // } + return undefined; +} diff --git a/packages/graphql-server/src/diffStats/diffStat.model.ts b/packages/graphql-server/src/diffStats/diffStat.model.ts new file mode 100644 index 00000000..cea0e69f --- /dev/null +++ b/packages/graphql-server/src/diffStats/diffStat.model.ts @@ -0,0 +1,54 @@ +import { Field, Float, ObjectType } from "@nestjs/graphql"; +import { RawRow, RawRows } from "../utils/commonTypes"; + +@ObjectType() +export class DiffStat { + @Field(_type => Float) + rowsUnmodified: number; + + @Field(_type => Float) + rowsAdded: number; + + @Field(_type => Float) + rowsDeleted: number; + + @Field(_type => Float) + rowsModified: number; + + @Field(_type => Float) + cellsModified: number; + + @Field(_type => Float) + rowCount: number; + + @Field(_type => Float) + cellCount: number; +} + +const defaultStat = { + rowsUnmodified: 0, + rowsAdded: 0, + rowsDeleted: 0, + rowsModified: 0, + cellsModified: 0, + rowCount: 0, + cellCount: 0, +}; + +export function fromDoltDiffStat(res: RawRows): DiffStat { + if (!res.length) return defaultStat; + + const reduced = res.reduce((acc: DiffStat, row: RawRow) => { + return { + rowsUnmodified: Number(row.rows_unmodified) + acc.rowsUnmodified, + rowsAdded: Number(row.rows_added) + acc.rowsAdded, + rowsDeleted: Number(row.rows_deleted) + acc.rowsDeleted, + rowsModified: Number(row.rows_modified) + acc.rowsModified, + cellsModified: Number(row.cells_modified) + acc.cellsModified, + rowCount: Number(row.new_row_count) + acc.rowCount, + cellCount: Number(row.new_cell_count) + acc.cellCount, + }; + }, defaultStat); + + return reduced; +} diff --git a/packages/graphql-server/src/diffStats/diffStat.queries.ts b/packages/graphql-server/src/diffStats/diffStat.queries.ts new file mode 100644 index 00000000..e00546c6 --- /dev/null +++ b/packages/graphql-server/src/diffStats/diffStat.queries.ts @@ -0,0 +1,5 @@ +export const getThreeDotDiffStatQuery = (hasTableName?: boolean): string => + `SELECT * FROM DOLT_DIFF_STAT(?${hasTableName ? `, ?` : ""})`; + +export const getDiffStatQuery = (hasTableName?: boolean): string => + `SELECT * FROM DOLT_DIFF_STAT(?, ?${hasTableName ? `, ?` : ""})`; diff --git a/packages/graphql-server/src/diffStats/diffStat.resolver.ts b/packages/graphql-server/src/diffStats/diffStat.resolver.ts new file mode 100644 index 00000000..ab68432c --- /dev/null +++ b/packages/graphql-server/src/diffStats/diffStat.resolver.ts @@ -0,0 +1,67 @@ +import { Args, ArgsType, Field, Query, Resolver } from "@nestjs/graphql"; +import { DataSourceService } from "../dataSources/dataSource.service"; +import { CommitDiffType } from "../diffSummaries/diffSummary.enums"; +import { DBArgs } from "../utils/commonTypes"; +import { DiffStat, fromDoltDiffStat } from "./diffStat.model"; +import { getDiffStatQuery, getThreeDotDiffStatQuery } from "./diffStat.queries"; + +@ArgsType() +export class DiffStatArgs extends DBArgs { + @Field() + fromRefName: string; + + @Field() + toRefName: string; + + @Field({ nullable: true }) + refName?: string; + + @Field(_type => CommitDiffType, { nullable: true }) + type?: CommitDiffType; + + @Field({ nullable: true }) + tableName?: string; +} + +@Resolver(_of => DiffStat) +export class DiffStatResolver { + constructor(private readonly dss: DataSourceService) {} + + @Query(_returns => DiffStat) + async diffStat(@Args() args: DiffStatArgs): Promise { + const type = args.type ?? CommitDiffType.TwoDot; + checkArgs(args); + + return this.dss.query(async query => { + if (type === CommitDiffType.ThreeDot) { + const res = await query(getThreeDotDiffStatQuery(!!args.tableName), [ + `${args.toRefName}...${args.fromRefName}`, + args.tableName, + ]); + return fromDoltDiffStat(res); + } + + const res = await query(getDiffStatQuery(!!args.tableName), [ + args.fromRefName, + args.toRefName, + args.tableName, + ]); + return fromDoltDiffStat(res); + }, args.databaseName); + } +} + +export function checkArgs(args: DiffStatArgs): void { + if ( + args.type === CommitDiffType.TwoDot && + (isRefKeyword(args.fromRefName) || isRefKeyword(args.toRefName)) && + !args.refName + ) { + throw new Error("refName is required for TwoDot diff with ref keyword"); + } +} + +function isRefKeyword(refName: string): boolean { + const upper = refName.toUpperCase(); + return upper === "WORKING" || upper === "HEAD" || upper === "STAGED"; +} diff --git a/packages/graphql-server/src/diffSummaries/diffSummary.enums.ts b/packages/graphql-server/src/diffSummaries/diffSummary.enums.ts new file mode 100644 index 00000000..e2df3b1e --- /dev/null +++ b/packages/graphql-server/src/diffSummaries/diffSummary.enums.ts @@ -0,0 +1,33 @@ +import { registerEnumType } from "@nestjs/graphql"; + +export enum TableDiffType { + Added, + Dropped, + Modified, + Renamed, +} + +registerEnumType(TableDiffType, { name: "TableDiffType" }); + +export function toTableDiffType(t: string): TableDiffType { + switch (t) { + case "added": + return TableDiffType.Added; + case "dropped": + return TableDiffType.Dropped; + case "modified": + return TableDiffType.Modified; + case "renamed": + return TableDiffType.Renamed; + default: + throw new Error(`Unknown table diff type: ${t}`); + } +} + +export enum CommitDiffType { + TwoDot, + ThreeDot, + Unspecified, +} + +registerEnumType(CommitDiffType, { name: "CommitDiffType" }); diff --git a/packages/graphql-server/src/diffSummaries/diffSummary.model.ts b/packages/graphql-server/src/diffSummaries/diffSummary.model.ts new file mode 100644 index 00000000..5c15de49 --- /dev/null +++ b/packages/graphql-server/src/diffSummaries/diffSummary.model.ts @@ -0,0 +1,53 @@ +import { Field, ID, ObjectType } from "@nestjs/graphql"; +import { RawRow } from "../utils/commonTypes"; +import { TableDiffType, toTableDiffType } from "./diffSummary.enums"; + +@ObjectType() +export class DiffSummary { + @Field(_type => ID) + _id: string; + + @Field() + fromTableName: string; + + @Field() + toTableName: string; + + @Field() + tableName: string; + + @Field(_type => TableDiffType) + tableType: TableDiffType; + + @Field() + hasDataChanges: boolean; + + @Field() + hasSchemaChanges: boolean; +} + +export function fromDoltDiffSummary(row: RawRow): DiffSummary { + const fromTableName = row.from_table_name; + const toTableName = row.to_table_name; + const tableName = getTableName(fromTableName, toTableName); + const _id = `tableDiffSummaries/${tableName}`; + return { + _id, + fromTableName, + toTableName, + tableName, + tableType: toTableDiffType(row.diff_type), + hasDataChanges: row.data_change, + hasSchemaChanges: row.schema_change, + }; +} + +function getTableName(fromTableName: string, toTableName: string): string { + if (!fromTableName.length && !toTableName.length) return ""; + if (!fromTableName.length) return toTableName; + if (!toTableName.length) return fromTableName; + if (fromTableName !== toTableName) { + return toTableName; + } + return toTableName; +} diff --git a/packages/graphql-server/src/diffSummaries/diffSummary.queries.ts b/packages/graphql-server/src/diffSummaries/diffSummary.queries.ts new file mode 100644 index 00000000..5fdda674 --- /dev/null +++ b/packages/graphql-server/src/diffSummaries/diffSummary.queries.ts @@ -0,0 +1,5 @@ +export const getDiffSummaryQuery = (hasTableName?: boolean): string => + `SELECT * FROM DOLT_DIFF_SUMMARY(?, ?${hasTableName ? `, ?` : ""})`; + +export const getThreeDotDiffSummaryQuery = (hasTableName?: boolean): string => + `SELECT * FROM DOLT_DIFF_SUMMARY(?${hasTableName ? `, ?` : ""})`; diff --git a/packages/graphql-server/src/diffSummaries/diffSummary.resolver.ts b/packages/graphql-server/src/diffSummaries/diffSummary.resolver.ts new file mode 100644 index 00000000..d90a3268 --- /dev/null +++ b/packages/graphql-server/src/diffSummaries/diffSummary.resolver.ts @@ -0,0 +1,71 @@ +import { Args, ArgsType, Field, Query, Resolver } from "@nestjs/graphql"; +import { DataSourceService, ParQuery } from "../dataSources/dataSource.service"; +import { checkArgs } from "../diffStats/diffStat.resolver"; +import { DBArgs } from "../utils/commonTypes"; +import { CommitDiffType } from "./diffSummary.enums"; +import { DiffSummary, fromDoltDiffSummary } from "./diffSummary.model"; +import { + getDiffSummaryQuery, + getThreeDotDiffSummaryQuery, +} from "./diffSummary.queries"; + +@ArgsType() +class DiffSummaryArgs extends DBArgs { + @Field() + fromRefName: string; + + @Field() + toRefName: string; + + @Field({ nullable: true }) + refName?: string; + + @Field(_type => CommitDiffType, { nullable: true }) + type?: CommitDiffType; + + @Field({ nullable: true }) + tableName?: string; +} + +@Resolver(_of => DiffSummary) +export class DiffSummaryResolver { + constructor(private readonly dss: DataSourceService) {} + + @Query(_returns => [DiffSummary]) + async diffSummaries(@Args() args: DiffSummaryArgs): Promise { + return this.dss.query( + async q => getDiffSummaries(q, args), + args.databaseName, + ); + } +} + +export async function getDiffSummaries( + query: ParQuery, + args: DiffSummaryArgs, +): Promise { + const type = args.type ?? CommitDiffType.TwoDot; + checkArgs(args); + + if (type === CommitDiffType.ThreeDot) { + const res = await query(getThreeDotDiffSummaryQuery(!!args.tableName), [ + `${args.toRefName}...${args.fromRefName}`, + args.tableName, + ]); + return res.map(fromDoltDiffSummary).sort(sortByTableName); + } + + const res = await query(getDiffSummaryQuery(!!args.tableName), [ + args.fromRefName, + args.toRefName, + args.tableName, + ]); + return res.map(fromDoltDiffSummary).sort(sortByTableName); +} + +function sortByTableName(a: DiffSummary, b: DiffSummary) { + if (a.toTableName.length && b.toTableName.length) { + return a.toTableName.localeCompare(b.toTableName); + } + return a.fromTableName.localeCompare(b.fromTableName); +} diff --git a/packages/graphql-server/src/resolvers.ts b/packages/graphql-server/src/resolvers.ts index 0520b1f8..96d21ad1 100644 --- a/packages/graphql-server/src/resolvers.ts +++ b/packages/graphql-server/src/resolvers.ts @@ -1,8 +1,12 @@ import { BranchResolver } from "./branches/branch.resolver"; import { CommitResolver } from "./commits/commit.resolver"; import { DatabaseResolver } from "./databases/database.resolver"; +import { DiffStatResolver } from "./diffStats/diffStat.resolver"; +import { DiffSummaryResolver } from "./diffSummaries/diffSummary.resolver"; import { DocsResolver } from "./docs/doc.resolver"; +import { RowDiffResolver } from "./rowDiffs/rowDiff.resolver"; import { RowResolver } from "./rows/row.resolver"; +import { SchemaDiffResolver } from "./schemaDiffs/schemaDiff.resolver"; import { SqlSelectResolver } from "./sqlSelects/sqlSelect.resolver"; import { StatusResolver } from "./status/status.resolver"; import { TableResolver } from "./tables/table.resolver"; @@ -13,9 +17,13 @@ const resolvers = [ BranchResolver, CommitResolver, DatabaseResolver, + DiffStatResolver, + DiffSummaryResolver, DocsResolver, FileUploadResolver, + RowDiffResolver, RowResolver, + SchemaDiffResolver, SqlSelectResolver, StatusResolver, TableResolver, diff --git a/packages/graphql-server/src/rowDiffs/rowDiff.enums.ts b/packages/graphql-server/src/rowDiffs/rowDiff.enums.ts new file mode 100644 index 00000000..0b21168d --- /dev/null +++ b/packages/graphql-server/src/rowDiffs/rowDiff.enums.ts @@ -0,0 +1,24 @@ +import { registerEnumType } from "@nestjs/graphql"; + +export enum DiffRowType { + Added, + Removed, + Modified, + All, +} + +registerEnumType(DiffRowType, { name: "DiffRowType" }); + +export function convertToStringForQuery(t?: DiffRowType): string | undefined { + if (t === undefined) return undefined; + switch (t) { + case DiffRowType.Added: + return "added"; + case DiffRowType.Modified: + return "modified"; + case DiffRowType.Removed: + return "removed"; + default: + return undefined; + } +} diff --git a/packages/graphql-server/src/rowDiffs/rowDiff.model.ts b/packages/graphql-server/src/rowDiffs/rowDiff.model.ts new file mode 100644 index 00000000..c22f62f0 --- /dev/null +++ b/packages/graphql-server/src/rowDiffs/rowDiff.model.ts @@ -0,0 +1,96 @@ +import { Field, ObjectType } from "@nestjs/graphql"; +import * as columns from "../columns/column.model"; +import { Column } from "../columns/column.model"; +import * as row from "../rows/row.model"; +import { ROW_LIMIT, getNextOffset } from "../utils"; +import { ListOffsetRes, RawRow } from "../utils/commonTypes"; +import { DiffRowType } from "./rowDiff.enums"; +import { canShowDroppedOrAddedRows } from "./utils"; + +@ObjectType() +export class RowDiff { + @Field(_type => row.Row, { nullable: true }) + added?: row.Row; + + @Field(_type => row.Row, { nullable: true }) + deleted?: row.Row; +} + +@ObjectType() +export class RowDiffList extends ListOffsetRes { + @Field(_type => [RowDiff]) + list: RowDiff[]; + + @Field(_type => [Column]) + columns: Column[]; +} + +@ObjectType() +export class RowListWithCols extends row.RowList { + @Field(_type => [columns.Column]) + columns: columns.Column[]; +} + +export function fromRowDiffRowsWithCols( + cols: Column[], + diffs: RawRow[], + offset: number, +): RowDiffList { + const rowDiffsList: RowDiff[] = diffs.map(rd => { + const addedVals: Array = []; + const deletedVals: Array = []; + cols.forEach(c => { + addedVals.push(rd[`to_${c.name}`]); + deletedVals.push(rd[`from_${c.name}`]); + }); + + return { added: getDiffRow(addedVals), deleted: getDiffRow(deletedVals) }; + }); + + return { + list: rowDiffsList.slice(0, ROW_LIMIT), + nextOffset: getNextOffset(rowDiffsList.length, offset), + columns: cols, + }; +} + +export function fromDoltListRowWithColsRes( + rows: RawRow[], + cols: RawRow[], + offset: number, +): RowListWithCols { + return { + list: rows.slice(0, ROW_LIMIT).map(row.fromDoltRowRes), + nextOffset: getNextOffset(rows.length, offset), + columns: cols.map(columns.fromDoltRowRes), + }; +} + +export function fromOneSidedTable( + rows: RowListWithCols, + type: "added" | "dropped", + filter?: DiffRowType, +): RowDiffList { + const emptyList = { list: [], columns: [] }; + if (!canShowDroppedOrAddedRows(type, filter)) { + return emptyList; + } + return { + list: rows.list.map(r => + type === "added" ? { added: r } : { deleted: r }, + ), + nextOffset: rows.nextOffset, + columns: rows.columns, + }; +} + +function getDiffRow( + vals: Array, +): row.Row | undefined { + if (vals.every(v => v === null || v === undefined)) return undefined; + return { + columnValues: vals.map(v => { + return { displayValue: row.getCellValue(v) }; + }), + }; +} diff --git a/packages/graphql-server/src/rowDiffs/rowDiff.queries.ts b/packages/graphql-server/src/rowDiffs/rowDiff.queries.ts new file mode 100644 index 00000000..649784b0 --- /dev/null +++ b/packages/graphql-server/src/rowDiffs/rowDiff.queries.ts @@ -0,0 +1,38 @@ +import { getOrderByFromCols, getPKColsForRowsQuery } from "../rows/row.queries"; +import { RawRows } from "../utils/commonTypes"; + +export const getRowsQueryAsOf = ( + columns: RawRows, +): { q: string; cols: string[] } => { + const cols = getPKColsForRowsQuery(columns); + return { + q: `SELECT * FROM ?? AS OF ? ${getOrderByFromCols( + cols.length, + )}LIMIT ? OFFSET ?`, + cols, + }; +}; + +export const tableColsQueryAsOf = `DESCRIBE ?? AS OF ?`; + +export function getTableCommitDiffQuery( + cols: RawRows, + hasFilter = false, +): string { + const whereDiffType = hasFilter ? ` WHERE diff_type=? ` : ""; + return `SELECT * FROM DOLT_DIFF(?, ?, ?)${whereDiffType} + ${getOrderByFromDiffCols(cols)} + LIMIT ? + OFFSET ?`; +} + +export function getOrderByFromDiffCols(cols: RawRows): string { + const pkCols = cols.filter(col => col.Key === "PRI"); + const diffCols: string[] = []; + pkCols.forEach(col => { + diffCols.push(`to_${col.Field}`); + diffCols.push(`from_${col.Field}`); + }); + const orderBy = diffCols.map(c => `\`${c}\` ASC`).join(", "); + return orderBy === "" ? "" : `ORDER BY ${orderBy} `; +} diff --git a/packages/graphql-server/src/rowDiffs/rowDiff.resolver.ts b/packages/graphql-server/src/rowDiffs/rowDiff.resolver.ts new file mode 100644 index 00000000..d41ff59d --- /dev/null +++ b/packages/graphql-server/src/rowDiffs/rowDiff.resolver.ts @@ -0,0 +1,133 @@ +import { Args, ArgsType, Field, Query, Resolver } from "@nestjs/graphql"; +import * as column from "../columns/column.model"; +import { DataSourceService, ParQuery } from "../dataSources/dataSource.service"; +import { TableDiffType } from "../diffSummaries/diffSummary.enums"; +import { getDiffSummaries } from "../diffSummaries/diffSummary.resolver"; +import { ListRowsArgs } from "../rows/row.resolver"; +import { ROW_LIMIT } from "../utils"; +import { DBArgsWithOffset } from "../utils/commonTypes"; +import { DiffRowType, convertToStringForQuery } from "./rowDiff.enums"; +import { + RowDiff, + RowDiffList, + fromDoltListRowWithColsRes, + fromOneSidedTable, + fromRowDiffRowsWithCols, +} from "./rowDiff.model"; +import { + getRowsQueryAsOf, + getTableCommitDiffQuery, + tableColsQueryAsOf, +} from "./rowDiff.queries"; +import { unionCols } from "./utils"; + +@ArgsType() +class ListRowDiffsArgs extends DBArgsWithOffset { + // Uses resolved commits + @Field() + fromCommitId: string; + + @Field() + toCommitId: string; + + @Field({ nullable: true }) + refName?: string; + + @Field() + tableName: string; + + @Field(_type => DiffRowType, { nullable: true }) + filterByRowType?: DiffRowType; +} + +@Resolver(_of => RowDiff) +export class RowDiffResolver { + constructor(private readonly dss: DataSourceService) {} + + @Query(_returns => RowDiffList) + async rowDiffs( + @Args() + { databaseName, tableName, refName, ...args }: ListRowDiffsArgs, + ): Promise { + const dbArgs = { databaseName, refName }; + const offset = args.offset ?? 0; + + return this.dss.query( + async query => { + const ds = await getDiffSummaries(query, { + ...dbArgs, + tableName, + fromRefName: args.fromCommitId, + toRefName: args.toCommitId, + }); + if (!ds.length) { + throw new Error(`Could not get summary for table "${tableName}"`); + } + + const { tableType, fromTableName, toTableName } = ds[0]; + + if (tableType === TableDiffType.Dropped) { + const rows = await getRowsForDiff(query, { + ...dbArgs, + tableName, + refName: args.fromCommitId, + }); + return fromOneSidedTable(rows, "dropped", args.filterByRowType); + } + if (tableType === TableDiffType.Added) { + const rows = await getRowsForDiff(query, { + ...dbArgs, + tableName, + refName: args.toCommitId, + }); + return fromOneSidedTable(rows, "added", args.filterByRowType); + } + + const oldCols = await query(tableColsQueryAsOf, [ + fromTableName, + args.fromCommitId, + ]); + const newCols = await query(tableColsQueryAsOf, [ + toTableName, + args.toCommitId, + ]); + + const colsUnion = unionCols( + oldCols.map(column.fromDoltRowRes), + newCols.map(column.fromDoltRowRes), + ); + + const diffType = convertToStringForQuery(args.filterByRowType); + const refArgs = [args.fromCommitId, args.toCommitId, tableName]; + const pageArgs = [ROW_LIMIT + 1, offset]; + const diffs = await query( + getTableCommitDiffQuery(colsUnion, !!diffType), + diffType + ? [...refArgs, diffType, ...pageArgs] + : [...refArgs, ...pageArgs], + ); + + return fromRowDiffRowsWithCols(colsUnion, diffs, offset); + }, + databaseName, + refName, + ); + } +} + +async function getRowsForDiff(query: ParQuery, args: ListRowsArgs) { + const columns = await query(tableColsQueryAsOf, [ + args.tableName, + args.refName, + ]); + const offset = args.offset ?? 0; + const { q, cols } = getRowsQueryAsOf(columns); + const rows = await query(q, [ + args.tableName, + args.refName, + ...cols, + ROW_LIMIT + 1, + offset, + ]); + return fromDoltListRowWithColsRes(rows, columns, offset); +} diff --git a/packages/graphql-server/src/rowDiffs/utils.ts b/packages/graphql-server/src/rowDiffs/utils.ts new file mode 100644 index 00000000..c2997d9b --- /dev/null +++ b/packages/graphql-server/src/rowDiffs/utils.ts @@ -0,0 +1,25 @@ +import { Column } from "../columns/column.model"; +import { DiffRowType } from "./rowDiff.enums"; + +export function canShowDroppedOrAddedRows( + type: "added" | "dropped", + filter?: DiffRowType, +): boolean { + if (filter === undefined || filter === DiffRowType.All) return true; + if (type === "added" && filter === DiffRowType.Added) return true; + if (type === "dropped" && filter === DiffRowType.Removed) return true; + return false; +} + +export function unionCols(a: Column[], b: Column[]): Column[] { + const mergedArray = [...a, ...b]; + const set = new Set(); + const unionArray = mergedArray.filter(item => { + if (!set.has(item.name)) { + set.add(item.name); + return true; + } + return false; + }, set); + return unionArray; +} diff --git a/packages/graphql-server/src/rows/row.queries.ts b/packages/graphql-server/src/rows/row.queries.ts index e2b5664c..4cda1897 100644 --- a/packages/graphql-server/src/rows/row.queries.ts +++ b/packages/graphql-server/src/rows/row.queries.ts @@ -10,7 +10,7 @@ export const getRowsQuery = ( }; }; -function getPKColsForRowsQuery(cs: RawRows): string[] { +export function getPKColsForRowsQuery(cs: RawRows): string[] { const pkCols = cs.filter(col => col.Key === "PRI"); const cols = pkCols.map(c => c.Field); return cols; @@ -18,7 +18,7 @@ function getPKColsForRowsQuery(cs: RawRows): string[] { // Creates ORDER BY statement with column parameters // i.e. ORDER BY ::col1, ::col2 -function getOrderByFromCols(numCols: number): string { +export function getOrderByFromCols(numCols: number): string { if (!numCols) return ""; const pkCols = Array.from({ length: numCols }) .map(() => `? ASC`) diff --git a/packages/graphql-server/src/schemaDiffs/schemaDiff.model.ts b/packages/graphql-server/src/schemaDiffs/schemaDiff.model.ts new file mode 100644 index 00000000..75697cff --- /dev/null +++ b/packages/graphql-server/src/schemaDiffs/schemaDiff.model.ts @@ -0,0 +1,22 @@ +import { Field, Int, ObjectType } from "@nestjs/graphql"; + +@ObjectType() +export class TextDiff { + @Field() + leftLines: string; + + @Field() + rightLines: string; +} + +@ObjectType() +export class SchemaDiff { + @Field(_type => TextDiff, { nullable: true }) + schemaDiff?: TextDiff; + + @Field(_type => [String], { nullable: true }) + schemaPatch?: string[]; + + @Field(_type => Int, { nullable: true }) + numChangedSchemas?: number; +} diff --git a/packages/graphql-server/src/schemaDiffs/schemaDiff.queries.ts b/packages/graphql-server/src/schemaDiffs/schemaDiff.queries.ts new file mode 100644 index 00000000..45eda8c0 --- /dev/null +++ b/packages/graphql-server/src/schemaDiffs/schemaDiff.queries.ts @@ -0,0 +1,3 @@ +export const schemaPatchQuery = `SELECT * FROM DOLT_PATCH(?, ?, ?) WHERE diff_type="schema"`; + +export const schemaDiffQuery = `SELECT * FROM DOLT_SCHEMA_DIFF(?, ?, ?)`; diff --git a/packages/graphql-server/src/schemaDiffs/schemaDiff.resolver.ts b/packages/graphql-server/src/schemaDiffs/schemaDiff.resolver.ts new file mode 100644 index 00000000..e6e1abc1 --- /dev/null +++ b/packages/graphql-server/src/schemaDiffs/schemaDiff.resolver.ts @@ -0,0 +1,55 @@ +import { Args, ArgsType, Field, Query, Resolver } from "@nestjs/graphql"; +import { DataSourceService } from "../dataSources/dataSource.service"; +import { DBArgs } from "../utils/commonTypes"; +import { SchemaDiff } from "./schemaDiff.model"; +import { schemaDiffQuery, schemaPatchQuery } from "./schemaDiff.queries"; + +@ArgsType() +class SchemaDiffArgs extends DBArgs { + // Uses resolved commits + @Field() + fromCommitId: string; + + @Field() + toCommitId: string; + + @Field({ nullable: true }) + refName?: string; + + @Field() + tableName: string; +} + +@Resolver(_of => SchemaDiff) +export class SchemaDiffResolver { + constructor(private readonly dss: DataSourceService) {} + + @Query(_returns => SchemaDiff, { nullable: true }) + async schemaDiff( + @Args() args: SchemaDiffArgs, + ): Promise { + const commitArgs = [args.fromCommitId, args.toCommitId, args.tableName]; + + return this.dss.query( + async query => { + const res = await query(schemaPatchQuery, commitArgs); + const schemaPatch = res.map(r => r.statement); + + const diffRes = await query(schemaDiffQuery, commitArgs); + const schemaDiff = diffRes.length + ? { + leftLines: diffRes[0].from_create_statement, + rightLines: diffRes[0].to_create_statement, + } + : undefined; + + return { + schemaDiff, + schemaPatch, + }; + }, + args.databaseName, + args.refName, + ); + } +} diff --git a/packages/web/components/CellButtons/HideDiffColumnButton.tsx b/packages/web/components/CellButtons/HideDiffColumnButton.tsx new file mode 100644 index 00000000..9803f60c --- /dev/null +++ b/packages/web/components/CellButtons/HideDiffColumnButton.tsx @@ -0,0 +1,14 @@ +import Button from "@components/Button"; +import css from "./index.module.css"; + +type Props = { + onClick: () => void; +}; + +export default function HideDiffColumn(props: Props) { + return ( + + Hide Column + + ); +} diff --git a/packages/web/components/CellButtons/HistoryButton.tsx b/packages/web/components/CellButtons/HistoryButton.tsx new file mode 100644 index 00000000..8b6f9a88 --- /dev/null +++ b/packages/web/components/CellButtons/HistoryButton.tsx @@ -0,0 +1,152 @@ +import Button from "@components/Button"; +import Loader from "@components/Loader"; +import useViewList from "@components/Views/useViewList"; +import { useDataTableContext } from "@contexts/dataTable"; +import { useSqlEditorContext } from "@contexts/sqleditor"; +import { + ColumnForDataTableFragment, + RowForDataTableFragment, +} from "@gen/graphql-types"; +import { isDoltSystemTable } from "@lib/doltSystemTables"; +import { TableParams } from "@lib/params"; +import { parseSelectQuery } from "@lib/parseSqlQuery"; +import { BsFillQuestionCircleFill } from "@react-icons/all-files/bs/BsFillQuestionCircleFill"; +import cx from "classnames"; +import { ReactNode, useEffect, useState } from "react"; +import TableType from "./TableType"; +import css from "./index.module.css"; +import { getDoltDiffQuery } from "./queryHelpers"; +import { getTableColsFromQueryCols, isKeyless, queryShowingPKs } from "./utils"; + +type Props = { + cidx: number; + row: RowForDataTableFragment; + columns: ColumnForDataTableFragment[]; + doltDisabled?: boolean; +}; + +type InnerProps = Omit & { + disabled?: boolean; + disabledPopup?: ReactNode; + params: TableParams; +}; + +function Inner(props: InnerProps) { + const currCol = props.columns[props.cidx]; + const { executeQuery } = useSqlEditorContext(); + const [submitting, setSubmitting] = useState(false); + const isPK = currCol.isPrimaryKey; + + useEffect(() => { + if (!submitting) { + return; + } + const query = getDoltDiffQuery({ ...props, row: props.row, isPK }); + executeQuery({ ...props.params, query }).catch(console.error); + setSubmitting(false); + }, [submitting, props.params, executeQuery, isPK, props]); + + return ( +
+ + setSubmitting(true)} + className={css.button} + disabled={props.disabled} + > + {isPK ? "Row History" : "Cell History"} + {props.disabled && } + +
{props.disabledPopup}
+
+ ); +} + +export default function HistoryButton(props: Props): JSX.Element | null { + const { params, columns } = useDataTableContext(); + const { tableName } = params; + const { views, loading } = useViewList(params); + + if (loading) { + return ( +
Loading history...
+ ); + } + + if (!tableName) return null; + + const keyless = isKeyless(columns); + const isView = getIsView(tableName, views); + const isSystemTable = isDoltSystemTable(tableName); + const pksShowing = queryShowingPKs(props.columns, columns); + const isJoin = queryHasMultipleTables(params.q); + + const disabled = + props.doltDisabled || + keyless || + isView || + isSystemTable || + !columns || + // Need values of all PK columns to generate query for history + !pksShowing || + // History will not work for joins + isJoin; + + return ( + + + History not available{" "} + + + + } + /> + ); +} + +function getIsView( + tableName: string, + views?: RowForDataTableFragment[], +): boolean { + if (!views) return false; + return views.some(v => v.columnValues[1].displayValue === tableName); +} + +function queryHasMultipleTables(q?: string): boolean { + if (!q) return false; + const parsed = parseSelectQuery(q); + if (!parsed?.from) return false; + return parsed.from.length > 1; +} + +type ReasonProps = { + isView: boolean; + isSystemTable: boolean; + pksShowing: boolean; + isJoin: boolean; + keyless: boolean; + doltDisabled?: boolean; +}; + +function HistoryNotAvailableReason(props: ReasonProps) { + if (props.doltDisabled) return for non-Dolt databases.; + if (props.keyless) return for keyless tables.; + if (props.isJoin) return for multiple tables.; + if (!props.pksShowing) return for partial primary keys.; + return ( + + ); +} diff --git a/packages/web/components/CellButtons/TableType.tsx b/packages/web/components/CellButtons/TableType.tsx new file mode 100644 index 00000000..6e9e68aa --- /dev/null +++ b/packages/web/components/CellButtons/TableType.tsx @@ -0,0 +1,30 @@ +import DocsLink from "@components/links/DocsLink"; + +type Props = { + isView: boolean; + isDoltSystemTable: boolean; +}; + +export default function TableType({ + isView, + isDoltSystemTable = false, +}: Props): JSX.Element | null { + if (isView) { + return ( + + for Views + + ); + } + if (isDoltSystemTable) { + return ( + + for{" "} + + Dolt system tables + + + ); + } + return null; +} diff --git a/packages/web/components/DataTable/AddRowsButton/index.tsx b/packages/web/components/DataTable/AddRowsButton/index.tsx index d42f5779..d4d1054e 100644 --- a/packages/web/components/DataTable/AddRowsButton/index.tsx +++ b/packages/web/components/DataTable/AddRowsButton/index.tsx @@ -1,5 +1,4 @@ import Link from "@components/links/Link"; - import HideForNoWritesWrapper from "@components/util/HideForNoWritesWrapper"; import { TableParams } from "@lib/params"; import { editTable } from "@lib/urls"; diff --git a/packages/web/components/DataTable/Table/Body.tsx b/packages/web/components/DataTable/Table/Body.tsx index f23c097c..de52cf82 100644 --- a/packages/web/components/DataTable/Table/Body.tsx +++ b/packages/web/components/DataTable/Table/Body.tsx @@ -1,4 +1,8 @@ -import { isKeyless, queryShowingPKs } from "@components/CellButtons/utils"; +import { + getTableColsFromQueryCols, + isKeyless, + queryShowingPKs, +} from "@components/CellButtons/utils"; import { useDataTableContext } from "@contexts/dataTable"; import { ColumnForDataTableFragment, @@ -18,11 +22,22 @@ export default function Body(props: Props) { const { columns } = useDataTableContext(); const showRowDropdown = !isKeyless(columns) && queryShowingPKs(props.columns, columns); + const cols = getTableColsFromQueryCols(props.columns, columns); return ( - + {props.rows.map((r, ridx) => ( // eslint-disable-next-line react/jsx-key - + ))} ); diff --git a/packages/web/components/DataTable/Table/CellDropdown.tsx b/packages/web/components/DataTable/Table/CellDropdown.tsx index a3d490dc..e39c8e82 100644 --- a/packages/web/components/DataTable/Table/CellDropdown.tsx +++ b/packages/web/components/DataTable/Table/CellDropdown.tsx @@ -3,8 +3,10 @@ import CopyButton from "@components/CellButtons/CopyButton"; import EditCell from "@components/CellButtons/EditCell"; import FilterButton from "@components/CellButtons/FilterButton"; import ForeignKeyButton from "@components/CellButtons/ForeignKeyButton"; +import HistoryButton from "@components/CellButtons/HistoryButton"; import MakeNullButton from "@components/CellButtons/MakeNullButton"; import Dropdown from "@components/CellDropdown"; +import NotDoltWrapper from "@components/util/NotDoltWrapper"; import { ColumnForDataTableFragment, RowForDataTableFragment, @@ -53,12 +55,11 @@ export default function CellDropdown(props: Props) { currCol={props.currentCol} isNull={isNull} /> + + + - + {showCollapseCellButton && ( void; + loadMore: () => Promise; rows: RowForDataTableFragment[]; columns: ColumnForDataTableFragment[]; columnStatus: ColumnStatus; @@ -20,7 +20,7 @@ type Props = { }; export default function DesktopTable({ columns, rows, ...props }: Props) { - const { params } = useDataTableContext(); + const { params, showingWorkingDiff } = useDataTableContext(); return ( document.getElementById("main-content")} > - +
- +
{cols.map((c, i) => ( diff --git a/packages/web/components/DataTable/Table/HeadCell.tsx b/packages/web/components/DataTable/Table/HeadCell.tsx index a3c391bf..1357f89d 100644 --- a/packages/web/components/DataTable/Table/HeadCell.tsx +++ b/packages/web/components/DataTable/Table/HeadCell.tsx @@ -39,6 +39,9 @@ export default function HeadCell({ className={cx(css.cell, { [css.active]: showDropdown, })} + data-cy={`${isMobile ? "mobile-" : "desktop-"}db-data-table-column-${ + col.name + }`} > {col.name} {col.isPrimaryKey && } @@ -46,6 +49,7 @@ export default function HeadCell({ showDropdown={showDropdown} setShowDropdown={setShowDropdown} buttonClassName={css.menu} + data-cy={`${col.name}-column-button-dropdown`} isMobile={isMobile} > diff --git a/packages/web/components/DataTable/Table/MobileTable.tsx b/packages/web/components/DataTable/Table/MobileTable.tsx index 05dfa716..3d2fa22a 100644 --- a/packages/web/components/DataTable/Table/MobileTable.tsx +++ b/packages/web/components/DataTable/Table/MobileTable.tsx @@ -20,7 +20,7 @@ type Props = { export default function MobileTable({ columns, rows, ...props }: Props) { return (
- +
{ - setDisplayCellVal(getCellValue(value, colType, cellStatus)); + const val = getCellValue(value, colType, cellStatus); + setDisplayCellVal(val); }, [cellStatus]); return { displayCellVal, setDisplayCellVal, cellStatus, setCellStatus }; @@ -58,6 +59,7 @@ function getCellValue( return value; } } + if (colType === "bit(1)") { return getBitDisplayValue(value); } diff --git a/packages/web/components/DataTable/Table/utils.test.ts b/packages/web/components/DataTable/Table/utils.test.ts index 5751b3ab..45cb68d5 100644 --- a/packages/web/components/DataTable/Table/utils.test.ts +++ b/packages/web/components/DataTable/Table/utils.test.ts @@ -294,8 +294,8 @@ describe("test getDeleteRowQuery", () => { describe("test getFilterByCellQuery", () => { const refParams = { + databaseName: "dbname", refName: "master", - databaseName: "test", }; const tests: Array<{ desc: string; diff --git a/packages/web/components/DataTable/index.tsx b/packages/web/components/DataTable/index.tsx index 792fe0f4..681c2938 100644 --- a/packages/web/components/DataTable/index.tsx +++ b/packages/web/components/DataTable/index.tsx @@ -4,10 +4,10 @@ import Loader from "@components/Loader"; import { useDataTableContext } from "@contexts/dataTable"; import { ColumnForDataTableFragment, - Maybe, RowForDataTableFragment, } from "@gen/graphql-types"; import DataTableLayout from "@layouts/DataTableLayout"; +import Maybe from "@lib/Maybe"; import { RefParams, SqlQueryParams, TableParams } from "@lib/params"; import { ReactNode } from "react"; import AddRowsButton from "./AddRowsButton"; diff --git a/packages/web/components/DatabaseOptionsDropdown/index.tsx b/packages/web/components/DatabaseOptionsDropdown/index.tsx index a68333cd..457b7266 100644 --- a/packages/web/components/DatabaseOptionsDropdown/index.tsx +++ b/packages/web/components/DatabaseOptionsDropdown/index.tsx @@ -4,6 +4,8 @@ import useEffectOnMount from "@hooks/useEffectOnMount"; import fakeEscapePress from "@lib/fakeEscapePress"; import { SqlQueryParams } from "@lib/params"; import { isMutation } from "@lib/parseSqlQuery"; +import { CgArrowsH } from "@react-icons//all-files/cg/CgArrowsH"; +import { CgCompress } from "@react-icons//all-files/cg/CgCompress"; import { FaCaretDown } from "@react-icons/all-files/fa/FaCaretDown"; import { FaCaretUp } from "@react-icons/all-files/fa/FaCaretUp"; import { RiFileDownloadLine } from "@react-icons/all-files/ri/RiFileDownloadLine"; @@ -13,15 +15,15 @@ import CsvModal from "./CsvModal"; import css from "./index.module.css"; type Props = { - // onClickHideUnchangedCol?: () => void; - // showingHideUnchangedCol?: boolean; + onClickHideUnchangedCol?: () => void; + showingHideUnchangedCol?: boolean; children?: JSX.Element | null; className?: string; params?: SqlQueryParams; }; export default function DatabaseOptionsDropdown({ - // onClickHideUnchangedCol, + onClickHideUnchangedCol, ...props }: Props): JSX.Element | null { const [modalOpen, setModalOpen] = useState(false); @@ -32,7 +34,7 @@ export default function DatabaseOptionsDropdown({ return () => document.removeEventListener("wheel", fakeEscapePress); }); - if (!props.children && !props.params) return null; + if (!onClickHideUnchangedCol && !props.children && !props.params) return null; if (props.params && isMutation(props.params.q)) return null; return ( @@ -64,7 +66,7 @@ export default function DatabaseOptionsDropdown({ >
    - {/* {onClickHideUnchangedCol && ( + {onClickHideUnchangedCol && ( { @@ -86,7 +88,7 @@ export default function DatabaseOptionsDropdown({ columns - )} */} + )} {props.params && ( - - + + - - + + { + return { + value: c.commitId, + label: formatCommitShort(c), + }; + }); + + const setFromCommitId = (id?: string) => { + const diffPaths = diff({ ...props.params, fromCommitId: id }); + router.push(diffPaths.href, diffPaths.as).catch(console.error); + }; + + return ( + +
    + +
    +
    + ); +} + +export default function ForBranch({ params, className }: Props) { + const res = useCommitsForDiffSelectorQuery({ variables: params }); + return ( + ( + + )} + /> + ); +} diff --git a/packages/web/components/DiffSelector/component.tsx b/packages/web/components/DiffSelector/component.tsx new file mode 100644 index 00000000..e99d56d4 --- /dev/null +++ b/packages/web/components/DiffSelector/component.tsx @@ -0,0 +1,22 @@ +import { CommitForDiffSelectorFragment } from "@gen/graphql-types"; +import excerpt from "@lib/excerpt"; +import cx from "classnames"; +import { ReactNode } from "react"; +import css from "./index.module.css"; + +type Props = { + children: ReactNode; + className?: string; +}; + +export default function DiffSelector({ className, children }: Props) { + return ( +
    + {children} +
    + ); +} + +export function formatCommitShort(c: CommitForDiffSelectorFragment) { + return `${excerpt(c.message, 32)} (${c.commitId.substring(0, 6)})`; +} diff --git a/packages/web/components/DiffSelector/index.module.css b/packages/web/components/DiffSelector/index.module.css new file mode 100644 index 00000000..e423cac5 --- /dev/null +++ b/packages/web/components/DiffSelector/index.module.css @@ -0,0 +1,7 @@ +.selector { + @apply mx-3 py-3 text-primary; +} + +.branch { + @apply mb-6; +} diff --git a/packages/web/components/DiffSelector/index.tsx b/packages/web/components/DiffSelector/index.tsx new file mode 100644 index 00000000..7bb85608 --- /dev/null +++ b/packages/web/components/DiffSelector/index.tsx @@ -0,0 +1,6 @@ +import DiffSelector from "./component"; +import ForBranch from "./ForBranch"; + +export default Object.assign(DiffSelector, { + ForBranch, +}); diff --git a/packages/web/components/DiffSelector/queries.ts b/packages/web/components/DiffSelector/queries.ts new file mode 100644 index 00000000..3466c23b --- /dev/null +++ b/packages/web/components/DiffSelector/queries.ts @@ -0,0 +1,26 @@ +import { gql } from "@apollo/client"; + +export const COMMITS_FOR_DIFF_SELECTOR = gql` + fragment CommitForDiffSelector on Commit { + _id + commitId + message + committedAt + parents + committer { + _id + displayName + username + } + } + fragment CommitListForDiffSelector on CommitList { + list { + ...CommitForDiffSelector + } + } + query CommitsForDiffSelector($refName: String!, $databaseName: String!) { + commits(refName: $refName, databaseName: $databaseName) { + ...CommitListForDiffSelector + } + } +`; diff --git a/packages/web/components/DiffStat/SummaryStat.module.css b/packages/web/components/DiffStat/SummaryStat.module.css new file mode 100644 index 00000000..1e572b95 --- /dev/null +++ b/packages/web/components/DiffStat/SummaryStat.module.css @@ -0,0 +1,31 @@ +.stat { + @apply mr-4 text-base flex; + + @screen sm { + @apply flex-col text-sm; + } +} + +.flat { + @apply block mt-1 md:mt-3.5; +} + +.statNoText { + @apply mr-3 text-sm; +} + +.num { + @apply mr-1 font-semibold text-ld-darkgrey; +} + +.green { + @apply text-acc-green; +} + +.red { + @apply text-acc-red; +} + +.blue { + @apply text-ld-mediumblue; +} diff --git a/packages/web/components/DiffStat/SummaryStat.tsx b/packages/web/components/DiffStat/SummaryStat.tsx new file mode 100644 index 00000000..752934da --- /dev/null +++ b/packages/web/components/DiffStat/SummaryStat.tsx @@ -0,0 +1,67 @@ +import { numToStringWithCommas } from "@lib/numToStringConversions"; +import cx from "classnames"; +import css from "./SummaryStat.module.css"; + +type Props = { + value?: number; + statSingle?: string; + statPlural?: string; + red?: boolean; + green?: boolean; + err?: Error; + flat?: boolean; + blue?: boolean; + loading?: boolean; +}; + +export default function SummaryStat({ + value, + statSingle, + statPlural, + err, + red = false, + green = false, + flat = false, + blue = false, + loading = false, +}: Props) { + if (loading && statPlural) { + return ( +
    {statPlural}
    + ); + } + if (err || value === undefined) { + return
    -
    ; + } + + const num = ( + + {getPlusOrMinus(green, red)} + {numToStringWithCommas(Math.abs(value))} + + ); + + if (!statSingle || !statPlural) { + return
    {num}
    ; + } + + const stat = value === 1 ? statSingle : statPlural; + return ( +
    + {num} + {stat} +
    + ); +} + +function getPlusOrMinus(green: boolean, red: boolean): string { + if (green) return "+"; + if (red) return "-"; + return ""; +} diff --git a/packages/web/components/DiffStat/SummaryStats.tsx b/packages/web/components/DiffStat/SummaryStats.tsx new file mode 100644 index 00000000..9228a42a --- /dev/null +++ b/packages/web/components/DiffStat/SummaryStats.tsx @@ -0,0 +1,71 @@ +import { ApolloError } from "@apollo/client"; +import SmallLoader from "@components/SmallLoader"; +import { DiffStatForDiffsFragment } from "@gen/graphql-types"; +import cx from "classnames"; +import SummaryStat from "./SummaryStat"; +import css from "./index.module.css"; + +type Props = { + diffStat?: DiffStatForDiffsFragment; + err?: ApolloError; + flat?: boolean; + loading?: boolean; + numSchemaChanges: number; +}; + +export default function SummaryStats({ + diffStat, + err, + flat, + loading, + numSchemaChanges, +}: Props) { + return ( +
    + {loading && ( + + )} +
    + + + + +
    +
    + ); +} diff --git a/packages/web/components/DiffStat/index.module.css b/packages/web/components/DiffStat/index.module.css new file mode 100644 index 00000000..efa2fa9e --- /dev/null +++ b/packages/web/components/DiffStat/index.module.css @@ -0,0 +1,36 @@ +.summary { + @apply relative text-sm text-primary overflow-x-auto whitespace-nowrap border-b border-ld-lightgrey mx-3 pb-5 px-2 lg:px-0; + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar { + @apply hidden; + } +} + +.forPull { + @apply border-t-0; +} + +.stats { + @apply mt-3 flex flex-col relative; + + @screen sm { + @apply flex-row; + } +} + +.loading { + @apply pr-4 ml-3 pt-2; +} + +.bold { + @apply font-semibold; +} + +.marTop { + @apply mt-5; +} + +.primaryKeyChange { + @apply text-primary font-normal; +} diff --git a/packages/web/components/DiffStat/index.tsx b/packages/web/components/DiffStat/index.tsx new file mode 100644 index 00000000..88478a50 --- /dev/null +++ b/packages/web/components/DiffStat/index.tsx @@ -0,0 +1,54 @@ +import { ApolloError } from "@apollo/client"; +import ErrorMsg from "@components/ErrorMsg"; +import { useDiffContext } from "@contexts/diff"; +import { DiffStatForDiffsFragment, useDiffStatQuery } from "@gen/graphql-types"; +import { gqlErrorPrimaryKeyChange } from "@lib/errors/graphql"; +import { errorMatches } from "@lib/errors/helpers"; +import { DiffParamsWithRefs } from "@lib/params"; +import cx from "classnames"; +import SummaryStats from "./SummaryStats"; +import css from "./index.module.css"; + +type Props = { + params: DiffParamsWithRefs & { refName?: string }; + className?: string; + flat?: boolean; +}; + +type InnerProps = Props & { + diffStat?: DiffStatForDiffsFragment; + err?: ApolloError; + loading: boolean; +}; + +function Inner(props: InnerProps) { + const { diffSummaries } = useDiffContext(); + const isPrimaryKeyChange = errorMatches(gqlErrorPrimaryKeyChange, props.err); + return ( +
    + + ds.hasSchemaChanges).length + } + /> +
    + ); +} + +export default function DiffStat(props: Props) { + const { data, loading, error } = useDiffStatQuery({ + variables: props.params, + }); + + return ( + + ); +} diff --git a/packages/web/components/DiffStat/queries.ts b/packages/web/components/DiffStat/queries.ts new file mode 100644 index 00000000..f974ab8a --- /dev/null +++ b/packages/web/components/DiffStat/queries.ts @@ -0,0 +1,32 @@ +import { gql } from "@apollo/client"; + +export const DIFF_STAT = gql` + fragment DiffStatForDiffs on DiffStat { + rowsUnmodified + rowsAdded + rowsDeleted + rowsModified + cellsModified + rowCount + cellCount + } + query DiffStat( + $databaseName: String! + $fromRefName: String! + $toRefName: String! + $refName: String + $type: CommitDiffType + $tableName: String + ) { + diffStat( + databaseName: $databaseName + fromRefName: $fromRefName + toRefName: $toRefName + refName: $refName + type: $type + tableName: $tableName + ) { + ...DiffStatForDiffs + } + } +`; diff --git a/packages/web/components/DiffTable/DataDiff/Body.tsx b/packages/web/components/DiffTable/DataDiff/Body.tsx new file mode 100644 index 00000000..d4bf8bee --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/Body.tsx @@ -0,0 +1,29 @@ +import { + ColumnForDiffTableListFragment, + RowDiffForTableListFragment, +} from "@gen/graphql-types"; +import { ColumnStatus, SetColumnStatus } from "@lib/tableTypes"; +import Row from "./Row"; +import { HiddenColIndexes } from "./utils"; + +type Props = { + cols: ColumnForDiffTableListFragment[]; + rowDiffs: RowDiffForTableListFragment[]; + hiddenColIndexes: HiddenColIndexes; + hideCellButtons?: boolean; + columnStatus: ColumnStatus; + setColumnStatus: SetColumnStatus; + userCanWrite: boolean; + refName: string; +}; + +export default function Body(props: Props) { + return ( +
+ {props.rowDiffs.map((r, ridx) => ( + /* eslint-disable-next-line react/jsx-key */ + + ))} + + ); +} diff --git a/packages/web/components/DiffTable/DataDiff/Cell.tsx b/packages/web/components/DiffTable/DataDiff/Cell.tsx new file mode 100644 index 00000000..0251f926 --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/Cell.tsx @@ -0,0 +1,223 @@ +import ChangeCellStatusButton from "@components/CellButtons/ChangeCellStatusButton"; +import CopyButton from "@components/CellButtons/CopyButton"; +import EditCell from "@components/CellButtons/EditCell"; +import MakeNullButton from "@components/CellButtons/MakeNullButton"; +import CellDropdown from "@components/CellDropdown"; +import EditCellInput from "@components/EditCellInput"; +import { + ColumnForDataTableFragment, + ColumnValueForTableListFragment, + RowDiffForTableListFragment, +} from "@gen/graphql-types"; +import { getBitDisplayValue, isLongContentType } from "@lib/dataTable"; +import excerpt from "@lib/excerpt"; +import { getDisplayValue, isNullValue } from "@lib/null"; +import { prettyJSONText } from "@lib/prettyJSON"; +import { CellStatusActionType, ColumnStatus } from "@lib/tableTypes"; +import { IoReturnDownForwardSharp } from "@react-icons/all-files/io5/IoReturnDownForwardSharp"; +import { MdKeyboardTab } from "@react-icons/all-files/md/MdKeyboardTab"; +import { MdSpaceBar } from "@react-icons/all-files/md/MdSpaceBar"; +import cx from "classnames"; +import { useEffect, useState } from "react"; +import css from "./index.module.css"; +import { getJSONDiff, getTextDiff, hideButton } from "./utils"; + +export enum CellType { + Added = "added", + Deleted = "deleted", +} + +export enum CellStatusType { + Expanded = "expanded", + Collapsed = "collapsed", +} + +type Props = { + type: CellType; + thisVal: ColumnValueForTableListFragment; + otherVal?: ColumnValueForTableListFragment; + row: RowDiffForTableListFragment; + cols: ColumnForDataTableFragment[]; + cidx: number; + ridx: number; + hideCellButtons?: boolean; + columnStatus: ColumnStatus; + refName: string; +}; + +const whitespaceRegex = /\s|\r|\t|\n/g; + +export default function Cell(props: Props) { + const thisVal = props.thisVal.displayValue; + const otherVal = props.otherVal?.displayValue; + const cellModified = thisVal !== otherVal; + const [showDropdown, setShowDropdown] = useState(false); + const [editing, setEditing] = useState(false); + const currCol = + props.cidx < props.cols.length ? props.cols[props.cidx] : undefined; + const isNull = isNullValue(thisVal); + const showChangeStatusButtons = isLongContentType(currCol?.type); + const [cellStatusAction, setCellStatusAction] = + useState(props.columnStatus[props.cidx]); + + const className = cx(css.cell, { + [css.isNull]: isNull, + [css[props.type]]: thisVal !== otherVal, + [css.longContent]: cellStatusAction !== CellStatusActionType.Collapse, + }); + + const [cellVal, setCellVal] = useState( + getCellValue(thisVal, currCol?.type, cellStatusAction, otherVal), + ); + + useEffect(() => { + setCellStatusAction(props.columnStatus[props.cidx]); + }, [props.columnStatus]); + + useEffect(() => { + if (isLongContentType(currCol?.type)) { + const val = getCellValue( + thisVal, + currCol?.type, + cellStatusAction, + otherVal, + ); + setCellVal(val); + } + }, [cellStatusAction]); + + return ( + + ); +} + +const excerptLength = 40; + +function getCellValue( + thisVal: string, + colType?: string, + cellStatus?: CellStatusActionType, + otherVal?: string, +) { + const cellModified = thisVal !== otherVal; + const whitespaceDifference = + thisVal.replaceAll(whitespaceRegex, "") === + otherVal?.replaceAll(whitespaceRegex, ""); + const val = getDisplayValue(thisVal); + + if (colType === "bit(1)") { + if (val === "NULL") return val; + return getBitDisplayValue(val); + } + + const longContent = isLongContentType(colType); + if (!longContent && cellModified && whitespaceDifference) { + return val.split("").map(getLetterOrIcon); + } + + if (longContent) { + const isJSON = colType === "json"; + const deltas = isJSON + ? getJSONDiff(thisVal, otherVal) + : getTextDiff(val, getDisplayValue(otherVal || "")); + const expandedVal = + isJSON && !isNullValue(thisVal) ? prettyJSONText(val) : val; + if (cellStatus === CellStatusActionType.Expand) { + return expandedVal; + } + if ( + cellStatus === CellStatusActionType.Deltas || + (!cellStatus && cellModified) + ) { + return deltas; + } + return excerpt(val, excerptLength); + } + + return excerpt(val, excerptLength); +} + +function getLetterOrIcon(s: string): string | JSX.Element { + switch (s) { + case " ": + return ; + case "\n": + case "\r": + return ; + case "\t": + return ; + default: + return s; + } +} diff --git a/packages/web/components/DiffTable/DataDiff/DiffMsg.tsx b/packages/web/components/DiffTable/DataDiff/DiffMsg.tsx new file mode 100644 index 00000000..16cb5786 --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/DiffMsg.tsx @@ -0,0 +1,33 @@ +import { isUneditableDoltSystemTable } from "@lib/doltSystemTables"; +import { gqlNoRefFoundErr } from "@lib/errors/graphql"; +import { errorMatches } from "@lib/errors/helpers"; +import { ApolloErrorType } from "@lib/errors/types"; +import css from "./index.module.css"; + +type Props = { + err?: ApolloErrorType; + tableName: string; + refName: string; + isPKTable: boolean; +}; + +export default function DiffMsg(props: Props) { + return ( + <> + {errorMatches(gqlNoRefFoundErr, props.err) && ( +

+ The branch ${props.refName} does not exist in this database. Some + functionality, such as cell buttons, will not work. +

+ )} + {isUneditableDoltSystemTable(props.tableName) && ( +

+ Cannot edit system table {props.tableName} +

+ )} + {!props.isPKTable && ( +

Cannot edit keyless tables

+ )} + + ); +} diff --git a/packages/web/components/DiffTable/DataDiff/FilterByType.tsx b/packages/web/components/DiffTable/DataDiff/FilterByType.tsx new file mode 100644 index 00000000..893ec20b --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/FilterByType.tsx @@ -0,0 +1,33 @@ +import { DiffRowType } from "@gen/graphql-types"; +import FormSelect from "@components/FormSelect"; +import css from "./index.module.css"; + +type Props = { + filter?: DiffRowType; + setFilter: (f?: DiffRowType) => void; +}; + +export default function FilterByType(props: Props) { + return ( +
+ Filter by +
+ +
+
+ ); +} diff --git a/packages/web/components/DiffTable/DataDiff/Head.tsx b/packages/web/components/DiffTable/DataDiff/Head.tsx new file mode 100644 index 00000000..6ae4a9af --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/Head.tsx @@ -0,0 +1,28 @@ +import { ColumnForDataTableFragment } from "@gen/graphql-types"; +import { ColumnStatus, SetColumnStatus } from "@lib/tableTypes"; +import HeadCell from "./HeadCell"; +import { HiddenColIndexes, SetHiddenColIndexes, isHiddenColumn } from "./utils"; + +type Props = { + cols: ColumnForDataTableFragment[]; + hiddenColIndexes: HiddenColIndexes; + setHiddenColIndexes: SetHiddenColIndexes; + columnStatus: ColumnStatus; + setColumnStatus: SetColumnStatus; + hideCellButtons?: boolean; + refName: string; +}; + +export default function Head(props: Props) { + return ( +
+ + + + ); +} diff --git a/packages/web/components/DiffTable/DataDiff/HeadCell.tsx b/packages/web/components/DiffTable/DataDiff/HeadCell.tsx new file mode 100644 index 00000000..0e616671 --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/HeadCell.tsx @@ -0,0 +1,69 @@ +import ChangeColumnStatusButton from "@components/CellButtons/ChangeColumnStatusButton"; +import DropColumnButton from "@components/CellButtons/DropColumnButton"; +import HideDiffColumnButton from "@components/CellButtons/HideDiffColumnButton"; +import CellDropdown from "@components/CellDropdown"; +import { ColumnForDataTableFragment } from "@gen/graphql-types"; +import { isLongContentType } from "@lib/dataTable"; +import { + CellStatusActionType, + ColumnStatus, + SetColumnStatus, +} from "@lib/tableTypes"; +import { FiKey } from "@react-icons/all-files/fi/FiKey"; +import cx from "classnames"; +import { useState } from "react"; +import css from "./index.module.css"; +import { SetHiddenColIndexes, hideButton, hideColumn } from "./utils"; + +type Props = { + col: ColumnForDataTableFragment; + index: number; + setHiddenColIndexes: SetHiddenColIndexes; + hideCellButtons?: boolean; + columnStatus: ColumnStatus; + setColumnStatus: SetColumnStatus; + refName: string; +}; + +export default function HeadCell(props: Props) { + const [showDropdown, setShowDropdown] = useState(false); + const showChangeStatusButtons = isLongContentType(props.col.type); + return ( + + ); +} diff --git a/packages/web/components/DiffTable/DataDiff/HiddenCols.tsx b/packages/web/components/DiffTable/DataDiff/HiddenCols.tsx new file mode 100644 index 00000000..96ce734e --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/HiddenCols.tsx @@ -0,0 +1,38 @@ +import Btn from "@components/Btn"; +import { ColumnForDataTableFragment } from "@gen/graphql-types"; +import { IoMdClose } from "@react-icons/all-files/io/IoMdClose"; +import css from "./index.module.css"; +import { HiddenColIndexes, SetHiddenColIndexes, unhideColumn } from "./utils"; + +type Props = { + cols: ColumnForDataTableFragment[]; + hiddenColIndexes: HiddenColIndexes; + setHiddenColIndexes: SetHiddenColIndexes; + onClickUnchangedCols?: (i: number) => void; +}; + +export default function HiddenCols(props: Props) { + return ( +
+
{"Hidden columns:"}
+
    + {props.hiddenColIndexes.map(i => ( +
  1. + {props.cols[i].name} + { + if (props.onClickUnchangedCols) { + props.onClickUnchangedCols(i); + } + unhideColumn(i, props.setHiddenColIndexes); + }} + aria-label={`unhide column ${props.cols[i].name}`} + > + + +
  2. + ))} +
+
+ ); +} diff --git a/packages/web/components/DiffTable/DataDiff/Row.tsx b/packages/web/components/DiffTable/DataDiff/Row.tsx new file mode 100644 index 00000000..a175c34e --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/Row.tsx @@ -0,0 +1,113 @@ +/* eslint-disable react/jsx-key */ +import DeleteRowButton from "@components/CellButtons/DeleteRowButton"; +import CellDropdown from "@components/CellDropdown"; +import { + ColumnForDiffTableListFragment, + RowDiffForTableListFragment, +} from "@gen/graphql-types"; +import { ColumnStatus, SetColumnStatus } from "@lib/tableTypes"; +import cx from "classnames"; +import { useState } from "react"; +import Cell, { CellType } from "./Cell"; +import css from "./index.module.css"; +import { HiddenColIndexes, isHiddenColumn } from "./utils"; + +type Props = { + rowDiff: RowDiffForTableListFragment; + ridx: number; + hiddenColIndexes: HiddenColIndexes; + cols: ColumnForDiffTableListFragment[]; + hideCellButtons?: boolean; + columnStatus: ColumnStatus; + setColumnStatus: SetColumnStatus; + userCanWrite: boolean; + refName: string; +}; + +export default function Row(props: Props) { + const { added, deleted } = props.rowDiff; + const deletedRowClassName = cx({ + [css.rowDeleted]: !added, + [css.noBorder]: !!added, + }); + const [showDropdown, setShowDropdown] = useState(false); + + const deletedRow = !!deleted && ( + + + {deleted.columnValues.map((c, i) => { + if (isHiddenColumn(i, props.hiddenColIndexes)) return null; + return ( + // this will render false columnValues unless wrapped with fragment + // eslint-disable-next-line react/jsx-no-useless-fragment + <> + + + ); + })} + + ); + + const addedRowClassName = cx({ [css.rowAdded]: !deleted }); + const addedRow = !!added && props.rowDiff.added && ( + + + {added.columnValues.map((c, i) => { + if (isHiddenColumn(i, props.hiddenColIndexes)) return null; + return ( + // this will render false columnValues unless wrapped with fragment + // eslint-disable-next-line react/jsx-no-useless-fragment + <> + + + ); + })} + + ); + + return ( + <> + {deletedRow} + {addedRow} + + ); +} diff --git a/packages/web/components/DiffTable/DataDiff/Table.tsx b/packages/web/components/DiffTable/DataDiff/Table.tsx new file mode 100644 index 00000000..f8240bb5 --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/Table.tsx @@ -0,0 +1,79 @@ +import { ApolloError } from "@apollo/client"; +import useRole from "@hooks/useRole"; +import { + isDoltSystemTable, + isUneditableDoltSystemTable, +} from "@lib/doltSystemTables"; +import { gqlNoRefFoundErr } from "@lib/errors/graphql"; +import { errorMatches } from "@lib/errors/helpers"; +import { DiffParams } from "@lib/params"; +import cx from "classnames"; +import { useState } from "react"; +import Body from "./Body"; +import Head from "./Head"; +import css from "./index.module.css"; +import { RowDiffState } from "./state"; +import { + HiddenColIndexes, + SetHiddenColIndexes, + getColumnInitialStatus, +} from "./utils"; + +type Props = { + state: RowDiffState; + setHiddenColIndexes: SetHiddenColIndexes; + hiddenColIndexes: HiddenColIndexes; + params: DiffParams & { + tableName: string; + }; + hideCellButtons?: boolean; + refName: string; + error?: ApolloError; + forMobile?: boolean; + isPKTable?: boolean; +}; + +export default function Table(props: Props) { + const initialColumnStatus = getColumnInitialStatus(props.state.cols); + const [columnStatus, setColumnStatus] = useState(initialColumnStatus); + const { canWriteToDB } = useRole(); + const hideCellButtons = + props.hideCellButtons || + errorMatches(gqlNoRefFoundErr, props.error) || + !props.isPKTable; + + return ( +
+ {props.row.added ? ( + + {editing && currCol ? ( + { + setEditing(false); + setShowDropdown(false); + }} + largerMarginRight + queryCols={props.cols} + row={props.row.added} + refName={props.refName} + /> + ) : ( + <> + + + {props.type === CellType.Added && + !props.hideCellButtons && + currCol && ( + <> + + + + )} + {showChangeStatusButtons && + Object.values(CellStatusActionType) + .filter(c => !hideButton(c, cellStatusAction, cellModified)) + .map(c => ( + + ))} + + {cellVal} + + )} + + ) : ( + {cellVal} + )} +
+ {props.cols.map((c, i) => { + if (isHiddenColumn(i, props.hiddenColIndexes)) return null; + return ; + })} +
+ {props.col.name} + {props.col.isPrimaryKey && } + + + hideColumn(props.index, props.setHiddenColIndexes)} + /> + {!props.hideCellButtons && !props.col.isPrimaryKey && ( + + )} + {showChangeStatusButtons && + Object.values(CellStatusActionType) + .filter(c => !hideButton(c, props.columnStatus[props.index], true)) + .map(c => ( + + ))} + +
+ + {!props.hideCellButtons && props.userCanWrite && ( + + + + )} + + + +
+
+ + +
+ ); +} diff --git a/packages/web/components/DiffTable/DataDiff/ViewSqlLink.tsx b/packages/web/components/DiffTable/DataDiff/ViewSqlLink.tsx new file mode 100644 index 00000000..fdc230ef --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/ViewSqlLink.tsx @@ -0,0 +1,59 @@ +import { AiOutlineConsoleSql } from "@react-icons/all-files/ai/AiOutlineConsoleSql"; +import { + ColumnForDataTableFragment, + useDataTableQuery, +} from "@gen/graphql-types"; +import { DiffParams } from "@lib/params"; +import { sqlQuery } from "@lib/urls"; +import { DropdownItem } from "@components/DatabaseOptionsDropdown"; +import Link from "@components/links/Link"; +import SmallLoader from "@components/SmallLoader"; +import { getDoltCommitDiffQuery } from "../DiffTableStats/utils"; +import css from "./index.module.css"; +import { HiddenColIndexes } from "./utils"; + +type Props = { + params: Required & { + tableName: string; + }; + hiddenColIndexes: HiddenColIndexes; +}; + +type InnerProps = Props & { + columns: ColumnForDataTableFragment[]; +}; + +function Inner(props: InnerProps) { + return ( + } + data-cy="view-sql-link" + > + + View SQL + + + ); +} + +export default function ViewSqlLink(props: Props) { + const tableRes = useDataTableQuery({ + variables: props.params, + }); + + if (tableRes.loading) { + return ; + } + + if (!tableRes.data) { + return null; + } + + return ; +} diff --git a/packages/web/components/DiffTable/DataDiff/index.module.css b/packages/web/components/DiffTable/DataDiff/index.module.css new file mode 100644 index 00000000..170b158f --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/index.module.css @@ -0,0 +1,288 @@ +.container { + @apply relative overflow-x-auto lg:overflow-x-visible; +} + +.line { + @apply absolute top-0 w-full border-t border-ld-lightgrey h-2; +} + +.topPadding { + @apply pt-4; +} + +.infiniteScrollContainer { + @apply overflow-auto lg:overflow-visible hidden lg:block; +} + +.mobileTable { + @apply overflow-x-auto pb-4; + + @screen lg { + @apply hidden; + } +} + +.diffTable { + @apply leading-loose text-sm font-mono border-separate z-1 pt-2 pr-10 border-ld-lightgrey overflow-hidden pb-4 lg:pb-40; + border-spacing: 0; + + th { + @apply font-bold max-w-full whitespace-nowrap; + } + th, + td { + @apply pr-8 pl-2 border-b border-ld-lightgrey relative; + + &:first-of-type { + @apply min-w-0; + } + } + thead { + @apply text-left text-primary; + } + tbody { + @apply whitespace-nowrap text-ld-darkgrey relative; + + &::after { + @apply bg-white absolute left-0 w-[10000px] h-40 -bottom-40; + content: "\00a0"; + z-index: -1; + } + } +} + +.noChanges { + @apply pt-5; +} + +.forDocs { + @apply relative; +} + +.key { + @apply inline-block ml-2 text-ld-darkgrey mb-0.5; +} + +.diffTableColsHidden { + @apply pt-[4.5rem]; +} + +.cell { + @apply text-ld-darkgrey min-w-[130px] max-w-[400px]; +} + +.longContent { + @apply min-w-[300px]; + span { + @apply whitespace-pre-wrap break-words; + } +} + +.isNull { + @apply text-acc-grey; +} + +.menu { + @apply hidden; +} +@screen lg { + .clicked .menu, + .cell:hover .menu, + .added:hover .menu { + @apply block absolute bg-white rounded-full right-2 top-[0.35rem]; + + &:focus { + @apply outline-none widget-shadow-lightblue; + } + } +} + +.headCell:hover, +.cell:hover, +.clicked { + @screen lg { + /* Applies background color to full column on hover */ + &::after { + @apply bg-ld-lightblue absolute left-0 w-full h-[10000px] -top-[5000px]; + content: "\00a0"; + z-index: -5; + } + } +} + +.clicked:hover, +.headCell:hover, +.clicked:focus-within { + .menu { + @apply block absolute bg-white rounded-full right-2 top-[0.35rem]; + + &:focus { + @apply outline-none widget-shadow-lightblue; + } + } +} + +.minus { + @apply text-acc-red text-right; +} +.deleted { + @apply text-acc-red; +} + +.added { + @apply text-acc-green; +} + +.toTop { + @apply fixed bottom-4 right-8 px-2; +} + +.noBorder { + @apply border-b-0; +} + +.rowDeleted { + background-color: #ffeaec; +} + +.rowAdded { + background-color: #defbe4; +} + +.hiddenContainer { + @apply mb-3 pt-3 absolute w-full inline-flex items-center; + + ol { + @apply flex overflow-x-scroll mx-1 h-[3.5rem] px-2; + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar { + @apply hidden; + } + } +} + +.hiddenLabel { + @apply inline-block whitespace-nowrap text-sm; +} + +.hiddenCol { + @apply bg-ld-lightpurple rounded mx-[.2rem] my-[.6rem] pl-2 border border-ld-lightgrey inline-flex items-center; + + &:hover { + @apply bg-ld-lightblue; + } + + button { + @apply mx-2; + + svg { + @apply inline-block mb-0.5; + } + } +} + +.topContainer { + @apply absolute mb-6 right-2 ml-14 hidden -top-20; + + @screen lg { + @apply flex -top-10; + } + + a { + @apply mt-0.5; + } +} + +.filterContainer { + @apply flex ml-4; + + > span { + @apply mr-4 mt-0.5; + } +} + +.filterSelect { + @apply w-40; +} + +.err { + @apply pt-0 mb-4; +} + +.dot { + @apply px-1 pt-1; +} + +.icon { + @apply inline-block opacity-50; +} + +.space { + @apply mt-1.5; +} + +.marX { + @apply mx-0.5; +} + +.optionButton { + @apply hidden; + @media (min-width: 850px) { + @apply flex justify-end mt-0.5; + } +} + +.whitespaceToggle { + /* stylelint-disable-next-line no-descending-specificity */ + th, + tbody { + @apply whitespace-pre; + } +} + +.sqlIcon { + @apply text-lg font-normal; +} + +.sqlLink { + @apply font-thin text-primary hover:text-primary; +} + +.rowDropdown { + @apply hidden; + @screen lg { + @apply invisible mx-2 text-xl flex; + } +} + +.clicked .rowDropdown, +.addedRow:hover .rowDropdown { + @apply visible rounded-full bg-ld-lightgrey text-ld-darkergrey; + + &:focus { + @apply outline-none widget-shadow-lightblue; + } +} + +.deletedRow:hover { + @apply visible rounded-full text-ld-darkergrey bg-opacity-50; + background-color: #ffeaec; + + &:focus { + @apply outline-none widget-shadow-lightblue; + } +} + +.addedRow:hover { + @apply visible rounded-full bg-opacity-50 text-ld-darkergrey; + background-color: #defbe4; + + &:focus { + @apply outline-none widget-shadow-lightblue; + } +} + +.flexCenter { + @apply flex items-center; +} diff --git a/packages/web/components/DiffTable/DataDiff/index.tsx b/packages/web/components/DiffTable/DataDiff/index.tsx new file mode 100644 index 00000000..8e6eaf0d --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/index.tsx @@ -0,0 +1,141 @@ +import Button from "@components/Button"; +import DatabaseOptionsDropdown from "@components/DatabaseOptionsDropdown"; +import Errors from "@components/DatabaseTableHeader/Errors"; +import ErrorMsg from "@components/ErrorMsg"; +import Loader from "@components/Loader"; +import { useDataTableContext } from "@contexts/dataTable"; +import { useDiffContext } from "@contexts/diff"; +import { DiffRowType } from "@gen/graphql-types"; +import useFocus from "@hooks/useFocus"; +import { ApolloErrorType } from "@lib/errors/types"; +import { DiffParams } from "@lib/params"; +import InfiniteScroll from "react-infinite-scroller"; +import DiffMsg from "./DiffMsg"; +import FilterByType from "./FilterByType"; +import HiddenCols from "./HiddenCols"; +import Table from "./Table"; +import ViewSqlLink from "./ViewSqlLink"; +import css from "./index.module.css"; +import { RowDiffState } from "./state"; +import useRowDiffs from "./useRowDiffs"; +import useToggleColumns from "./useToggleColumns"; +import { HiddenColIndexes, SetHiddenColIndexes, getIsPKTable } from "./utils"; + +type Props = { + hiddenColIndexes: HiddenColIndexes; + setHiddenColIndexes: SetHiddenColIndexes; + params: Required & { + tableName: string; + }; + hideCellButtons?: boolean; +}; + +type InnerProps = Props & { + fetchMore: () => Promise; + setFilter: (d: DiffRowType | undefined) => void; + state: RowDiffState; + hasMore: boolean; + error?: ApolloErrorType; +}; + +function Inner(props: InnerProps) { + const { state, fetchMore, setFilter, hasMore, error } = props; + + const { setScrollToTop } = useFocus(); + const { removeCol, onClickHideUnchangedCol, hideUnchangedCols } = + useToggleColumns( + state.cols, + state.rowDiffs, + props.hiddenColIndexes, + props.setHiddenColIndexes, + ); + + const isPKTable = getIsPKTable(state.cols); + + // For diff tables we want to use `refName` from `useDiffContext`, which is either the `fromBranchName` + // for PR diffs or the current ref for commit diffs + const { refName } = useDiffContext(); + const res = useDataTableContext(); + + return ( +
+
+ + + + +
+ + + + {!!props.hiddenColIndexes.length && ( + + )} + + + {state.rowDiffs.length ? ( + Loading rows ...
} + useWindow={false} + getScrollParent={() => document.getElementById("main-content")} + className={css.infiniteScrollContainer} + > + + + ) : ( +

No changes to this table in this diff

+ )} + +
+
+ {hasMore && } + + + {state.rowDiffs.length > 50 && ( + + )} + + ); +} + +export default function DataDiff(props: Props) { + const { state, fetchMore, setFilter, hasMore, loading } = useRowDiffs( + props.params, + ); + + if (loading) return ; + + return ( + + ); +} diff --git a/packages/web/components/DiffTable/DataDiff/state.ts b/packages/web/components/DiffTable/DataDiff/state.ts new file mode 100644 index 00000000..a1c3650c --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/state.ts @@ -0,0 +1,30 @@ +import { + ColumnForDiffTableListFragment, + DiffRowType, + RowDiffForTableListFragment, + RowDiffListWithColsFragment, +} from "@gen/graphql-types"; +import Maybe from "@lib/Maybe"; +import { Dispatch } from "react"; + +export const defaultState = { + offset: undefined as Maybe, + rowDiffs: [] as RowDiffForTableListFragment[], + cols: [] as ColumnForDiffTableListFragment[], + filter: undefined as DiffRowType | undefined, +}; + +export type RowDiffState = typeof defaultState; + +export type RowDiffDispatch = Dispatch>; + +export function getDefaultState( + rowDiffList?: RowDiffListWithColsFragment, +): RowDiffState { + return { + ...defaultState, + rowDiffs: rowDiffList?.list ?? [], + cols: rowDiffList?.columns ?? [], + offset: rowDiffList?.nextOffset, + }; +} diff --git a/packages/web/components/DiffTable/DataDiff/useRowDiffs.ts b/packages/web/components/DiffTable/DataDiff/useRowDiffs.ts new file mode 100644 index 00000000..c35b76cf --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/useRowDiffs.ts @@ -0,0 +1,86 @@ +import { + DiffRowType, + RowDiffForTableListFragment, + RowDiffsDocument, + RowDiffsQuery, + RowDiffsQueryVariables, + useRowDiffsQuery, +} from "@gen/graphql-types"; +import useApolloError from "@hooks/useApolloError"; +import useSetState from "@hooks/useSetState"; +import Maybe from "@lib/Maybe"; +import { handleCaughtApolloError } from "@lib/errors/helpers"; +import { ApolloErrorType } from "@lib/errors/types"; +import { RequiredCommitsParams } from "@lib/params"; +import { useEffect, useState } from "react"; +import { RowDiffState, getDefaultState } from "./state"; + +type ReturnType = { + fetchMore: () => Promise; + setFilter: (d: DiffRowType | undefined) => void; + state: RowDiffState; + hasMore: boolean; + loading: boolean; + error?: ApolloErrorType; +}; + +type Params = RequiredCommitsParams & { + tableName: string; +}; + +export default function useRowDiffs(params: Params): ReturnType { + const { data, client, loading, error } = useRowDiffsQuery({ + variables: params, + }); + const [state, setState] = useSetState(getDefaultState(data?.rowDiffs)); + const [lastOffset, setLastOffset] = useState>(undefined); + const [err, setErr] = useApolloError(error); + + useEffect(() => { + if (loading || error || !data) return; + setState(getDefaultState(data.rowDiffs)); + }, [loading, error, data, setState]); + + const handleQuery = async ( + setRowDiffs: (rd: RowDiffForTableListFragment[]) => void, + offset: Maybe, + filterByRowType?: DiffRowType, + ) => { + if (err) setErr(undefined); + if (offset === undefined || offset === null) { + return; + } + setLastOffset(offset); + try { + const res = await client.query({ + query: RowDiffsDocument, + variables: { ...params, offset, filterByRowType }, + }); + setRowDiffs(res.data.rowDiffs.list); + setState({ offset: res.data.rowDiffs.nextOffset }); + } catch (er) { + handleCaughtApolloError(er, setErr); + } + }; + + const fetchMore = async () => { + const setRowDiffs = (rd: RowDiffForTableListFragment[]) => + setState({ rowDiffs: state.rowDiffs.concat(rd) }); + await handleQuery(setRowDiffs, state.offset, state.filter); + }; + + // Changes diff row filter, starts with first page diffs + const setFilter = async (rowType?: DiffRowType) => { + setState({ filter: rowType }); + const setRowDiffs = (rd: RowDiffForTableListFragment[]) => + setState({ rowDiffs: rd }); + await handleQuery(setRowDiffs, 0, rowType ?? DiffRowType.All); + }; + + const hasMore = + state.offset !== undefined && + state.offset !== null && + state.offset !== lastOffset; + + return { state, fetchMore, setFilter, hasMore, loading, error: err }; +} diff --git a/packages/web/components/DiffTable/DataDiff/useToggleColumns.tsx b/packages/web/components/DiffTable/DataDiff/useToggleColumns.tsx new file mode 100644 index 00000000..312f7a5c --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/useToggleColumns.tsx @@ -0,0 +1,73 @@ +import { + ColumnForDiffTableListFragment, + RowDiffForTableListFragment, +} from "@gen/graphql-types"; +import { useCallback, useEffect, useState } from "react"; +import { + HiddenColIndexes, + SetHiddenColIndexes, + getUnchangedColIndexes, +} from "./utils"; + +type ReturnType = { + removeCol: (idx: number) => void; + onClickHideUnchangedCol: () => void; + hideUnchangedCols: boolean; +}; + +export default function useToggleColumns( + cols: ColumnForDiffTableListFragment[], + rowDiffs: RowDiffForTableListFragment[], + hiddenColIndexes: HiddenColIndexes, + setHiddenColIndexes: SetHiddenColIndexes, +): ReturnType { + const [hideUnchangedCols, setHideUnchangedCols] = useState(false); + const [currentUnchangedCols, setCurrentUnchangedCols] = useState( + [] as HiddenColIndexes, + ); + + const removeCol = (idx: number) => { + const unchangedCols = new Set(currentUnchangedCols); + unchangedCols.delete(idx); + setCurrentUnchangedCols([...unchangedCols]); + }; + + const toggleHideUnchangedCols = useCallback( + (toggle?: boolean) => { + const unchangedCols = getUnchangedColIndexes(cols, rowDiffs); + const shouldAddHiddenCols = toggle + ? !hideUnchangedCols + : hideUnchangedCols; + + if (shouldAddHiddenCols) { + setHiddenColIndexes([...unchangedCols]); + setCurrentUnchangedCols([...unchangedCols]); + } else { + if (toggle) { + setHiddenColIndexes([]); + } + + setCurrentUnchangedCols([]); + } + }, + [hideUnchangedCols, hiddenColIndexes, setHiddenColIndexes, rowDiffs, cols], + ); + + useEffect(() => { + if (currentUnchangedCols.length !== 0) { + return; + } + setHideUnchangedCols(false); + }, [currentUnchangedCols]); + + useEffect(() => { + toggleHideUnchangedCols(); + }, [rowDiffs.length]); + + const onClickHideUnchangedCol = () => { + setHideUnchangedCols(() => !hideUnchangedCols); + toggleHideUnchangedCols(true); + }; + + return { removeCol, onClickHideUnchangedCol, hideUnchangedCols }; +} diff --git a/packages/web/components/DiffTable/DataDiff/utils.test.ts b/packages/web/components/DiffTable/DataDiff/utils.test.ts new file mode 100644 index 00000000..14124706 --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/utils.test.ts @@ -0,0 +1,33 @@ +import { breakTextIntoLines } from "./utils"; + +describe("test breakTextIntoLines", () => { + const tests = [ + { + desc: "text content", + text: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", + expectedText: + "Lorem Ipsum is simply dummy text of the\nprinting and typesetting industry. Lorem\nIpsum has been the industry's standard\ndummy text ever since the 1500s, when an\nunknown printer took a galley of type\nand scrambled it to make a type specimen\nbook. It has survived not only five\ncenturies, but also the leap into\nelectronic typesetting, remaining\nessentially unchanged. It was\npopularised in the 1960s with the\nrelease of Letraset sheets containing\nLorem Ipsum passages, and more recently\nwith desktop publishing software like\nAldus PageMaker including versions of\nLorem Ipsum.", + }, + { + desc: "2 lines content", + text: "0-day ATX Macintosh rollback DPI this with the Macintosh pdf with query", + expectedText: + "0-day ATX Macintosh rollback DPI this\nwith the Macintosh pdf with query", + }, + { + desc: "one line content", + text: "one line", + expectedText: "one line", + }, + { + desc: "NULL", + text: "NULL", + expectedText: "NULL", + }, + ]; + tests.forEach(test => { + it(test.desc, () => { + expect(breakTextIntoLines(test.text, 40)).toEqual(test.expectedText); + }); + }); +}); diff --git a/packages/web/components/DiffTable/DataDiff/utils.ts b/packages/web/components/DiffTable/DataDiff/utils.ts new file mode 100644 index 00000000..8320391d --- /dev/null +++ b/packages/web/components/DiffTable/DataDiff/utils.ts @@ -0,0 +1,159 @@ +import { + ColumnForDataTableFragment, + ColumnForDiffTableListFragment, + RowDiff, +} from "@gen/graphql-types"; +import { isLongContentType } from "@lib/dataTable"; +import { getDisplayValue, isNullValue } from "@lib/null"; +import safeJSONParse from "@lib/safeJSONParse"; +import { CellStatusActionType, ColumnStatus } from "@lib/tableTypes"; +import * as diff from "diff"; + +export type HiddenColIndexes = number[]; + +export type SetHiddenColIndexes = React.Dispatch< + React.SetStateAction +>; + +export function hideColumn( + index: number, + setHiddenColIndexes: SetHiddenColIndexes, +): void { + setHiddenColIndexes(oldIndexes => oldIndexes.concat(index)); +} + +export function isHiddenColumn( + index: number, + hiddenColIndexes: HiddenColIndexes, +): boolean { + return hiddenColIndexes.includes(index); +} + +export function unhideColumn( + index: number, + setHiddenColIndexes: SetHiddenColIndexes, +): void { + setHiddenColIndexes(oldIndexes => oldIndexes.filter(i => i !== index)); +} + +export function hasColDiff(rowDiffs: RowDiff[], colNum: number): boolean { + return !!rowDiffs.some( + rowDiff => + !rowDiff.added?.columnValues || + !rowDiff.deleted?.columnValues || + rowDiff.added.columnValues[colNum].displayValue !== + rowDiff.deleted.columnValues[colNum].displayValue, + ); +} + +export function getUnchangedColIndexes( + rowDiffColumns: ColumnForDataTableFragment[], + rowDiffs: RowDiff[], +): HiddenColIndexes { + const unchangedColIndexes = [] as HiddenColIndexes; + + rowDiffColumns.forEach((_, i) => { + if (!hasColDiff(rowDiffs, i)) { + unchangedColIndexes.push(i); + } + }); + + return unchangedColIndexes; +} + +export function getIsPKTable(cols: ColumnForDataTableFragment[]): boolean { + return cols.some(col => col.isPrimaryKey); +} + +export function getColumnInitialStatus( + rowDiffColumns: ColumnForDiffTableListFragment[], +): ColumnStatus { + const rowLength = rowDiffColumns.length || 0; + const initialColumnStatus: ColumnStatus = {}; + for (let i = 0; i < rowLength; i++) { + if (!isLongContentType(rowDiffColumns[i].type || "")) { + initialColumnStatus[i] = CellStatusActionType.Collapse; + } + } + return initialColumnStatus; +} + +export function hideButton( + c: CellStatusActionType, + cellStatusAction?: CellStatusActionType, + cellModified?: boolean, +): boolean { + /* cellStatusAction===undefined:initial load for long content cells + for cells not modified, show excerpt content + for cells modified, show deltas + */ + return ( + c === cellStatusAction || + (!cellStatusAction && + !cellModified && + c === CellStatusActionType.Collapse) || + (!cellStatusAction && !!cellModified && c === CellStatusActionType.Deltas) + ); +} + +function concatChanges(changes: Diff.Change[]): string { + let deleted = ""; + if (changes.filter(c => c.removed).length === 0) { + return "..."; + } + changes.forEach((change, i) => { + if (change.removed) { + deleted += change.value; + } + if (!change.added && !change.removed) { + const unchanged = change.value.split("\n"); + const len = unchanged.length; + if (i === changes.length - 1 || len === 2) { + deleted += len > 2 ? `${unchanged[0]}\n...` : `${unchanged[0]}\n`; + } else { + deleted += + i === 0 + ? `...\n${unchanged[unchanged.length - 2]}\n` + : `${unchanged[0]}\n${len > 3 ? "..." : ""}\n${ + unchanged[unchanged.length - 2] + }\n`; + } + } + }); + return deleted; +} + +export function getJSONDiff(thisVal: string, otherVal?: string): string { + if (!otherVal || isNullValue(thisVal) || isNullValue(otherVal)) { + return getDisplayValue(thisVal); + } + + const changes = diff.diffJson( + safeJSONParse(thisVal), + safeJSONParse(otherVal), + ); + return concatChanges(changes); +} + +export function breakTextIntoLines(text: string, width: number): string { + const words = text.split(" "); + let res = ""; + let oneLine = ""; + words.forEach(w => { + if (oneLine.length + 1 + w.length <= width) { + oneLine = oneLine.length ? `${oneLine} ${w}` : w; + } else { + res = res.length ? `${res}\n${oneLine}` : oneLine; + oneLine = w; + } + }); + + return oneLine.length ? `${res}${res.length ? "\n" : ""}${oneLine}` : res; +} + +export function getTextDiff(thisVal: string, otherVal?: string): string { + const thisValBreak = breakTextIntoLines(thisVal, 40); + const otherValBreak = breakTextIntoLines(otherVal ?? "", 40); + const changes = diff.diffLines(thisValBreak, otherValBreak); + return concatChanges(changes); +} diff --git a/packages/web/components/DiffTable/DataSection.tsx b/packages/web/components/DiffTable/DataSection.tsx new file mode 100644 index 00000000..0d73615e --- /dev/null +++ b/packages/web/components/DiffTable/DataSection.tsx @@ -0,0 +1,31 @@ +import { DataTableProvider } from "@contexts/dataTable"; +import { useDiffContext } from "@contexts/diff"; +import { SqlEditorProvider } from "@contexts/sqleditor"; +import { DiffSummaryFragment } from "@gen/graphql-types"; +import DataDiff from "./DataDiff"; +import { HiddenColIndexes, SetHiddenColIndexes } from "./DataDiff/utils"; + +type Props = { + diffSummary: DiffSummaryFragment; + hiddenColIndexes: HiddenColIndexes; + setHiddenColIndexes: SetHiddenColIndexes; + hideCellButtons?: boolean; +}; + +export default function DataSection(props: Props) { + const { refName, params } = useDiffContext(); + + const diffParams = { + ...params, + tableName: props.diffSummary.tableName, + refName, + }; + + return ( + + + + + + ); +} diff --git a/packages/web/components/DiffTable/DiffTableStats/Stats.tsx b/packages/web/components/DiffTable/DiffTableStats/Stats.tsx new file mode 100644 index 00000000..0f42325f --- /dev/null +++ b/packages/web/components/DiffTable/DiffTableStats/Stats.tsx @@ -0,0 +1,46 @@ +import { DiffStatForDiffsFragment, TableDiffType } from "@gen/graphql-types"; +import SummaryStat from "./SummaryStat"; + +type Props = { + diffStat: DiffStatForDiffsFragment; + tableType: TableDiffType; +}; + +export default function Stats({ diffStat, tableType }: Props) { + return ( + <> + + + + + + + ); +} diff --git a/packages/web/components/DiffTable/DiffTableStats/SummaryStat/index.module.css b/packages/web/components/DiffTable/DiffTableStats/SummaryStat/index.module.css new file mode 100644 index 00000000..0e5909d4 --- /dev/null +++ b/packages/web/components/DiffTable/DiffTableStats/SummaryStat/index.module.css @@ -0,0 +1,24 @@ +.stat { + @apply mr-10; +} + +.smallStat { + @apply ml-5; + font-size: 0.82rem; +} + +.num { + @apply mr-1 font-semibold text-ld-darkgrey; +} + +.green { + @apply text-acc-green; +} + +.red { + @apply text-acc-red; +} + +.percent { + @apply text-ld-darkgrey pl-1; +} diff --git a/packages/web/components/DiffTable/DiffTableStats/SummaryStat/index.test.tsx b/packages/web/components/DiffTable/DiffTableStats/SummaryStat/index.test.tsx new file mode 100644 index 00000000..9ef9f584 --- /dev/null +++ b/packages/web/components/DiffTable/DiffTableStats/SummaryStat/index.test.tsx @@ -0,0 +1,62 @@ +import { render, screen } from "@testing-library/react"; +import SummaryStat from "."; + +describe("test SummaryStat", () => { + it("renders stat with no value or count", () => { + render(); + expect(screen.getByText("-")).toBeVisible(); + }); + + it("renders stat with error", () => { + render( + , + ); + expect(screen.getByText("-")).toBeVisible(); + expect(screen.queryByText("10")).not.toBeInTheDocument(); + }); + + it("renders stat with no count", () => { + render( + , + ); + expect(screen.queryByText("-")).not.toBeInTheDocument(); + expect(screen.getByText("10")).toBeVisible(); + expect(screen.getByText("Rows Added")).toBeVisible(); + expect(screen.queryByText("0.00%")).not.toBeInTheDocument(); + }); + + it("renders stat with singular value", () => { + render( + , + ); + expect(screen.queryByText("-")).not.toBeInTheDocument(); + expect(screen.getByText("1")).toBeVisible(); + expect(screen.getByText("Row Added")).toBeVisible(); + expect(screen.getByText("1.00%")).toBeVisible(); + }); + + it("renders stat with >1 value", () => { + render( + , + ); + expect(screen.queryByText("-")).not.toBeInTheDocument(); + expect(screen.getByText("1,000")).toBeVisible(); + expect(screen.getByText("Rows Added")).toBeVisible(); + expect(screen.getByText("10.00%")).toBeVisible(); + }); +}); diff --git a/packages/web/components/DiffTable/DiffTableStats/SummaryStat/index.tsx b/packages/web/components/DiffTable/DiffTableStats/SummaryStat/index.tsx new file mode 100644 index 00000000..72657a29 --- /dev/null +++ b/packages/web/components/DiffTable/DiffTableStats/SummaryStat/index.tsx @@ -0,0 +1,44 @@ +import cx from "classnames"; +import { numToStringWithCommas } from "@lib/numToStringConversions"; +import css from "./index.module.css"; + +type Props = { + value?: number; + count?: number; + statSingle: string; + statPlural: string; + red?: boolean; + green?: boolean; + small?: boolean; + err?: Error; +}; + +export default function SummaryStat({ + value, + count, + statSingle, + statPlural, + err, + red = false, + green = false, + small = false, +}: Props) { + if (err || value === undefined) { + return
-
; + } + + const stat = value === 1 ? statSingle : statPlural; + const percent = count ? (value / count) * 100 : 0; + + return ( +
+ + {numToStringWithCommas(Math.abs(value))} + {" "} + {stat}{" "} + {count !== undefined && ( + {`${percent.toFixed(2)}%`} + )} +
+ ); +} diff --git a/packages/web/components/DiffTable/DiffTableStats/index.module.css b/packages/web/components/DiffTable/DiffTableStats/index.module.css new file mode 100644 index 00000000..5c12cd38 --- /dev/null +++ b/packages/web/components/DiffTable/DiffTableStats/index.module.css @@ -0,0 +1,15 @@ +.tableInfo { + @apply py-3 whitespace-nowrap overflow-x-auto flex flex-wrap; +} + +.err { + @apply inline-block pt-0 ml-4; +} + +.loader { + @apply py-3 ml-3; +} + +.primaryKeyChange { + @apply text-primary font-normal; +} diff --git a/packages/web/components/DiffTable/DiffTableStats/index.tsx b/packages/web/components/DiffTable/DiffTableStats/index.tsx new file mode 100644 index 00000000..5e469598 --- /dev/null +++ b/packages/web/components/DiffTable/DiffTableStats/index.tsx @@ -0,0 +1,63 @@ +import ErrorMsg from "@components/ErrorMsg"; +import SmallLoader from "@components/SmallLoader"; +import { useDiffContext } from "@contexts/diff"; +import { DiffSummaryFragment, useDiffStatQuery } from "@gen/graphql-types"; +import { gqlErrorPrimaryKeyChange } from "@lib/errors/graphql"; +import { errorMatches } from "@lib/errors/helpers"; +import cx from "classnames"; +import Stats from "./Stats"; +import css from "./index.module.css"; + +type Props = { + hiddenColIndexes: number[]; + diffSummary: DiffSummaryFragment; +}; + +export default function DiffTableStats(props: Props) { + const { params, refName } = useDiffContext(); + const { data, loading, error } = useDiffStatQuery({ + variables: { + ...params, + refName, + fromRefName: params.fromCommitId, + toRefName: params.toCommitId, + tableName: props.diffSummary.tableName, + }, + }); + + if (loading) { + return ( + + ); + } + + if (error || !data?.diffStat) { + const isPrimaryKeyChange = errorMatches(gqlErrorPrimaryKeyChange, error); + return ( +
+ Table stats unavailable{" "} + +
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/packages/web/components/DiffTable/DiffTableStats/mocks.ts b/packages/web/components/DiffTable/DiffTableStats/mocks.ts new file mode 100644 index 00000000..72c2016d --- /dev/null +++ b/packages/web/components/DiffTable/DiffTableStats/mocks.ts @@ -0,0 +1,32 @@ +import { fakeCommitId } from "@components/CustomFormSelect/mocks"; +import { ColumnForDataTableFragment } from "@gen/graphql-types"; +import { DiffParams, TableParams } from "@lib/params"; + +type Params = Required & { tableName: string }; + +const tableParams: TableParams = { + databaseName: "dbname", + refName: "master", + tableName: "test-table", +}; + +export const params: Params = { + ...tableParams, + fromCommitId: fakeCommitId(), + toCommitId: fakeCommitId(), +}; + +export const tableCols: ColumnForDataTableFragment[] = [ + { + __typename: "Column", + name: "id", + isPrimaryKey: true, + type: "INT", + }, + { + __typename: "Column", + name: "name", + isPrimaryKey: false, + type: "VARCHAR(16383)", + }, +]; diff --git a/packages/web/components/DiffTable/DiffTableStats/utils.test.ts b/packages/web/components/DiffTable/DiffTableStats/utils.test.ts new file mode 100644 index 00000000..2c36f9a5 --- /dev/null +++ b/packages/web/components/DiffTable/DiffTableStats/utils.test.ts @@ -0,0 +1,28 @@ +import { params, tableCols } from "./mocks"; +import { getDoltCommitDiffQuery } from "./utils"; + +describe("test getDoltCommitDiffQuery for diff tables", () => { + it("returns query for no diff tags or removed columns", () => { + expect( + getDoltCommitDiffQuery({ + params, + columns: tableCols, + hiddenColIndexes: [], + }), + ).toEqual( + `SELECT diff_type, \`from_id\`, \`to_id\`, \`from_name\`, \`to_name\`, from_commit, from_commit_date, to_commit, to_commit_date FROM \`dolt_commit_diff_${params.tableName}\` WHERE from_commit="${params.fromCommitId}" AND to_commit="${params.toCommitId}"`, + ); + }); + + it("returns query for no diff tags and removed columns", () => { + expect( + getDoltCommitDiffQuery({ + params, + columns: tableCols, + hiddenColIndexes: [1], + }), + ).toEqual( + `SELECT diff_type, \`from_id\`, \`to_id\`, from_commit, from_commit_date, to_commit, to_commit_date FROM \`dolt_commit_diff_${params.tableName}\` WHERE from_commit="${params.fromCommitId}" AND to_commit="${params.toCommitId}"`, + ); + }); +}); diff --git a/packages/web/components/DiffTable/DiffTableStats/utils.ts b/packages/web/components/DiffTable/DiffTableStats/utils.ts new file mode 100644 index 00000000..4b5e843e --- /dev/null +++ b/packages/web/components/DiffTable/DiffTableStats/utils.ts @@ -0,0 +1,44 @@ +import { ColumnForDataTableFragment } from "@gen/graphql-types"; +import { CommitsParams, DiffParams } from "@lib/params"; +import { getAllSelectColumns } from "@components/CellButtons/queryHelpers"; +import { isHiddenColumn } from "../DataDiff/utils"; + +type Props = { + params: Required & { + tableName: string; + }; + columns: ColumnForDataTableFragment[]; + hiddenColIndexes: number[]; +}; + +// Returns a dolt_commit_diff_$TABLENAME query that looks like: +// SELECT diff_type, `from_[col]`, `to_[col]`, [...], from_commit, from_commit_date, to_commit, to_commit_date +// FROM dolt_commit_diff_[tableName] +// WHERE from_commit="[fromCommitId]" AND to_commit="[toCommitId]" +export function getDoltCommitDiffQuery(props: Props): string { + const colsWithNamesAndVals = transformColsFromDiffCols( + props.columns, + props.hiddenColIndexes, + ); + const cols = getAllSelectColumns(colsWithNamesAndVals); + return `SELECT ${cols} FROM \`dolt_commit_diff_${ + props.params.tableName + }\`${getWhereClause(props.params)}`; +} + +function getWhereClause(params: CommitsParams): string { + return ` WHERE from_commit="${params.fromCommitId}" AND to_commit="${params.toCommitId}"`; +} + +// Get names and values for every column based on row value and dolt_commit_diff table +// column names, excluding hidden columns +export function transformColsFromDiffCols( + cols: ColumnForDataTableFragment[], + hiddenColIndexes: number[], +): Array<{ names: string[] }> { + return cols + .filter((_, i) => !isHiddenColumn(i, hiddenColIndexes)) + .map(col => { + return { names: [col.name] }; + }); +} diff --git a/packages/web/components/DiffTable/SchemaDiff/index.module.css b/packages/web/components/DiffTable/SchemaDiff/index.module.css new file mode 100644 index 00000000..b6209df0 --- /dev/null +++ b/packages/web/components/DiffTable/SchemaDiff/index.module.css @@ -0,0 +1,3 @@ +.container { + @apply mb-10 max-w-7xl; +} diff --git a/packages/web/components/DiffTable/SchemaDiff/index.tsx b/packages/web/components/DiffTable/SchemaDiff/index.tsx new file mode 100644 index 00000000..cb9f53e5 --- /dev/null +++ b/packages/web/components/DiffTable/SchemaDiff/index.tsx @@ -0,0 +1,27 @@ +import { SchemaDiffForTableListFragment } from "@gen/graphql-types"; +import Maybe from "@lib/Maybe"; +import ReactDiffViewer from "react-diff-viewer"; +import css from "./index.module.css"; + +type Props = { + schemaDiff?: Maybe; +}; + +export default function SchemaDiff({ schemaDiff }: Props) { + if (!schemaDiff) { + return
No schema changes in this diff
; + } + + const { leftLines, rightLines } = schemaDiff; + + return ( +
+ +
+ ); +} diff --git a/packages/web/components/DiffTable/SchemaPatch/index.module.css b/packages/web/components/DiffTable/SchemaPatch/index.module.css new file mode 100644 index 00000000..f5d3c639 --- /dev/null +++ b/packages/web/components/DiffTable/SchemaPatch/index.module.css @@ -0,0 +1,7 @@ +.patch { + @apply max-w-7xl; + + tr > td:first-of-type { + @apply hidden; + } +} diff --git a/packages/web/components/DiffTable/SchemaPatch/index.tsx b/packages/web/components/DiffTable/SchemaPatch/index.tsx new file mode 100644 index 00000000..ac8fd080 --- /dev/null +++ b/packages/web/components/DiffTable/SchemaPatch/index.tsx @@ -0,0 +1,24 @@ +import Maybe from "@lib/Maybe"; +import ReactDiffViewer from "react-diff-viewer"; +import css from "./index.module.css"; + +type Props = { + schemaPatch?: Maybe; +}; + +export default function SchemaPatch({ schemaPatch }: Props) { + if (!schemaPatch) { + return
No schema changes in this diff
; + } + const content = schemaPatch.join("\n"); + return ( +
+ +
+ ); +} diff --git a/packages/web/components/DiffTable/SchemaSection.tsx b/packages/web/components/DiffTable/SchemaSection.tsx new file mode 100644 index 00000000..4d89b15e --- /dev/null +++ b/packages/web/components/DiffTable/SchemaSection.tsx @@ -0,0 +1,52 @@ +import QueryHandler from "@components/util/QueryHandler"; +import { useDiffContext } from "@contexts/diff"; +import { + DiffSummaryFragment, + SchemaDiffFragment, + useSchemaDiffQuery, +} from "@gen/graphql-types"; +import Maybe from "@lib/Maybe"; +import SchemaDiff from "./SchemaDiff"; +import SchemaPatch from "./SchemaPatch"; +import css from "./index.module.css"; + +type Props = { + diffSummary: DiffSummaryFragment; +}; + +type InnerProps = { + schemaDiff?: Maybe; +}; + +function Inner({ schemaDiff }: InnerProps) { + const showSchemaPatch = + !!schemaDiff?.schemaPatch?.length && schemaDiff.schemaPatch[0] !== ""; + return ( +
+

Schema Diff

+ + {showSchemaPatch && ( +
+

Schema Patch

+ +
+ )} +
+ ); +} + +export default function SchemaSection(props: Props) { + const { params } = useDiffContext(); + const res = useSchemaDiffQuery({ + variables: { + ...params, + tableName: props.diffSummary.tableName, + }, + }); + return ( + } + /> + ); +} diff --git a/packages/web/components/DiffTable/TabButtons.tsx b/packages/web/components/DiffTable/TabButtons.tsx new file mode 100644 index 00000000..03e2beaf --- /dev/null +++ b/packages/web/components/DiffTable/TabButtons.tsx @@ -0,0 +1,38 @@ +import Btn from "@components/Btn"; +import cx from "classnames"; +import css from "./index.module.css"; + +type Props = { + showData: boolean; + setShowData: (s: boolean) => void; + hasSchemaChanges: boolean; + hasDataChanges: boolean; +}; + +export default function TabButtons({ + showData, + setShowData, + hasSchemaChanges, + hasDataChanges, +}: Props) { + return ( +
+ setShowData(true)} + className={cx({ [css.active]: showData })} + disabled={!hasDataChanges} + data-cy="data-button" + > + Data{!hasDataChanges ? " (0)" : ""} + + setShowData(false)} + className={cx({ [css.active]: !showData })} + disabled={!hasSchemaChanges} + data-cy="schema-button" + > + Schema{!hasSchemaChanges ? " (0)" : ""} + +
+ ); +} diff --git a/packages/web/components/DiffTable/index.module.css b/packages/web/components/DiffTable/index.module.css new file mode 100644 index 00000000..4550395f --- /dev/null +++ b/packages/web/components/DiffTable/index.module.css @@ -0,0 +1,43 @@ +.container { + @apply my-8; +} + +.header { + @apply mx-4 mb-4 flex items-center justify-between; + + h1 { + @apply mr-4; + } +} + +.diffStat { + @apply mx-4 mb-12 lg:mb-4; +} + +.schemaSection { + @apply mx-6 pt-7 border-t border-ld-lightgrey overflow-x-auto lg:overflow-x-visible; + + h2 { + @apply mb-2; + } +} + +.buttons { + @apply w-full mx-2 text-acc-hoverlinkblue; + + button { + @apply pb-2 mr-4 px-6 font-semibold; + + &:focus { + @apply outline-none widget-shadow-lightblue; + } + + &:disabled { + @apply text-ld-darkgrey cursor-default; + } + } +} + +.active { + @apply text-ld-orange border-b-3 border-ld-orange; +} diff --git a/packages/web/components/DiffTable/index.tsx b/packages/web/components/DiffTable/index.tsx new file mode 100644 index 00000000..8fd203ed --- /dev/null +++ b/packages/web/components/DiffTable/index.tsx @@ -0,0 +1,90 @@ +import StatusWithOptions from "@components/StatusWithOptions"; +import { useDiffContext } from "@contexts/diff"; +import { DiffSummaryFragment, TableDiffType } from "@gen/graphql-types"; +import { DatabaseParams, RequiredCommitsParams } from "@lib/params"; +import { useEffect, useState } from "react"; +import DataSection from "./DataSection"; +import DiffTableStats from "./DiffTableStats"; +import SchemaSection from "./SchemaSection"; +import TabButtons from "./TabButtons"; +import css from "./index.module.css"; + +type Props = { + params: DatabaseParams; + hideCellButtons?: boolean; +}; + +type InnerProps = { + diffSummary: DiffSummaryFragment; + hideCellButtons?: boolean; +}; + +export function Inner({ diffSummary, hideCellButtons }: InnerProps) { + const { params, refName } = useDiffContext(); + const [hiddenColIndexes, setHiddenColIndexes] = useState([]); + const displayedTableName = + diffSummary.tableType === TableDiffType.Renamed + ? `${diffSummary.fromTableName} → ${diffSummary.toTableName}` + : diffSummary.tableName; + + const [showData, setShowData] = useState(diffSummary.hasDataChanges); + + useEffect(() => { + setShowData(diffSummary.hasDataChanges); + }, [diffSummary.hasDataChanges, displayedTableName]); + + return ( +
+
+

{displayedTableName}

+ {isShowingUncommittedChanges(params) && ( + + )} +
+
+ +
+ + {showData ? ( + + ) : ( + + )} +
+ ); +} + +export default function DiffTable(props: Props) { + const { activeTableName, diffSummaries } = useDiffContext(); + + if (!activeTableName) return null; + const currentDiffSummary = diffSummaries.find( + ds => + ds.fromTableName === activeTableName || + ds.toTableName === activeTableName, + ); + if (!currentDiffSummary) return null; + return ; +} + +function isShowingUncommittedChanges(params: RequiredCommitsParams): boolean { + return ( + params.fromCommitId === "WORKING" || + params.toCommitId === "WORKING" || + params.fromCommitId === "STAGED" || + params.toCommitId === "STAGED" + ); +} diff --git a/packages/web/components/DiffTable/queries.ts b/packages/web/components/DiffTable/queries.ts new file mode 100644 index 00000000..d2dd89f1 --- /dev/null +++ b/packages/web/components/DiffTable/queries.ts @@ -0,0 +1,88 @@ +import { gql } from "@apollo/client"; + +export const ROW_DIFFS = gql` + fragment ColumnForDiffTableList on Column { + name + isPrimaryKey + type + constraints { + notNull + } + } + fragment ColumnValueForTableList on ColumnValue { + displayValue + } + fragment RowForTableList on Row { + columnValues { + ...ColumnValueForTableList + } + } + fragment RowDiffForTableList on RowDiff { + added { + ...RowForTableList + } + deleted { + ...RowForTableList + } + } + fragment RowDiffListWithCols on RowDiffList { + list { + ...RowDiffForTableList + } + columns { + ...ColumnForDiffTableList + } + nextOffset + } + query RowDiffs( + $databaseName: String! + $tableName: String! + $fromCommitId: String! + $toCommitId: String! + $refName: String + $offset: Int + $filterByRowType: DiffRowType + ) { + rowDiffs( + databaseName: $databaseName + tableName: $tableName + fromCommitId: $fromCommitId + toCommitId: $toCommitId + refName: $refName + offset: $offset + filterByRowType: $filterByRowType + ) { + ...RowDiffListWithCols + } + } +`; + +export const SCHEMA_DIFF = gql` + fragment SchemaDiffForTableList on TextDiff { + leftLines + rightLines + } + fragment SchemaDiff on SchemaDiff { + schemaDiff { + ...SchemaDiffForTableList + } + schemaPatch + } + query SchemaDiff( + $databaseName: String! + $tableName: String! + $fromCommitId: String! + $toCommitId: String! + $refName: String + ) { + schemaDiff( + databaseName: $databaseName + tableName: $tableName + fromCommitId: $fromCommitId + toCommitId: $toCommitId + refName: $refName + ) { + ...SchemaDiff + } + } +`; diff --git a/packages/web/components/DiffTableNav/BackButton.tsx b/packages/web/components/DiffTableNav/BackButton.tsx new file mode 100644 index 00000000..aab2ff09 --- /dev/null +++ b/packages/web/components/DiffTableNav/BackButton.tsx @@ -0,0 +1,43 @@ +import Button from "@components/Button"; +import Loader from "@components/Loader"; +import CommitLogLink from "@components/links/CommitLogLink"; +import useDefaultBranch from "@hooks/useDefaultBranch"; +import { DatabaseParams } from "@lib/params"; +import { FaChevronLeft } from "@react-icons/all-files/fa/FaChevronLeft"; +import cx from "classnames"; +import css from "./index.module.css"; + +type Params = DatabaseParams & { + refName?: string; +}; + +type Props = { + params: Params; + open: boolean; +}; + +export default function BackButton(props: Props) { + const { defaultBranchName, loading } = useDefaultBranch(props.params); + return ( +
+ + + + + +
+ ); +} diff --git a/packages/web/components/DiffTableNav/CommitInfo/Inner.tsx b/packages/web/components/DiffTableNav/CommitInfo/Inner.tsx new file mode 100644 index 00000000..108ed6a7 --- /dev/null +++ b/packages/web/components/DiffTableNav/CommitInfo/Inner.tsx @@ -0,0 +1,54 @@ +import CommitLink from "@components/links/CommitLink"; +import { CommitForAfterCommitHistoryFragment } from "@gen/graphql-types"; +import { RefParams } from "@lib/params"; +import { pluralize } from "@lib/pluralize"; +import ReactTimeago from "react-timeago"; +import css from "./index.module.css"; + +type InnerProps = { + params: RefParams; + toCommitInfo: CommitForAfterCommitHistoryFragment; +}; + +export default function Inner({ toCommitInfo, params }: InnerProps) { + const { parents } = toCommitInfo; + const parentLen = parents.length; + return ( +
+
+
+ Viewing changes from{" "} + {toCommitInfo.commitId} +
+
+
+
+ {toCommitInfo.message} +
+
+ + {toCommitInfo.committer.displayName} + {" "} + committed +
+
+ {parentLen} {pluralize(parentLen, "parent")}:{" "} + {parents.map((p, i) => ( + + + + {shortCommit(p)} + + + {i !== parentLen - 1 && +} + + ))} +
+
+
+ ); +} + +export function shortCommit(commitId: string): string { + return commitId.slice(0, 7); +} diff --git a/packages/web/components/DiffTableNav/CommitInfo/index.module.css b/packages/web/components/DiffTableNav/CommitInfo/index.module.css new file mode 100644 index 00000000..89061a5b --- /dev/null +++ b/packages/web/components/DiffTableNav/CommitInfo/index.module.css @@ -0,0 +1,51 @@ +.container { + @apply mt-3 mb-5 px-2 lg:px-0; +} + +.top { + @apply mx-3 mb-3; +} + +.hash { + @apply font-mono font-semibold text-sm; +} + +.browse { + @apply px-0 mb-2 mt-1; +} + +.infoContainer { + @apply border-y-2 text-sm py-4 px-3; +} + +.commitMsg { + @apply break-words; +} + +.committer { + @apply mt-0.5 mb-2.5 text-ld-darkgrey; +} + +.userLink { + @apply text-ld-darkgrey; + + &:hover { + @apply text-ld-darkergrey; + } +} + +.parentHash { + @apply font-mono text-xs; + + a { + @apply text-primary; + + &:hover { + @apply text-acc-hoverblue; + } + } +} + +.plus { + @apply mx-1; +} diff --git a/packages/web/components/DiffTableNav/CommitInfo/index.test.tsx b/packages/web/components/DiffTableNav/CommitInfo/index.test.tsx new file mode 100644 index 00000000..a20c6602 --- /dev/null +++ b/packages/web/components/DiffTableNav/CommitInfo/index.test.tsx @@ -0,0 +1,81 @@ +import { MockedProvider } from "@apollo/client/testing"; +import useMockRouter from "@hooks/useMockRouter"; +import { render, screen } from "@testing-library/react"; +import { commit as commitLink, diff } from "@lib/urls"; +import CommitInfo, { getDiffRange } from "."; +import * as mocks from "./mocks"; + +const jestRouter = jest.spyOn(require("next/router"), "useRouter"); + +jest.mock("next/router", () => { + return { + useRouter: () => { + return { route: "", pathname: "", query: "", asPath: "" }; + }, + }; +}); + +describe("test CommitInfo", () => { + mocks.tests.forEach(test => { + it(`renders CommitInfo for ${test.desc}`, async () => { + useMockRouter(jestRouter, { + asPath: diff({ + ...mocks.params, + fromCommitId: test.commitId, + }).asPathname(), + }); + + const commit = mocks.commitOpts[test.commitId]; + render( + + + , + ); + + expect(screen.getByRole("progressbar")).toBeInTheDocument(); + + await screen.findByText(test.commitId); + expect(screen.getByText(commit.message)).toBeInTheDocument(); + expect( + screen.getByText(commit.committer.displayName), + ).toBeInTheDocument(); + + if (test.expectedParents === 1) { + expect(screen.getByText("1 parent:")).toBeInTheDocument(); + expect(screen.queryByText("+")).not.toBeInTheDocument(); + } else { + expect(screen.getByText("2 parents:")).toBeVisible(); + expect(screen.getByText("+")).toBeInTheDocument(); + } + + commit.parents.forEach(parent => { + expect(screen.getByText(parent.slice(0, 7))).toHaveAttribute( + "href", + commitLink({ ...mocks.params, commitId: parent }).asPathname(), + ); + }); + }); + }); + + it("renders abbreviated CommitInfo for diff range", () => { + const { fromCommitId } = mocks; + const toCommitId = mocks.commitOneParent; + useMockRouter(jestRouter, { + asPath: diff({ ...mocks.params, fromCommitId, toCommitId }).asPathname(), + }); + render( + , + ); + + expect( + screen.getByText(getDiffRange(fromCommitId, toCommitId)), + ).toBeVisible(); + expect(screen.queryByText("1 parent")).not.toBeInTheDocument(); + }); +}); diff --git a/packages/web/components/DiffTableNav/CommitInfo/index.tsx b/packages/web/components/DiffTableNav/CommitInfo/index.tsx new file mode 100644 index 00000000..003348f7 --- /dev/null +++ b/packages/web/components/DiffTableNav/CommitInfo/index.tsx @@ -0,0 +1,60 @@ +import QueryHandler from "@components/util/QueryHandler"; +import { useHistoryForCommitQuery } from "@gen/graphql-types"; +import { RefParams } from "@lib/params"; +import { useRouter } from "next/router"; +import Inner, { shortCommit } from "./Inner"; +import css from "./index.module.css"; + +type Props = { + params: RefParams & { toCommitId: string; fromCommitId?: string }; +}; + +function Query({ params }: Props) { + const res = useHistoryForCommitQuery({ + variables: { + databaseName: params.databaseName, + afterCommitId: params.toCommitId, + }, + }); + + return ( + ( + + )} + /> + ); +} + +export default function CommitInfo({ params }: Props) { + const router = useRouter(); + // Only use fromCommitId in Commit if it is present in the route + const fromCommitForInfo = isDiffRange(router.asPath) + ? params.fromCommitId + : undefined; + + if (fromCommitForInfo) { + return ( +
+
+
+ Viewing changes from{" "} + + {getDiffRange(fromCommitForInfo, params.toCommitId)} + +
+
+
+ ); + } + return ; +} + +export function getDiffRange(fromCommit: string, toCommit: string): string { + return `${shortCommit(fromCommit)}..${shortCommit(toCommit)}`; +} + +function isDiffRange(asPath: string) { + return asPath.includes(".."); +} diff --git a/packages/web/components/DiffTableNav/CommitInfo/mocks.ts b/packages/web/components/DiffTableNav/CommitInfo/mocks.ts new file mode 100644 index 00000000..e035057d --- /dev/null +++ b/packages/web/components/DiffTableNav/CommitInfo/mocks.ts @@ -0,0 +1,85 @@ +import { MockedResponse } from "@apollo/client/testing"; +import { fakeCommitId } from "@components/CustomFormSelect/mocks"; +import { + CommitForAfterCommitHistoryFragment, + HistoryForCommitDocument, +} from "@gen/graphql-types"; +import chance from "@lib/chance"; +import { RefParams } from "@lib/params"; + +export const params: RefParams = { + refName: "main", + databaseName: "dbname", +}; +const date = Date.now(); + +const commitFrag = ( + commitId: string, + commit?: Partial, +): CommitForAfterCommitHistoryFragment => { + const username = chance.word(); + return { + __typename: "Commit", + _id: `database/${params.databaseName}/commits/${commitId}`, + commitId, + message: chance.sentence(), + committedAt: date, + parents: [fakeCommitId()], + committer: { + __typename: "DoltWriter", + _id: `${username}@gmail.com`, + displayName: chance.name(), + username, + }, + ...commit, + }; +}; + +const commitTwoParents = fakeCommitId(); +export const commitOneParent = fakeCommitId(); +const commitTwoSameParents = fakeCommitId(); +const parent = fakeCommitId(); +export const fromCommitId = fakeCommitId(); + +export const commitOpts: Record = { + [commitTwoParents]: commitFrag(commitTwoParents, { + parents: [fakeCommitId(), fakeCommitId()], + }), + [commitOneParent]: commitFrag(commitOneParent), + [commitTwoSameParents]: commitFrag(commitTwoSameParents, { + parents: [parent, parent], + }), +}; + +export const tests = [ + { + desc: "commit with two parents", + commitId: commitTwoParents, + expectedParents: 2, + }, + { + desc: "commit with one parent", + commitId: commitOneParent, + expectedParents: 1, + }, +]; + +export const commitsQuery = (afterCommitId: string): MockedResponse => { + return { + request: { + query: HistoryForCommitDocument, + variables: { + databaseName: params.databaseName, + afterCommitId, + }, + }, + result: { + data: { + commits: { + __typename: "CommitList", + list: [commitOpts[afterCommitId]], + }, + }, + }, + }; +}; diff --git a/packages/web/components/DiffTableNav/DiffTableStats/ListItem.tsx b/packages/web/components/DiffTableNav/DiffTableStats/ListItem.tsx new file mode 100644 index 00000000..2828ea70 --- /dev/null +++ b/packages/web/components/DiffTableNav/DiffTableStats/ListItem.tsx @@ -0,0 +1,39 @@ +import { DiffSummaryFragment } from "@gen/graphql-types"; +import { RequiredCommitsParams } from "@lib/params"; +import cx from "classnames"; +import StatIcon from "./StatIcon"; +import TableName from "./TableName"; +import TableStat from "./TableStat"; +import css from "./index.module.css"; + +type Props = { + diffSummary: DiffSummaryFragment; + params: RequiredCommitsParams & { refName: string }; + isActive: boolean; +}; + +export default function ListItem(props: Props) { + return ( +
  • + + + + + + + +
  • + ); +} diff --git a/packages/web/components/DiffTableNav/DiffTableStats/StatIcon.tsx b/packages/web/components/DiffTableNav/DiffTableStats/StatIcon.tsx new file mode 100644 index 00000000..563365fe --- /dev/null +++ b/packages/web/components/DiffTableNav/DiffTableStats/StatIcon.tsx @@ -0,0 +1,22 @@ +import { TableDiffType } from "@gen/graphql-types"; +import { GoDiffAdded } from "@react-icons/all-files/go/GoDiffAdded"; +import { GoDiffModified } from "@react-icons/all-files/go/GoDiffModified"; +import { GoDiffRemoved } from "@react-icons/all-files/go/GoDiffRemoved"; +import { GoDiffRenamed } from "@react-icons/all-files/go/GoDiffRenamed"; + +type Props = { + tableType: TableDiffType; +}; + +export default function StatIcon({ tableType }: Props) { + switch (tableType) { + case TableDiffType.Added: + return ; + case TableDiffType.Dropped: + return ; + case TableDiffType.Renamed: + return ; + default: + return ; + } +} diff --git a/packages/web/components/DiffTableNav/DiffTableStats/TableName.tsx b/packages/web/components/DiffTableNav/DiffTableStats/TableName.tsx new file mode 100644 index 00000000..47a0d2f5 --- /dev/null +++ b/packages/web/components/DiffTableNav/DiffTableStats/TableName.tsx @@ -0,0 +1,50 @@ +import Button from "@components/Button"; +import Link from "@components/links/Link"; +import { useDiffContext } from "@contexts/diff"; +import { DiffSummaryFragment, TableDiffType } from "@gen/graphql-types"; +import { useRouter } from "next/router"; + +type TableProps = { + diffSummary: DiffSummaryFragment; + displayedTableName: string; +}; + +type Props = { + isActive: boolean; + diffSummary: DiffSummaryFragment; +}; + +function TableLink({ displayedTableName, diffSummary }: TableProps) { + const router = useRouter(); + const { stayWithinPage, setActiveTableName } = useDiffContext(); + const queryDelimiter = router.asPath.includes("?refName") ? "&" : "?"; + const asPathWithoutQuery = router.asPath.split( + `${queryDelimiter}tableName=`, + )[0]; + const url = { + href: `${asPathWithoutQuery}${queryDelimiter}tableName=${diffSummary.tableName}`, + as: `${asPathWithoutQuery}${queryDelimiter}tableName=${diffSummary.tableName}`, + }; + + if (stayWithinPage) { + return ( + setActiveTableName(diffSummary.tableName)}> + {displayedTableName} + + ); + } + return {displayedTableName}; +} + +export default function TableName(props: Props) { + const displayedTableName = + props.diffSummary.tableType === TableDiffType.Renamed + ? `${props.diffSummary.fromTableName} → ${props.diffSummary.toTableName}` + : props.diffSummary.tableName; + + return props.isActive ? ( + {displayedTableName} + ) : ( + + ); +} diff --git a/packages/web/components/DiffTableNav/DiffTableStats/TableStat.tsx b/packages/web/components/DiffTableNav/DiffTableStats/TableStat.tsx new file mode 100644 index 00000000..194934ee --- /dev/null +++ b/packages/web/components/DiffTableNav/DiffTableStats/TableStat.tsx @@ -0,0 +1,92 @@ +import SummaryStat from "@components/DiffStat/SummaryStat"; +import ErrorMsg from "@components/ErrorMsg"; +import SmallLoader from "@components/SmallLoader"; +import { + DiffStatForDiffsFragment, + DiffSummaryFragment, + TableDiffType, + useDiffStatQuery, +} from "@gen/graphql-types"; +import { gqlErrorPrimaryKeyChange } from "@lib/errors/graphql"; +import { errorMatches } from "@lib/errors/helpers"; +import { DiffParamsWithRefs } from "@lib/params"; +import cx from "classnames"; +import css from "./index.module.css"; + +type Props = { + diffSummary: DiffSummaryFragment; + params: DiffParamsWithRefs & { refName: string }; +}; + +type InnerProps = { + diffStat?: DiffStatForDiffsFragment; + diffSummary: DiffSummaryFragment; + loading: boolean; + error?: Error; +}; + +function Inner({ diffStat, diffSummary, loading, error }: InnerProps) { + if (loading) { + return ( +
    + +
    + ); + } + + const isPrimaryKeyChange = errorMatches(gqlErrorPrimaryKeyChange, error); + if (isPrimaryKeyChange) { + return null; + } + + if (diffStat && !error) { + return ( + <> + + + + + + ); + } + + return ( +
    + +
    + ); +} + +export default function TableStat(props: Props) { + const res = useDiffStatQuery({ + variables: { ...props.params, tableName: props.diffSummary.tableName }, + }); + return ( +
    + +
    + ); +} diff --git a/packages/web/components/DiffTableNav/DiffTableStats/index.module.css b/packages/web/components/DiffTableNav/DiffTableStats/index.module.css new file mode 100644 index 00000000..677cf8d7 --- /dev/null +++ b/packages/web/components/DiffTableNav/DiffTableStats/index.module.css @@ -0,0 +1,43 @@ +.list { + @apply mb-8 lg:mb-20; +} + +.tableInfo { + @apply py-2 pr-3 flex justify-between whitespace-nowrap overflow-x-auto; + + > div { + @apply flex flex-row; + } +} + +.name { + @apply ml-2 text-sm font-semibold text-ld-darkgrey flex items-center; +} + +.icon { + @apply text-lg mr-3; +} + +.marLeft { + @apply ml-4; +} + +.err { + @apply inline-block pt-0 ml-4 text-sm; +} + +.loading { + @apply text-sm; +} + +.iconAdded { + @apply text-acc-green; +} + +.iconDropped { + @apply text-acc-red; +} + +.iconModified { + @apply text-yellow-500; +} diff --git a/packages/web/components/DiffTableNav/DiffTableStats/index.tsx b/packages/web/components/DiffTableNav/DiffTableStats/index.tsx new file mode 100644 index 00000000..0ded9a41 --- /dev/null +++ b/packages/web/components/DiffTableNav/DiffTableStats/index.tsx @@ -0,0 +1,34 @@ +import { useDiffContext } from "@contexts/diff"; +import { DiffSummaryFragment } from "@gen/graphql-types"; +import cx from "classnames"; +import ListItem from "./ListItem"; +import css from "./index.module.css"; + +type Props = { + className?: string; +}; + +export default function DiffTableStats(props: Props) { + const { diffSummaries, params, activeTableName, refName } = useDiffContext(); + return ( +
      + {diffSummaries.map(ds => ( + + ))} +
    + ); +} + +function tableIsActive( + ds: DiffSummaryFragment, + activeTableName: string, +): boolean { + return ( + ds.fromTableName === activeTableName || ds.toTableName === activeTableName + ); +} diff --git a/packages/web/components/DiffTableNav/ForCommits.tsx b/packages/web/components/DiffTableNav/ForCommits.tsx new file mode 100644 index 00000000..833ccbfd --- /dev/null +++ b/packages/web/components/DiffTableNav/ForCommits.tsx @@ -0,0 +1,38 @@ +import DiffStat from "@components/DiffStat"; +import { useDiffContext } from "@contexts/diff"; +import { RefParams } from "@lib/params"; +import CommitInfo from "./CommitInfo"; +import DiffTableStats from "./DiffTableStats"; +import DiffTableNav from "./component"; + +type Props = { + params: RefParams; +}; + +export default function ForCommits(props: Props) { + const { params, refName } = useDiffContext(); + return ( + + } + diffSelector={ + + } + diffTables={} + /> + ); +} diff --git a/packages/web/components/DiffTableNav/ForRef.tsx b/packages/web/components/DiffTableNav/ForRef.tsx new file mode 100644 index 00000000..71388c37 --- /dev/null +++ b/packages/web/components/DiffTableNav/ForRef.tsx @@ -0,0 +1,23 @@ +import DiffSelector from "@components/DiffSelector"; +import { RefParams } from "@lib/params"; +import DiffTableNav from "./component"; +import css from "./index.module.css"; + +type Props = { + params: RefParams; +}; + +export default function ForRef(props: Props) { + return ( + + Select commit to view diff +

    + } + diffSelector={} + diffTables={null} + /> + ); +} diff --git a/packages/web/components/DiffTableNav/ForWorking.tsx b/packages/web/components/DiffTableNav/ForWorking.tsx new file mode 100644 index 00000000..bc9d7726 --- /dev/null +++ b/packages/web/components/DiffTableNav/ForWorking.tsx @@ -0,0 +1,32 @@ +import DiffStat from "@components/DiffStat"; +import { RequiredCommitsParams } from "@lib/params"; +import cx from "classnames"; +import DiffTableStats from "./DiffTableStats"; +import css from "./index.module.css"; + +type Props = { + params: RequiredCommitsParams & { refName: string }; +}; + +export default function ForWorking(props: Props) { + return ( +
    +
    +
    +

    Working Changes

    + +
    + +
    +
    + ); +} diff --git a/packages/web/components/DiffTableNav/component.tsx b/packages/web/components/DiffTableNav/component.tsx new file mode 100644 index 00000000..581a6258 --- /dev/null +++ b/packages/web/components/DiffTableNav/component.tsx @@ -0,0 +1,54 @@ +import { DatabaseParams } from "@lib/params"; +import { GiHamburgerMenu } from "@react-icons/all-files/gi/GiHamburgerMenu"; +import cx from "classnames"; +import { ReactNode, useState } from "react"; +import BackButton from "./BackButton"; +import css from "./index.module.css"; + +type Params = DatabaseParams & { + refName?: string; +}; + +type Props = { + params: Params; + diffStat: ReactNode; + diffSelector?: ReactNode; + diffTables?: ReactNode; + white?: boolean; +}; + +export default function DiffTableNav({ white = false, ...props }: Props) { + const [open, setOpen] = useState(true); + const toggleMenu = () => { + setOpen(!open); + }; + + return ( +
    + {!!props.diffSelector && ( +
    + + +
    + )} +
    + {props.diffSelector} +
    +

    Diff Overview

    + {props.diffStat} +
    + {props.diffTables} +
    +
    + ); +} diff --git a/packages/web/components/DiffTableNav/index.module.css b/packages/web/components/DiffTableNav/index.module.css new file mode 100644 index 00000000..c0492402 --- /dev/null +++ b/packages/web/components/DiffTableNav/index.module.css @@ -0,0 +1,91 @@ +.container { + @apply bg-ld-lightpurple pt-3 flex flex-col; +} + +.whiteBg { + @apply bg-white; +} + +.openContainer { + @apply transition-width duration-75 basis-80 mx-4; + + @screen lg { + @apply w-96 mx-0; + } +} + +.closedContainer { + @apply transition-width w-16 duration-75 basis-12; +} + +.top { + @apply flex justify-between w-full px-3; +} + +.openItem { + @apply opacity-100 transition-opacity duration-75 overflow-y-auto; +} + +.closedItem { + @apply hidden; +} + +.menuIcon { + @apply text-ld-blue cursor-pointer mt-2.5 ml-2 mr-2 pb-0 mb-5 hidden sm:block; +} + +.backButton { + @apply mr-4 mt-1; + + button { + @apply px-1 bg-opacity-0 shadow-none border-none text-acc-hoverlinkblue; + + &:hover, + &:focus { + @apply bg-opacity-0 shadow-none border-none text-acc-linkblue; + } + + @screen lg { + @apply w-48 rounded py-1 outline-none text-white text-sm font-semibold leading-relaxed button-shadow bg-acc-hoverlinkblue; + + &:hover { + @apply button-shadow-hover bg-acc-linkblue text-white; + } + + &:focus { + @apply outline-none widget-shadow-lightblue bg-acc-linkblue text-white; + } + } + } +} + +.chevron { + @apply inline-block mr-2 mb-1; +} + +.overview { + h1, + h4 { + @apply mx-3 mt-4; + } +} + +.wkOverview { + @apply flex flex-wrap border-b border-ld-lightgrey mb-2; + + h1 { + @apply mt-4 mr-12; + } +} + +.noDiff { + @apply m-4 pb-60 text-center; +} + +.wkSummary { + @apply border-none; +} + +.wkTables { + @apply max-w-lg mb-0; +} diff --git a/packages/web/components/DiffTableNav/index.tsx b/packages/web/components/DiffTableNav/index.tsx new file mode 100644 index 00000000..3c172560 --- /dev/null +++ b/packages/web/components/DiffTableNav/index.tsx @@ -0,0 +1,10 @@ +import ForCommits from "./ForCommits"; +import ForRef from "./ForRef"; +import ForWorking from "./ForWorking"; +import DiffTableNav from "./component"; + +export default Object.assign(DiffTableNav, { + ForCommits, + ForRef, + ForWorking, +}); diff --git a/packages/web/components/SqlDataTable/SqlMessage/SuccessMsg.tsx b/packages/web/components/SqlDataTable/SqlMessage/SuccessMsg.tsx index d5455a97..d5da593c 100644 --- a/packages/web/components/SqlDataTable/SqlMessage/SuccessMsg.tsx +++ b/packages/web/components/SqlDataTable/SqlMessage/SuccessMsg.tsx @@ -1,7 +1,10 @@ +import Link from "@components/links/Link"; import { SqlQueryParams } from "@lib/params"; import { isMutation } from "@lib/parseSqlQuery"; import { pluralize } from "@lib/pluralize"; +import { ref } from "@lib/urls"; import css from "./index.module.css"; +import { parseQuery } from "./utils"; type Props = { executionMessage?: string; @@ -10,22 +13,22 @@ type Props = { }; export default function SuccessMsg(props: Props) { - // const lower = props.params.q.toLowerCase(); + const lower = props.params.q.toLowerCase(); if (isMutation(props.params.q)) { return (
    - {/* {lower.startsWith("use") && } */} + {lower.startsWith("use") && }

    {props.executionMessage?.length ? props.executionMessage : "Query OK."}

    - {/* {lower.includes("dolt_checkout") && ( + {lower.includes("dolt_checkout") && (

    Warning: using DOLT_CHECKOUT to change a branch from the workbench will not work. Please use the branch dropdown.

    - )} */} + )}
    ); } @@ -36,3 +39,26 @@ export default function SuccessMsg(props: Props) {

    ); } + +function BranchMsg(props: { params: SqlQueryParams }) { + const { branchName, databaseName } = parseQuery(props.params.q); + return ( +

    + Cannot use USE statements to change branches from the + workbench. Please use the branch dropdown.{" "} + {branchName && ( + + Change to "{databaseName ? `${databaseName}/` : ""} + {branchName}". + + )} +
    +

    + ); +} diff --git a/packages/web/components/SqlDataTable/SqlMessage/index.module.css b/packages/web/components/SqlDataTable/SqlMessage/index.module.css index bd318e64..899893b1 100644 --- a/packages/web/components/SqlDataTable/SqlMessage/index.module.css +++ b/packages/web/components/SqlDataTable/SqlMessage/index.module.css @@ -1,5 +1,6 @@ .status { @apply my-3 mx-6 text-base; + @screen lg { @apply mx-5; } diff --git a/packages/web/components/SqlDataTable/SqlMessage/index.test.tsx b/packages/web/components/SqlDataTable/SqlMessage/index.test.tsx index c3967ad8..1b07e77f 100644 --- a/packages/web/components/SqlDataTable/SqlMessage/index.test.tsx +++ b/packages/web/components/SqlDataTable/SqlMessage/index.test.tsx @@ -5,7 +5,7 @@ import { render, screen } from "@testing-library/react"; import SqlMessage from "."; const params: SqlQueryParams = { - databaseName: "test", + databaseName: "dbname", refName: "master", q: "SELECT * FROM tablename", }; diff --git a/packages/web/components/SqlDataTable/SqlMessage/index.tsx b/packages/web/components/SqlDataTable/SqlMessage/index.tsx index fb4cd519..b78b1b4a 100644 --- a/packages/web/components/SqlDataTable/SqlMessage/index.tsx +++ b/packages/web/components/SqlDataTable/SqlMessage/index.tsx @@ -24,10 +24,13 @@ type Props = TimeoutProps & { export default function SqlMessage(props: Props) { if (props.gqlError) { - return isTimeoutError(props.gqlError.message) || - props.gqlError.message === "" ? ( - - ) : ( + if ( + isTimeoutError(props.gqlError.message) || + props.gqlError.message === "" + ) { + return ; + } + return ( ; + if (error) return ; + + return ( +
    + + +
    + ); +} + +export default function WorkingDiff(props: Props) { + const fromCommitId = "HEAD"; + const toCommitId = "WORKING"; + const params = { ...props.params, toCommitId, fromCommitId }; + return ( + + + + + + ); +} diff --git a/packages/web/components/SqlDataTable/index.tsx b/packages/web/components/SqlDataTable/index.tsx index 42863a33..d8e81476 100644 --- a/packages/web/components/SqlDataTable/index.tsx +++ b/packages/web/components/SqlDataTable/index.tsx @@ -19,6 +19,8 @@ import { isMutation } from "@lib/parseSqlQuery"; import { refetchSqlUpdateQueriesCacheEvict } from "@lib/refetchQueries"; import { useEffect, useState } from "react"; import SqlMessage from "./SqlMessage"; +import { isReadOnlyDatabaseRevisionError } from "./SqlMessage/utils"; +import WorkingDiff from "./WorkingDiff"; import css from "./index.module.css"; type Props = { @@ -67,12 +69,9 @@ function Inner(props: InnerProps) { message={msg} /> - {/* {isMut && !isReadOnlyDatabaseRevisionError(props.gqlError) && ( - <> - - - - )} */} + {isMut && !isReadOnlyDatabaseRevisionError(props.gqlError) && ( + + )} ); } @@ -80,7 +79,8 @@ function Inner(props: InnerProps) { function Query(props: Props) { const { data, loading, error, client } = useSqlSelectForSqlDataTableQuery({ variables: { - ...props.params, + databaseName: props.params.databaseName, + refName: props.params.refName, queryString: props.params.q, }, fetchPolicy: "cache-and-network", @@ -112,7 +112,7 @@ export default function SqlDataTable(props: Props) { Warning: You recently ran this query. Are you sure you want to run it again? - {/* */} + ); } diff --git a/packages/web/components/SqlDataTable/queries.ts b/packages/web/components/SqlDataTable/queries.ts index 75296e5a..8d084194 100644 --- a/packages/web/components/SqlDataTable/queries.ts +++ b/packages/web/components/SqlDataTable/queries.ts @@ -21,7 +21,6 @@ export const SQL_SELECT_QUERY = gql` refName: $refName queryString: $queryString ) { - _id queryExecutionStatus queryExecutionMessage columns { diff --git a/packages/web/components/breadcrumbs/CommitDiffBreadcrumbs.tsx b/packages/web/components/breadcrumbs/CommitDiffBreadcrumbs.tsx new file mode 100644 index 00000000..d7cafca0 --- /dev/null +++ b/packages/web/components/breadcrumbs/CommitDiffBreadcrumbs.tsx @@ -0,0 +1,26 @@ +import { RefParams } from "@lib/params"; +import { useRouter } from "next/router"; +import Breadcrumbs from "."; +import { commitDiffBreadcrumbDetails } from "./breadcrumbDetails"; +import { getDiffRangeFromString } from "./utils"; + +type Props = { + params: RefParams; + className?: string; +}; + +export default function CommitDiffBreadcrumbs({ params, ...props }: Props) { + const router = useRouter(); + const diffRange = getDiffRangeFromString( + "choose a commit", + router.query.diffRange, + ); + return ( + + ); +} diff --git a/packages/web/components/breadcrumbs/breadcrumbDetails.tsx b/packages/web/components/breadcrumbs/breadcrumbDetails.tsx index 0f1f0004..da021116 100644 --- a/packages/web/components/breadcrumbs/breadcrumbDetails.tsx +++ b/packages/web/components/breadcrumbs/breadcrumbDetails.tsx @@ -1,7 +1,9 @@ import DatabasesDropdown from "@components/DatabasesDropdown"; +import CommitLogLink from "@components/links/CommitLogLink"; import Link from "@components/links/Link"; import { DatabaseParams, + DiffRangeParams, RefParams, SqlQueryParams, TableParams, @@ -201,3 +203,23 @@ export function docBreadcrumbsDetails( }, ]; } + +export function commitDiffBreadcrumbDetails( + params: DiffRangeParams, +): BreadcrumbDetails[] { + const commitLogLink = commits; + const commitDiffText = {params.diffRange}; + return [ + ...databaseBreadcrumbs(params), + { + child: commitLogLink, + name: BreadcrumbName.DBCommitLog, + type: BreadcrumbType.Link, + }, + { + child: commitDiffText, + name: BreadcrumbName.DBCommitDiff, + type: BreadcrumbType.Text, + }, + ]; +} diff --git a/packages/web/components/layouts/DatabaseLayout/index.tsx b/packages/web/components/layouts/DatabaseLayout/index.tsx index fb8db939..81e6f93a 100644 --- a/packages/web/components/layouts/DatabaseLayout/index.tsx +++ b/packages/web/components/layouts/DatabaseLayout/index.tsx @@ -28,6 +28,7 @@ type Props = { routeRefChangeTo?: RefUrl; initialSmallHeader?: boolean; smallHeaderBreadcrumbs?: ReactNode; + leftTableNav?: ReactNode; }; export default function DatabaseLayout(props: Props) { @@ -40,7 +41,6 @@ export default function DatabaseLayout(props: Props) { const useFullWidth = forDataTable || !!props.wide; const { isMobile } = useReactiveWidth(null, 1024); const [showTableNav, setShowTableNav] = useState(false); - return ( - + {props.leftTableNav || ( + + )}
    {!!showHeader && diff --git a/packages/web/components/links/CommitLogLink.tsx b/packages/web/components/links/CommitLogLink.tsx new file mode 100644 index 00000000..8d949a11 --- /dev/null +++ b/packages/web/components/links/CommitLogLink.tsx @@ -0,0 +1,21 @@ +import { RefParams } from "@lib/params"; +import { commitLog } from "@lib/urls"; +import { ReactNode } from "react"; +import Link, { LinkProps } from "./Link"; + +type Props = LinkProps & { + children: ReactNode; + params: RefParams; + commitId?: string; +}; + +export default function CommitLogLink({ children, ...props }: Props) { + return ( + + {children} + + ); +} diff --git a/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/ForBranch.tsx b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/ForBranch.tsx new file mode 100644 index 00000000..22c92116 --- /dev/null +++ b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/ForBranch.tsx @@ -0,0 +1,24 @@ +import DiffTableNav from "@components/DiffTableNav"; +import CommitDiffBreadcrumbs from "@components/breadcrumbs/CommitDiffBreadcrumbs"; +import { RefParams } from "@lib/params"; +import { commitLog } from "@lib/urls"; +import DatabasePage from "../../component"; + +type Props = { + params: RefParams; +}; + +export default function ForBranch(props: Props) { + return ( + } + wide + routeRefChangeTo={commitLog} + smallHeaderBreadcrumbs={} + > +
    + + ); +} diff --git a/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/ForDefaultBranch.tsx b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/ForDefaultBranch.tsx new file mode 100644 index 00000000..1a5da731 --- /dev/null +++ b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/ForDefaultBranch.tsx @@ -0,0 +1,22 @@ +import Loader from "@components/Loader"; +import { useDefaultBranchPageQuery } from "@gen/graphql-types"; +import { DatabaseParams } from "@lib/params"; +import ForEmpty from "../../ForEmpty"; +import ForBranch from "./ForBranch"; + +type Props = { + params: DatabaseParams; +}; + +export default function ForDefaultBranch(props: Props) { + const { data, loading } = useDefaultBranchPageQuery({ + variables: props.params, + }); + if (loading) return ; + const refName = data?.defaultBranch?.branchName; + + if (!refName) { + return ; + } + return ; +} diff --git a/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/ForDiffRange.tsx b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/ForDiffRange.tsx new file mode 100644 index 00000000..ddebde0c --- /dev/null +++ b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/ForDiffRange.tsx @@ -0,0 +1,67 @@ +import CommitDiffBreadcrumbs from "@components/breadcrumbs/CommitDiffBreadcrumbs"; +import QueryHandler from "@components/util/QueryHandler"; +import { useHistoryForCommitQuery } from "@gen/graphql-types"; +import splitDiffRange from "@lib/diffRange"; +import { DiffRangeParams, RefParams } from "@lib/params"; +import ForError from "../../ForError"; +import DiffPage from "./component"; + +type Props = { + params: DiffRangeParams; + tableName?: string; +}; + +type InnerProps = { + params: RefParams & { fromCommitId: string }; + tableName?: string; +}; + +function Inner(props: InnerProps) { + const { databaseName } = props.params; + const res = useHistoryForCommitQuery({ + variables: { + databaseName, + afterCommitId: props.params.fromCommitId, + }, + }); + + return ( + } + render={data => ( + + } + /> + )} + /> + ); +} + +export default function ForDiffRange(props: Props) { + const { fromCommitId, toCommitId } = splitDiffRange(props.params.diffRange); + + if (toCommitId) { + return ( + } + /> + ); + } + + return ; +} diff --git a/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/ForNotDolt.tsx b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/ForNotDolt.tsx new file mode 100644 index 00000000..4b842b5b --- /dev/null +++ b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/ForNotDolt.tsx @@ -0,0 +1,28 @@ +import NotDoltWrapper from "@components/util/NotDoltWrapper"; +import { OptionalRefParams } from "@lib/params"; +import { commitLog } from "@lib/urls"; +import DatabasePage from "../../component"; + +type Props = { + params: OptionalRefParams; + compare?: boolean; +}; + +export default function ForNotDolt(props: Props) { + return ( + + +
    + + + ); +} diff --git a/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/component.tsx b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/component.tsx new file mode 100644 index 00000000..fed1f881 --- /dev/null +++ b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/component.tsx @@ -0,0 +1,61 @@ +import DiffTable from "@components/DiffTable"; +import DiffTableNav from "@components/DiffTableNav"; +import Loader from "@components/Loader"; +import { DiffProvider, useDiffContext } from "@contexts/diff"; +import { RefParams } from "@lib/params"; +import { commitLog } from "@lib/urls"; +import { ReactNode } from "react"; +import ForError from "../../ForError"; +import DatabasePage from "../../component"; +import css from "./index.module.css"; + +type InnerProps = { + params: RefParams; + smallHeaderBreadcrumbs?: ReactNode; +}; + +function Inner(props: InnerProps) { + const { loading, error } = useDiffContext(); + if (loading) return ; + if (error) return ; + return ( + } + smallHeaderBreadcrumbs={props.smallHeaderBreadcrumbs} + initialSmallHeader + wide + routeRefChangeTo={commitLog} + title="commitDiff" + > +
    + +
    +
    + ); +} + +type Props = { + initialFromCommitId: string; + initialToCommitId: string; + params: RefParams; + smallHeaderBreadcrumbs?: ReactNode; + tableName?: string; +}; + +export default function DiffPage(props: Props) { + return ( + + + + ); +} diff --git a/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/index.module.css b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/index.module.css new file mode 100644 index 00000000..7e4ac79a --- /dev/null +++ b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/index.module.css @@ -0,0 +1,3 @@ +.container { + @apply sm:mx-3; +} diff --git a/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/index.tsx b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/index.tsx new file mode 100644 index 00000000..9ca42e16 --- /dev/null +++ b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/index.tsx @@ -0,0 +1,12 @@ +import ForBranch from "./ForBranch"; +import ForDefaultBranch from "./ForDefaultBranch"; +import ForDiffRange from "./ForDiffRange"; +import ForNotDolt from "./ForNotDolt"; +import DiffPage from "./component"; + +export default Object.assign(DiffPage, { + ForBranch, + ForDefaultBranch, + ForDiffRange, + ForNotDolt, +}); diff --git a/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/queries.ts b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/queries.ts new file mode 100644 index 00000000..f7c6c63b --- /dev/null +++ b/packages/web/components/pageComponents/DatabasePage/ForCommits/DiffPage/queries.ts @@ -0,0 +1,23 @@ +import { gql } from "@apollo/client"; + +export const HISTORY_FOR_COMMIT = gql` + fragment CommitForAfterCommitHistory on Commit { + _id + commitId + parents + message + committedAt + committer { + _id + displayName + username + } + } + query HistoryForCommit($databaseName: String!, $afterCommitId: String!) { + commits(afterCommitId: $afterCommitId, databaseName: $databaseName) { + list { + ...CommitForAfterCommitHistory + } + } + } +`; diff --git a/packages/web/components/pageComponents/DatabasePage/ForCommits/index.tsx b/packages/web/components/pageComponents/DatabasePage/ForCommits/index.tsx index 76be6052..5ff87926 100644 --- a/packages/web/components/pageComponents/DatabasePage/ForCommits/index.tsx +++ b/packages/web/components/pageComponents/DatabasePage/ForCommits/index.tsx @@ -1,19 +1,20 @@ import CommitsBreadcrumbs from "@components/breadcrumbs/CommitsBreadcrumbs"; import NotDoltWrapper from "@components/util/NotDoltWrapper"; -import { RefParams } from "@lib/params"; +import useIsDolt from "@hooks/useIsDolt"; +import { OptionalRefParams } from "@lib/params"; import { commitLog } from "@lib/urls"; import DatabasePage from "../component"; import CommitLog from "./CommitLog"; +import DiffPage from "./DiffPage"; type Props = { - params: RefParams & { diffRange?: string }; - initialFromCommitId?: string; - initialToCommitId?: string; + params: OptionalRefParams & { diffRange?: string }; compare?: boolean; tableName?: string; }; export default function ForCommits(props: Props) { + const { isDolt } = useIsDolt(); const notCommitLogPage = !props.params.refName || props.compare || !!props.params.diffRange; const commonProps = { @@ -22,28 +23,37 @@ export default function ForCommits(props: Props) { wide: notCommitLogPage, }; - // if (props.compare) { - // return ; - // } + if (!isDolt) { + return ; + } - // if (props.params.diffRange) { - // return ( - // - // ); - // } + if (!props.params.refName) { + return ; + } + + const refParams = { ...props.params, refName: props.params.refName }; + if (props.compare) { + return ; + } + + if (props.params.diffRange) { + return ( + + ); + } return ( } + smallHeaderBreadcrumbs={} title="commitLog" routeRefChangeTo={commitLog} > - + ); diff --git a/packages/web/components/pageComponents/DatabasePage/ForQuery.tsx b/packages/web/components/pageComponents/DatabasePage/ForQuery.tsx index 55e2988b..8941d907 100644 --- a/packages/web/components/pageComponents/DatabasePage/ForQuery.tsx +++ b/packages/web/components/pageComponents/DatabasePage/ForQuery.tsx @@ -56,7 +56,10 @@ function Inner({ params }: Props) { export default function ForQuery(props: Props) { return ( - + ); diff --git a/packages/web/components/pageComponents/DatabasePage/ForReleases/ReleaseList/mocks.ts b/packages/web/components/pageComponents/DatabasePage/ForReleases/ReleaseList/mocks.ts index 8b291c29..8d1e27bb 100644 --- a/packages/web/components/pageComponents/DatabasePage/ForReleases/ReleaseList/mocks.ts +++ b/packages/web/components/pageComponents/DatabasePage/ForReleases/ReleaseList/mocks.ts @@ -45,8 +45,6 @@ export function buildFakeData(list: TagForListFragment[]): TagListQuery { // QUERY MOCKS export const databaseParams = { - deploymentName: "test", - ownerName: "dolthub", databaseName: "dbname", }; diff --git a/packages/web/components/util/NotDoltWrapper/index.tsx b/packages/web/components/util/NotDoltWrapper/index.tsx index 8f335c9c..a5ed3171 100644 --- a/packages/web/components/util/NotDoltWrapper/index.tsx +++ b/packages/web/components/util/NotDoltWrapper/index.tsx @@ -21,7 +21,7 @@ export default function NotDoltWrapper(props: Props) { return props.children; } if (props.hideNotDolt) { - return null; + return false; } if (props.showNotDoltMsg) { return ( diff --git a/packages/web/contexts/dataTable/index.tsx b/packages/web/contexts/dataTable/index.tsx index f2084723..f1d68418 100644 --- a/packages/web/contexts/dataTable/index.tsx +++ b/packages/web/contexts/dataTable/index.tsx @@ -28,6 +28,7 @@ type DataTableContextType = { columns?: ColumnForDataTableFragment[]; foreignKeys?: ForeignKeysForDataTableFragment[]; error?: ApolloError; + showingWorkingDiff: boolean; }; export const DataTableContext = @@ -36,6 +37,7 @@ export const DataTableContext = type Props = { params: DataTableParams | SqlQueryParams; children: ReactNode; + showingWorkingDiff?: boolean; }; type TableProps = Props & { @@ -91,6 +93,7 @@ function ProviderForTableName(props: TableProps) { columns: tableRes.data?.table.columns, foreignKeys: tableRes.data?.table.foreignKeys, error: tableRes.error ?? rowRes.error, + showingWorkingDiff: !!props.showingWorkingDiff, }; }, [ loadMore, @@ -104,6 +107,7 @@ function ProviderForTableName(props: TableProps) { tableRes.data?.table.foreignKeys, tableRes.error, tableRes.loading, + props.showingWorkingDiff, ]); return ( @@ -114,7 +118,11 @@ function ProviderForTableName(props: TableProps) { } // DataTableProvider should only wrap DatabasePage.ForTable and DatabasePage.ForQueries -export function DataTableProvider({ params, children }: Props) { +export function DataTableProvider({ + params, + children, + showingWorkingDiff, +}: Props) { const tableName = "tableName" in params ? params.tableName : tryTableNameForSelect(params.q); @@ -124,8 +132,9 @@ export function DataTableProvider({ params, children }: Props) { loading: false, loadMore: async () => {}, hasMore: false, + showingWorkingDiff: !!showingWorkingDiff, }; - }, [params]); + }, [params, showingWorkingDiff]); const isMut = "q" in params && isMutation(params.q); if (isMut || !tableName) { diff --git a/packages/web/contexts/diff/index.tsx b/packages/web/contexts/diff/index.tsx new file mode 100644 index 00000000..590609e6 --- /dev/null +++ b/packages/web/contexts/diff/index.tsx @@ -0,0 +1,80 @@ +import { ApolloError } from "@apollo/client"; +import { DiffSummaryFragment, useDiffSummariesQuery } from "@gen/graphql-types"; +import useContextWithError from "@hooks/useContextWithError"; +import { createCustomContext } from "@lib/createCustomContext"; +import { RequiredCommitsParams } from "@lib/params"; +import { ReactNode, useEffect, useMemo, useState } from "react"; + +type Props = { + children: ReactNode; + params: RequiredCommitsParams & { refName?: string }; + initialTableName?: string; + stayWithinPage?: boolean; +}; + +// This contexts handles the diff summaries for the diff page +type DiffContextType = { + diffSummaries: DiffSummaryFragment[]; + loading: boolean; + error?: ApolloError; + params: RequiredCommitsParams; + activeTableName: string; + setActiveTableName: (a: string) => void; + refName: string; + setRefName: (r: string) => void; + stayWithinPage: boolean; // Changing tables within diff doesn't change URL +}; + +export const DiffContext = createCustomContext("DiffContext"); + +// DiffProvider should only be used in all pages with diffs +export function DiffProvider({ + params, + children, + initialTableName, + stayWithinPage = false, +}: Props): JSX.Element { + const [activeTableName, setActiveTableName] = useState( + initialTableName ?? "", + ); + const [refName, setRefName] = useState(params.refName ?? ""); + const { data, error, loading } = useDiffSummariesQuery({ + variables: params, + }); + + useEffect(() => { + if (initialTableName || !data) return; + if (data.diffSummaries.length === 0) return; + setActiveTableName(data.diffSummaries[0].tableName); + }, [data, initialTableName]); + + const value = useMemo(() => { + return { + loading, + error, + diffSummaries: data?.diffSummaries || [], + params, + activeTableName, + setActiveTableName, + refName, + setRefName, + stayWithinPage, + }; + }, [ + data, + error, + loading, + params, + activeTableName, + setActiveTableName, + refName, + setRefName, + stayWithinPage, + ]); + + return {children}; +} + +export function useDiffContext(): DiffContextType { + return useContextWithError(DiffContext); +} diff --git a/packages/web/contexts/diff/queries.ts b/packages/web/contexts/diff/queries.ts new file mode 100644 index 00000000..4507e417 --- /dev/null +++ b/packages/web/contexts/diff/queries.ts @@ -0,0 +1,28 @@ +import { gql } from "@apollo/client"; + +export const DIFF_SUMMARIES = gql` + fragment DiffSummary on DiffSummary { + _id + fromTableName + toTableName + tableName + tableType + hasDataChanges + hasSchemaChanges + } + query DiffSummaries( + $databaseName: String! + $fromCommitId: String! + $toCommitId: String! + $refName: String + ) { + diffSummaries( + databaseName: $databaseName + fromRefName: $fromCommitId + toRefName: $toCommitId + refName: $refName + ) { + ...DiffSummary + } + } +`; diff --git a/packages/web/gen/graphql-types.tsx b/packages/web/gen/graphql-types.tsx index 20783a33..d917fc4f 100644 --- a/packages/web/gen/graphql-types.tsx +++ b/packages/web/gen/graphql-types.tsx @@ -75,12 +75,47 @@ export type Commit = { parents: Array; }; +export enum CommitDiffType { + ThreeDot = 'ThreeDot', + TwoDot = 'TwoDot', + Unspecified = 'Unspecified' +} + export type CommitList = { __typename?: 'CommitList'; list: Array; nextOffset?: Maybe; }; +export enum DiffRowType { + Added = 'Added', + All = 'All', + Modified = 'Modified', + Removed = 'Removed' +} + +export type DiffStat = { + __typename?: 'DiffStat'; + cellCount: Scalars['Float']['output']; + cellsModified: Scalars['Float']['output']; + rowCount: Scalars['Float']['output']; + rowsAdded: Scalars['Float']['output']; + rowsDeleted: Scalars['Float']['output']; + rowsModified: Scalars['Float']['output']; + rowsUnmodified: Scalars['Float']['output']; +}; + +export type DiffSummary = { + __typename?: 'DiffSummary'; + _id: Scalars['ID']['output']; + fromTableName: Scalars['String']['output']; + hasDataChanges: Scalars['Boolean']['output']; + hasSchemaChanges: Scalars['Boolean']['output']; + tableName: Scalars['String']['output']; + tableType: TableDiffType; + toTableName: Scalars['String']['output']; +}; + export type Doc = { __typename?: 'Doc'; branchName: Scalars['String']['output']; @@ -224,11 +259,15 @@ export type Query = { currentDatabase?: Maybe; databases: Array; defaultBranch?: Maybe; + diffStat: DiffStat; + diffSummaries: Array; docOrDefaultDoc?: Maybe; docs: DocList; doltDatabaseDetails: DoltDatabaseDetails; hasDatabaseEnv: Scalars['Boolean']['output']; + rowDiffs: RowDiffList; rows: RowList; + schemaDiff?: Maybe; sqlSelect: SqlSelect; sqlSelectForCsvDownload: Scalars['String']['output']; status: Array; @@ -260,9 +299,10 @@ export type QueryBranchesArgs = { export type QueryCommitsArgs = { + afterCommitId?: InputMaybe; databaseName: Scalars['String']['input']; offset?: InputMaybe; - refName: Scalars['String']['input']; + refName?: InputMaybe; }; @@ -271,6 +311,26 @@ export type QueryDefaultBranchArgs = { }; +export type QueryDiffStatArgs = { + databaseName: Scalars['String']['input']; + fromRefName: Scalars['String']['input']; + refName?: InputMaybe; + tableName?: InputMaybe; + toRefName: Scalars['String']['input']; + type?: InputMaybe; +}; + + +export type QueryDiffSummariesArgs = { + databaseName: Scalars['String']['input']; + fromRefName: Scalars['String']['input']; + refName?: InputMaybe; + tableName?: InputMaybe; + toRefName: Scalars['String']['input']; + type?: InputMaybe; +}; + + export type QueryDocOrDefaultDocArgs = { databaseName: Scalars['String']['input']; docType?: InputMaybe; @@ -284,6 +344,17 @@ export type QueryDocsArgs = { }; +export type QueryRowDiffsArgs = { + databaseName: Scalars['String']['input']; + filterByRowType?: InputMaybe; + fromCommitId: Scalars['String']['input']; + offset?: InputMaybe; + refName?: InputMaybe; + tableName: Scalars['String']['input']; + toCommitId: Scalars['String']['input']; +}; + + export type QueryRowsArgs = { databaseName: Scalars['String']['input']; offset?: InputMaybe; @@ -292,6 +363,15 @@ export type QueryRowsArgs = { }; +export type QuerySchemaDiffArgs = { + databaseName: Scalars['String']['input']; + fromCommitId: Scalars['String']['input']; + refName?: InputMaybe; + tableName: Scalars['String']['input']; + toCommitId: Scalars['String']['input']; +}; + + export type QuerySqlSelectArgs = { databaseName: Scalars['String']['input']; queryString: Scalars['String']['input']; @@ -361,12 +441,32 @@ export type Row = { columnValues: Array; }; +export type RowDiff = { + __typename?: 'RowDiff'; + added?: Maybe; + deleted?: Maybe; +}; + +export type RowDiffList = { + __typename?: 'RowDiffList'; + columns: Array; + list: Array; + nextOffset?: Maybe; +}; + export type RowList = { __typename?: 'RowList'; list: Array; nextOffset?: Maybe; }; +export type SchemaDiff = { + __typename?: 'SchemaDiff'; + numChangedSchemas?: Maybe; + schemaDiff?: Maybe; + schemaPatch?: Maybe>; +}; + export enum SortBranchesBy { LastUpdated = 'LastUpdated', Unspecified = 'Unspecified' @@ -404,6 +504,13 @@ export type Table = { tableName: Scalars['String']['output']; }; +export enum TableDiffType { + Added = 'Added', + Dropped = 'Dropped', + Modified = 'Modified', + Renamed = 'Renamed' +} + export type TableNames = { __typename?: 'TableNames'; list: Array; @@ -425,6 +532,12 @@ export type TagList = { list: Array; }; +export type TextDiff = { + __typename?: 'TextDiff'; + leftLines: Scalars['String']['output']; + rightLines: Scalars['String']['output']; +}; + export type CreateDatabaseMutationVariables = Exact<{ databaseName: Scalars['String']['input']; }>; @@ -487,6 +600,70 @@ export type DatabasesQueryVariables = Exact<{ [key: string]: never; }>; export type DatabasesQuery = { __typename?: 'Query', databases: Array }; +export type CommitForDiffSelectorFragment = { __typename?: 'Commit', _id: string, commitId: string, message: string, committedAt: any, parents: Array, committer: { __typename?: 'DoltWriter', _id: string, displayName: string, username?: string | null } }; + +export type CommitListForDiffSelectorFragment = { __typename?: 'CommitList', list: Array<{ __typename?: 'Commit', _id: string, commitId: string, message: string, committedAt: any, parents: Array, committer: { __typename?: 'DoltWriter', _id: string, displayName: string, username?: string | null } }> }; + +export type CommitsForDiffSelectorQueryVariables = Exact<{ + refName: Scalars['String']['input']; + databaseName: Scalars['String']['input']; +}>; + + +export type CommitsForDiffSelectorQuery = { __typename?: 'Query', commits: { __typename?: 'CommitList', list: Array<{ __typename?: 'Commit', _id: string, commitId: string, message: string, committedAt: any, parents: Array, committer: { __typename?: 'DoltWriter', _id: string, displayName: string, username?: string | null } }> } }; + +export type DiffStatForDiffsFragment = { __typename?: 'DiffStat', rowsUnmodified: number, rowsAdded: number, rowsDeleted: number, rowsModified: number, cellsModified: number, rowCount: number, cellCount: number }; + +export type DiffStatQueryVariables = Exact<{ + databaseName: Scalars['String']['input']; + fromRefName: Scalars['String']['input']; + toRefName: Scalars['String']['input']; + refName?: InputMaybe; + type?: InputMaybe; + tableName?: InputMaybe; +}>; + + +export type DiffStatQuery = { __typename?: 'Query', diffStat: { __typename?: 'DiffStat', rowsUnmodified: number, rowsAdded: number, rowsDeleted: number, rowsModified: number, cellsModified: number, rowCount: number, cellCount: number } }; + +export type ColumnForDiffTableListFragment = { __typename?: 'Column', name: string, isPrimaryKey: boolean, type: string, constraints?: Array<{ __typename?: 'ColConstraint', notNull: boolean }> | null }; + +export type ColumnValueForTableListFragment = { __typename?: 'ColumnValue', displayValue: string }; + +export type RowForTableListFragment = { __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> }; + +export type RowDiffForTableListFragment = { __typename?: 'RowDiff', added?: { __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> } | null, deleted?: { __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> } | null }; + +export type RowDiffListWithColsFragment = { __typename?: 'RowDiffList', nextOffset?: number | null, list: Array<{ __typename?: 'RowDiff', added?: { __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> } | null, deleted?: { __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> } | null }>, columns: Array<{ __typename?: 'Column', name: string, isPrimaryKey: boolean, type: string, constraints?: Array<{ __typename?: 'ColConstraint', notNull: boolean }> | null }> }; + +export type RowDiffsQueryVariables = Exact<{ + databaseName: Scalars['String']['input']; + tableName: Scalars['String']['input']; + fromCommitId: Scalars['String']['input']; + toCommitId: Scalars['String']['input']; + refName?: InputMaybe; + offset?: InputMaybe; + filterByRowType?: InputMaybe; +}>; + + +export type RowDiffsQuery = { __typename?: 'Query', rowDiffs: { __typename?: 'RowDiffList', nextOffset?: number | null, list: Array<{ __typename?: 'RowDiff', added?: { __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> } | null, deleted?: { __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> } | null }>, columns: Array<{ __typename?: 'Column', name: string, isPrimaryKey: boolean, type: string, constraints?: Array<{ __typename?: 'ColConstraint', notNull: boolean }> | null }> } }; + +export type SchemaDiffForTableListFragment = { __typename?: 'TextDiff', leftLines: string, rightLines: string }; + +export type SchemaDiffFragment = { __typename?: 'SchemaDiff', schemaPatch?: Array | null, schemaDiff?: { __typename?: 'TextDiff', leftLines: string, rightLines: string } | null }; + +export type SchemaDiffQueryVariables = Exact<{ + databaseName: Scalars['String']['input']; + tableName: Scalars['String']['input']; + fromCommitId: Scalars['String']['input']; + toCommitId: Scalars['String']['input']; + refName?: InputMaybe; +}>; + + +export type SchemaDiffQuery = { __typename?: 'Query', schemaDiff?: { __typename?: 'SchemaDiff', schemaPatch?: Array | null, schemaDiff?: { __typename?: 'TextDiff', leftLines: string, rightLines: string } | null } | null }; + export type ColumnsListForTableListFragment = { __typename?: 'IndexColumn', name: string, sqlType?: string | null }; export type IndexForTableListFragment = { __typename?: 'Index', name: string, type: string, comment: string, columns: Array<{ __typename?: 'IndexColumn', name: string, sqlType?: string | null }> }; @@ -576,6 +753,16 @@ export type CreateBranchMutationVariables = Exact<{ export type CreateBranchMutation = { __typename?: 'Mutation', createBranch: { __typename?: 'Branch', databaseName: string, branchName: string } }; +export type CommitForAfterCommitHistoryFragment = { __typename?: 'Commit', _id: string, commitId: string, parents: Array, message: string, committedAt: any, committer: { __typename?: 'DoltWriter', _id: string, displayName: string, username?: string | null } }; + +export type HistoryForCommitQueryVariables = Exact<{ + databaseName: Scalars['String']['input']; + afterCommitId: Scalars['String']['input']; +}>; + + +export type HistoryForCommitQuery = { __typename?: 'Query', commits: { __typename?: 'CommitList', list: Array<{ __typename?: 'Commit', _id: string, commitId: string, parents: Array, message: string, committedAt: any, committer: { __typename?: 'DoltWriter', _id: string, displayName: string, username?: string | null } }> } }; + export type DefaultBranchPageQueryVariables = Exact<{ databaseName: Scalars['String']['input']; filterSystemTables?: InputMaybe; @@ -703,6 +890,18 @@ export type RowsForDataTableQueryVariables = Exact<{ export type RowsForDataTableQuery = { __typename?: 'Query', rows: { __typename?: 'RowList', nextOffset?: number | null, list: Array<{ __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> }> } }; +export type DiffSummaryFragment = { __typename?: 'DiffSummary', _id: string, fromTableName: string, toTableName: string, tableName: string, tableType: TableDiffType, hasDataChanges: boolean, hasSchemaChanges: boolean }; + +export type DiffSummariesQueryVariables = Exact<{ + databaseName: Scalars['String']['input']; + fromCommitId: Scalars['String']['input']; + toCommitId: Scalars['String']['input']; + refName?: InputMaybe; +}>; + + +export type DiffSummariesQuery = { __typename?: 'Query', diffSummaries: Array<{ __typename?: 'DiffSummary', _id: string, fromTableName: string, toTableName: string, tableName: string, tableType: TableDiffType, hasDataChanges: boolean, hasSchemaChanges: boolean }> }; + export type DoltWriterForHistoryFragment = { __typename?: 'DoltWriter', _id: string, username?: string | null, displayName: string, emailAddress: string }; export type CommitForHistoryFragment = { __typename?: 'Commit', _id: string, message: string, commitId: string, committedAt: any, parents: Array, committer: { __typename?: 'DoltWriter', _id: string, username?: string | null, displayName: string, emailAddress: string } }; @@ -769,6 +968,96 @@ export const TagListForTagListFragmentDoc = gql` } } ${TagForListFragmentDoc}`; +export const CommitForDiffSelectorFragmentDoc = gql` + fragment CommitForDiffSelector on Commit { + _id + commitId + message + committedAt + parents + committer { + _id + displayName + username + } +} + `; +export const CommitListForDiffSelectorFragmentDoc = gql` + fragment CommitListForDiffSelector on CommitList { + list { + ...CommitForDiffSelector + } +} + ${CommitForDiffSelectorFragmentDoc}`; +export const DiffStatForDiffsFragmentDoc = gql` + fragment DiffStatForDiffs on DiffStat { + rowsUnmodified + rowsAdded + rowsDeleted + rowsModified + cellsModified + rowCount + cellCount +} + `; +export const ColumnValueForTableListFragmentDoc = gql` + fragment ColumnValueForTableList on ColumnValue { + displayValue +} + `; +export const RowForTableListFragmentDoc = gql` + fragment RowForTableList on Row { + columnValues { + ...ColumnValueForTableList + } +} + ${ColumnValueForTableListFragmentDoc}`; +export const RowDiffForTableListFragmentDoc = gql` + fragment RowDiffForTableList on RowDiff { + added { + ...RowForTableList + } + deleted { + ...RowForTableList + } +} + ${RowForTableListFragmentDoc}`; +export const ColumnForDiffTableListFragmentDoc = gql` + fragment ColumnForDiffTableList on Column { + name + isPrimaryKey + type + constraints { + notNull + } +} + `; +export const RowDiffListWithColsFragmentDoc = gql` + fragment RowDiffListWithCols on RowDiffList { + list { + ...RowDiffForTableList + } + columns { + ...ColumnForDiffTableList + } + nextOffset +} + ${RowDiffForTableListFragmentDoc} +${ColumnForDiffTableListFragmentDoc}`; +export const SchemaDiffForTableListFragmentDoc = gql` + fragment SchemaDiffForTableList on TextDiff { + leftLines + rightLines +} + `; +export const SchemaDiffFragmentDoc = gql` + fragment SchemaDiff on SchemaDiff { + schemaDiff { + ...SchemaDiffForTableList + } + schemaPatch +} + ${SchemaDiffForTableListFragmentDoc}`; export const ForeignKeyColumnForDataTableFragmentDoc = gql` fragment ForeignKeyColumnForDataTable on ForeignKeyColumn { referencedColumnName @@ -882,6 +1171,20 @@ export const BranchForCreateBranchFragmentDoc = gql` branchName } `; +export const CommitForAfterCommitHistoryFragmentDoc = gql` + fragment CommitForAfterCommitHistory on Commit { + _id + commitId + parents + message + committedAt + committer { + _id + displayName + username + } +} + `; export const DocRowForDocPageFragmentDoc = gql` fragment DocRowForDocPage on Row { columnValues { @@ -935,6 +1238,17 @@ export const RowListRowsFragmentDoc = gql` } } ${RowForDataTableFragmentDoc}`; +export const DiffSummaryFragmentDoc = gql` + fragment DiffSummary on DiffSummary { + _id + fromTableName + toTableName + tableName + tableType + hasDataChanges + hasSchemaChanges +} + `; export const CommitForHistoryFragmentDoc = gql` fragment CommitForHistory on Commit { _id @@ -1239,6 +1553,183 @@ export function useDatabasesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions< export type DatabasesQueryHookResult = ReturnType; export type DatabasesLazyQueryHookResult = ReturnType; export type DatabasesQueryResult = Apollo.QueryResult; +export const CommitsForDiffSelectorDocument = gql` + query CommitsForDiffSelector($refName: String!, $databaseName: String!) { + commits(refName: $refName, databaseName: $databaseName) { + ...CommitListForDiffSelector + } +} + ${CommitListForDiffSelectorFragmentDoc}`; + +/** + * __useCommitsForDiffSelectorQuery__ + * + * To run a query within a React component, call `useCommitsForDiffSelectorQuery` and pass it any options that fit your needs. + * When your component renders, `useCommitsForDiffSelectorQuery` 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 } = useCommitsForDiffSelectorQuery({ + * variables: { + * refName: // value for 'refName' + * databaseName: // value for 'databaseName' + * }, + * }); + */ +export function useCommitsForDiffSelectorQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(CommitsForDiffSelectorDocument, options); + } +export function useCommitsForDiffSelectorLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(CommitsForDiffSelectorDocument, options); + } +export type CommitsForDiffSelectorQueryHookResult = ReturnType; +export type CommitsForDiffSelectorLazyQueryHookResult = ReturnType; +export type CommitsForDiffSelectorQueryResult = Apollo.QueryResult; +export const DiffStatDocument = gql` + query DiffStat($databaseName: String!, $fromRefName: String!, $toRefName: String!, $refName: String, $type: CommitDiffType, $tableName: String) { + diffStat( + databaseName: $databaseName + fromRefName: $fromRefName + toRefName: $toRefName + refName: $refName + type: $type + tableName: $tableName + ) { + ...DiffStatForDiffs + } +} + ${DiffStatForDiffsFragmentDoc}`; + +/** + * __useDiffStatQuery__ + * + * To run a query within a React component, call `useDiffStatQuery` and pass it any options that fit your needs. + * When your component renders, `useDiffStatQuery` 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 } = useDiffStatQuery({ + * variables: { + * databaseName: // value for 'databaseName' + * fromRefName: // value for 'fromRefName' + * toRefName: // value for 'toRefName' + * refName: // value for 'refName' + * type: // value for 'type' + * tableName: // value for 'tableName' + * }, + * }); + */ +export function useDiffStatQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(DiffStatDocument, options); + } +export function useDiffStatLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(DiffStatDocument, options); + } +export type DiffStatQueryHookResult = ReturnType; +export type DiffStatLazyQueryHookResult = ReturnType; +export type DiffStatQueryResult = Apollo.QueryResult; +export const RowDiffsDocument = gql` + query RowDiffs($databaseName: String!, $tableName: String!, $fromCommitId: String!, $toCommitId: String!, $refName: String, $offset: Int, $filterByRowType: DiffRowType) { + rowDiffs( + databaseName: $databaseName + tableName: $tableName + fromCommitId: $fromCommitId + toCommitId: $toCommitId + refName: $refName + offset: $offset + filterByRowType: $filterByRowType + ) { + ...RowDiffListWithCols + } +} + ${RowDiffListWithColsFragmentDoc}`; + +/** + * __useRowDiffsQuery__ + * + * To run a query within a React component, call `useRowDiffsQuery` and pass it any options that fit your needs. + * When your component renders, `useRowDiffsQuery` 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 } = useRowDiffsQuery({ + * variables: { + * databaseName: // value for 'databaseName' + * tableName: // value for 'tableName' + * fromCommitId: // value for 'fromCommitId' + * toCommitId: // value for 'toCommitId' + * refName: // value for 'refName' + * offset: // value for 'offset' + * filterByRowType: // value for 'filterByRowType' + * }, + * }); + */ +export function useRowDiffsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(RowDiffsDocument, options); + } +export function useRowDiffsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(RowDiffsDocument, options); + } +export type RowDiffsQueryHookResult = ReturnType; +export type RowDiffsLazyQueryHookResult = ReturnType; +export type RowDiffsQueryResult = Apollo.QueryResult; +export const SchemaDiffDocument = gql` + query SchemaDiff($databaseName: String!, $tableName: String!, $fromCommitId: String!, $toCommitId: String!, $refName: String) { + schemaDiff( + databaseName: $databaseName + tableName: $tableName + fromCommitId: $fromCommitId + toCommitId: $toCommitId + refName: $refName + ) { + ...SchemaDiff + } +} + ${SchemaDiffFragmentDoc}`; + +/** + * __useSchemaDiffQuery__ + * + * To run a query within a React component, call `useSchemaDiffQuery` and pass it any options that fit your needs. + * When your component renders, `useSchemaDiffQuery` 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 } = useSchemaDiffQuery({ + * variables: { + * databaseName: // value for 'databaseName' + * tableName: // value for 'tableName' + * fromCommitId: // value for 'fromCommitId' + * toCommitId: // value for 'toCommitId' + * refName: // value for 'refName' + * }, + * }); + */ +export function useSchemaDiffQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(SchemaDiffDocument, options); + } +export function useSchemaDiffLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(SchemaDiffDocument, options); + } +export type SchemaDiffQueryHookResult = ReturnType; +export type SchemaDiffLazyQueryHookResult = ReturnType; +export type SchemaDiffQueryResult = Apollo.QueryResult; export const TableListForSchemasDocument = gql` query TableListForSchemas($databaseName: String!, $refName: String!) { tables(databaseName: $databaseName, refName: $refName) { @@ -1545,6 +2036,44 @@ export function useCreateBranchMutation(baseOptions?: Apollo.MutationHookOptions export type CreateBranchMutationHookResult = ReturnType; export type CreateBranchMutationResult = Apollo.MutationResult; export type CreateBranchMutationOptions = Apollo.BaseMutationOptions; +export const HistoryForCommitDocument = gql` + query HistoryForCommit($databaseName: String!, $afterCommitId: String!) { + commits(afterCommitId: $afterCommitId, databaseName: $databaseName) { + list { + ...CommitForAfterCommitHistory + } + } +} + ${CommitForAfterCommitHistoryFragmentDoc}`; + +/** + * __useHistoryForCommitQuery__ + * + * To run a query within a React component, call `useHistoryForCommitQuery` and pass it any options that fit your needs. + * When your component renders, `useHistoryForCommitQuery` 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 } = useHistoryForCommitQuery({ + * variables: { + * databaseName: // value for 'databaseName' + * afterCommitId: // value for 'afterCommitId' + * }, + * }); + */ +export function useHistoryForCommitQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(HistoryForCommitDocument, options); + } +export function useHistoryForCommitLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(HistoryForCommitDocument, options); + } +export type HistoryForCommitQueryHookResult = ReturnType; +export type HistoryForCommitLazyQueryHookResult = ReturnType; +export type HistoryForCommitQueryResult = Apollo.QueryResult; export const DefaultBranchPageQueryDocument = gql` query DefaultBranchPageQuery($databaseName: String!, $filterSystemTables: Boolean) { defaultBranch(databaseName: $databaseName) { @@ -2046,6 +2575,49 @@ export function useRowsForDataTableQueryLazyQuery(baseOptions?: Apollo.LazyQuery export type RowsForDataTableQueryHookResult = ReturnType; export type RowsForDataTableQueryLazyQueryHookResult = ReturnType; export type RowsForDataTableQueryQueryResult = Apollo.QueryResult; +export const DiffSummariesDocument = gql` + query DiffSummaries($databaseName: String!, $fromCommitId: String!, $toCommitId: String!, $refName: String) { + diffSummaries( + databaseName: $databaseName + fromRefName: $fromCommitId + toRefName: $toCommitId + refName: $refName + ) { + ...DiffSummary + } +} + ${DiffSummaryFragmentDoc}`; + +/** + * __useDiffSummariesQuery__ + * + * To run a query within a React component, call `useDiffSummariesQuery` and pass it any options that fit your needs. + * When your component renders, `useDiffSummariesQuery` 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 } = useDiffSummariesQuery({ + * variables: { + * databaseName: // value for 'databaseName' + * fromCommitId: // value for 'fromCommitId' + * toCommitId: // value for 'toCommitId' + * refName: // value for 'refName' + * }, + * }); + */ +export function useDiffSummariesQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(DiffSummariesDocument, options); + } +export function useDiffSummariesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(DiffSummariesDocument, options); + } +export type DiffSummariesQueryHookResult = ReturnType; +export type DiffSummariesLazyQueryHookResult = ReturnType; +export type DiffSummariesQueryResult = Apollo.QueryResult; export const HistoryForBranchDocument = gql` query HistoryForBranch($databaseName: String!, $refName: String!, $offset: Int) { commits(databaseName: $databaseName, refName: $refName, offset: $offset) { diff --git a/packages/web/lib/diffRange.test.ts b/packages/web/lib/diffRange.test.ts new file mode 100644 index 00000000..6c924d0d --- /dev/null +++ b/packages/web/lib/diffRange.test.ts @@ -0,0 +1,19 @@ +import splitDiffRange from "./diffRange"; + +describe("test diffRange", () => { + it("gets split diff range", () => { + const commit1 = "ewihrjlewjerwwe"; + const commit2 = "lkmopaiwjekjwnn"; + const split = splitDiffRange(`${commit1}...${commit2}`); + expect(split.fromCommitId).toEqual(commit1); + expect(split.toCommitId).toEqual(commit2); + + const twoDot = splitDiffRange(`${commit1}..${commit2}`); + expect(twoDot.fromCommitId).toEqual(commit1); + expect(twoDot.toCommitId).toEqual(commit2); + + const oneSideSplit = splitDiffRange(`${commit1}...`); + expect(oneSideSplit.fromCommitId).toEqual(commit1); + expect(oneSideSplit.toCommitId).toEqual(""); + }); +}); diff --git a/packages/web/lib/diffRange.tsx b/packages/web/lib/diffRange.tsx new file mode 100644 index 00000000..7f4e5086 --- /dev/null +++ b/packages/web/lib/diffRange.tsx @@ -0,0 +1,9 @@ +type Commits = { + fromCommitId: string; + toCommitId: string; +}; + +export default function splitDiffRange(diffRange: string): Commits { + const [fromCommitId, toCommitId] = diffRange.split(/\.+/); + return { fromCommitId, toCommitId }; +} diff --git a/packages/web/lib/numToStringConversions.test.ts b/packages/web/lib/numToStringConversions.test.ts new file mode 100644 index 00000000..433393b4 --- /dev/null +++ b/packages/web/lib/numToStringConversions.test.ts @@ -0,0 +1,50 @@ +import { + numToRoundedUsdWithCommas, + numToStringWithCommas, + numToUsdWithCommas, +} from "./numToStringConversions"; + +describe("test numToStringWithCommas util function", () => { + it("formats whole numbers correctly", () => { + expect(numToStringWithCommas(0)).toEqual("0"); + expect(numToStringWithCommas(5)).toEqual("5"); + expect(numToStringWithCommas(600)).toEqual("600"); + expect(numToStringWithCommas(140045)).toEqual("140,045"); + }); + it("formats numbers with decimals", () => { + expect(numToStringWithCommas(0.0)).toEqual("0"); + expect(numToStringWithCommas(5.1)).toEqual("5.1"); + expect(numToStringWithCommas(600.009)).toEqual("600.009"); + expect(numToStringWithCommas(140045.994)).toEqual("140,045.994"); + }); +}); + +describe("test numToUsdWithCommas util function", () => { + it("formats whole numbers correctly", () => { + expect(numToUsdWithCommas(0)).toEqual("$0.00"); + expect(numToUsdWithCommas(5)).toEqual("$5.00"); + expect(numToUsdWithCommas(600)).toEqual("$600.00"); + expect(numToUsdWithCommas(140045)).toEqual("$140,045.00"); + }); + it("formats numbers with decimals correctly", () => { + expect(numToUsdWithCommas(0.0)).toEqual("$0.00"); + expect(numToUsdWithCommas(5.1)).toEqual("$5.10"); + expect(numToUsdWithCommas(600.009)).toEqual("$600.01"); + expect(numToUsdWithCommas(140045.994)).toEqual("$140,045.99"); + }); + + describe("test numToRoundedUsdWithCommas util function", () => { + it("formats whole numbers correctly", () => { + expect(numToRoundedUsdWithCommas(0)).toEqual("$0"); + expect(numToRoundedUsdWithCommas(5)).toEqual("$5"); + expect(numToRoundedUsdWithCommas(600)).toEqual("$600"); + expect(numToRoundedUsdWithCommas(140045)).toEqual("$140,045"); + }); + it("formats numbers with decimals correctly", () => { + expect(numToRoundedUsdWithCommas(0.0)).toEqual("$0"); + expect(numToRoundedUsdWithCommas(5.1)).toEqual("$5"); + expect(numToRoundedUsdWithCommas(600.009)).toEqual("$600"); + expect(numToRoundedUsdWithCommas(140045.994)).toEqual("$140,046"); + }); + }); +}); diff --git a/packages/web/lib/numToStringConversions.ts b/packages/web/lib/numToStringConversions.ts new file mode 100644 index 00000000..2110cb78 --- /dev/null +++ b/packages/web/lib/numToStringConversions.ts @@ -0,0 +1,19 @@ +export function numToStringWithCommas(num: number): string { + return new Intl.NumberFormat("en-US").format(num); +} + +export function numToUsdWithCommas(num: number): string { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(num); +} + +export function numToRoundedUsdWithCommas(num: number): string { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(num); +} diff --git a/packages/web/lib/params.ts b/packages/web/lib/params.ts index 5449a88a..ce274079 100644 --- a/packages/web/lib/params.ts +++ b/packages/web/lib/params.ts @@ -54,3 +54,12 @@ export type DiffParams = RefParams & CommitsParams; export type UploadParams = DatabaseParams & { uploadId: string; }; + +export type DiffRangeParams = RefParams & { + diffRange: string; +}; + +export type DiffParamsWithRefs = DatabaseParams & { + fromRefName: string; + toRefName: string; +}; diff --git a/packages/web/lib/refetchQueries.ts b/packages/web/lib/refetchQueries.ts index 229e6516..364e079d 100644 --- a/packages/web/lib/refetchQueries.ts +++ b/packages/web/lib/refetchQueries.ts @@ -6,7 +6,12 @@ import { RefetchQueriesOptions, } from "@apollo/client"; import * as gen from "@gen/graphql-types"; -import { DatabaseParams, RefParams, TableParams } from "./params"; +import { + DatabaseParams, + RefParams, + RequiredCommitsParams, + TableParams, +} from "./params"; export type RefetchQueries = Array; @@ -41,31 +46,32 @@ export const refetchBranchQueries = ( export const refetchResetChangesQueries = ( variables: RefParams, isDolt = false, -): RefetchQueries => - // const diffVariables: RequiredCommitsParams = { - // ...variables, - // fromCommitId: "HEAD", - // toCommitId: "WORKING", - // }; - [ +): RefetchQueries => { + const diffVariables: RequiredCommitsParams = { + ...variables, + fromCommitId: "HEAD", + toCommitId: "WORKING", + }; + return [ ...(isDolt ? [{ query: gen.GetStatusDocument, variables }] : []), - // { - // query: gen.DiffStatDocument, - // variables: { - // ...variables, - // fromRefName: diffVariables.fromCommitId, - // toRefName: diffVariables.toCommitId, - // }, - // }, - // { - // query: gen.DiffSummariesDocument, - // variables: diffVariables, - // }, + { + query: gen.DiffStatDocument, + variables: { + ...variables, + fromRefName: diffVariables.fromCommitId, + toRefName: diffVariables.toCommitId, + }, + }, + { + query: gen.DiffSummariesDocument, + variables: diffVariables, + }, { query: gen.TableNamesDocument, variables: { ...variables, filterSystemTables: true }, }, ]; +}; export const refetchTableQueries = (variables: TableParams) => [ { query: gen.DataTableQueryDocument, variables }, diff --git a/packages/web/package.json b/packages/web/package.json index 11722090..d98268ef 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -26,6 +26,7 @@ "chance": "^1.1.11", "classnames": "^2.3.2", "commit-graph": "^1.6.0", + "diff": "^5.1.0", "github-markdown-css": "^5.3.0", "graphql": "^16.8.1", "isomorphic-unfetch": "^4.0.2", @@ -39,6 +40,7 @@ "react-ace": "^10.1.0", "react-copy-to-clipboard": "^5.1.0", "react-data-grid": "7.0.0-beta.40", + "react-diff-viewer": "^3.1.1", "react-dom": "latest", "react-flow-renderer": "^10.3.17", "react-hotkeys": "^2.0.0", @@ -62,6 +64,7 @@ "@testing-library/user-event": "^14.5.1", "@types/apollo-upload-client": "^17.0.4", "@types/chance": "^1.1.5", + "@types/diff": "^5.0.7", "@types/jest": "^29.5.6", "@types/lodash": "^4.14.199", "@types/node": "^20.8.4", diff --git a/packages/web/pages/database/[databaseName]/compare/[refName]/[diffRange].tsx b/packages/web/pages/database/[databaseName]/compare/[refName]/[diffRange].tsx new file mode 100644 index 00000000..4731f471 --- /dev/null +++ b/packages/web/pages/database/[databaseName]/compare/[refName]/[diffRange].tsx @@ -0,0 +1,43 @@ +import Page from "@components/util/Page"; +import { RefParams } from "@lib/params"; +import DatabasePage from "@pageComponents/DatabasePage"; +import { GetServerSideProps, NextPage } from "next"; + +type Params = RefParams & { + diffRange: string; +}; + +type Props = { + params: Params; + tableName?: string; +}; + +const DiffRangePage: NextPage = ({ params, tableName }) => ( + + + +); + +export const getServerSideProps: GetServerSideProps = async ({ + params, + query, +}) => { + return { + props: { + params: params as Params, + tableName: query.tableName ? String(query.tableName) : "", + }, + }; +}; + +export default DiffRangePage; diff --git a/packages/web/pages/database/[databaseName]/compare/[refName]/index.tsx b/packages/web/pages/database/[databaseName]/compare/[refName]/index.tsx new file mode 100644 index 00000000..bc327184 --- /dev/null +++ b/packages/web/pages/database/[databaseName]/compare/[refName]/index.tsx @@ -0,0 +1,39 @@ +import Page from "@components/util/Page"; +import { RefParams } from "@lib/params"; +import DatabasePage from "@pageComponents/DatabasePage"; +import { GetServerSideProps, NextPage } from "next"; + +type Props = { + params: RefParams; + tableName?: string; +}; + +const DiffForRefPage: NextPage = ({ params, ...props }) => ( + + + +); + +export const getServerSideProps: GetServerSideProps = async ({ + params, + query, +}) => { + return { + props: { + params: params as RefParams, + tableName: query.tableName ? String(query.tableName) : "", + }, + }; +}; + +export default DiffForRefPage; diff --git a/packages/web/pages/database/[databaseName]/compare/index.tsx b/packages/web/pages/database/[databaseName]/compare/index.tsx new file mode 100644 index 00000000..8e47e49a --- /dev/null +++ b/packages/web/pages/database/[databaseName]/compare/index.tsx @@ -0,0 +1,26 @@ +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 DiffPageForDefaultBranch: NextPage = props => ( + + + +); + +export const getServerSideProps: GetServerSideProps = async ({ + params, +}) => { + return { + props: { + params: params as DatabaseParams, + }, + }; +}; + +export default DiffPageForDefaultBranch; diff --git a/yarn.lock b/yarn.lock index 4e59d1a5..b5e99acd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -538,7 +538,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15": +"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15": version: 7.22.15 resolution: "@babel/helper-module-imports@npm:7.22.15" dependencies: @@ -1122,6 +1122,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.7.2": + version: 7.23.2 + resolution: "@babel/runtime@npm:7.23.2" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: 6c4df4839ec75ca10175f636d6362f91df8a3137f86b38f6cd3a4c90668a0fe8e9281d320958f4fbd43b394988958585a17c3aab2a4ea6bf7316b22916a371fb + languageName: node + linkType: hard + "@babel/template@npm:^7.18.10, @babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3": version: 7.22.15 resolution: "@babel/template@npm:7.22.15" @@ -1616,6 +1625,7 @@ __metadata: "@testing-library/user-event": ^14.5.1 "@types/apollo-upload-client": ^17.0.4 "@types/chance": ^1.1.5 + "@types/diff": ^5.0.7 "@types/jest": ^29.5.6 "@types/lodash": ^4.14.199 "@types/node": ^20.8.4 @@ -1633,6 +1643,7 @@ __metadata: classnames: ^2.3.2 commit-graph: ^1.6.0 cssnano: ^6.0.1 + diff: ^5.1.0 eslint: latest eslint-config-next: latest github-markdown-css: ^5.3.0 @@ -1652,6 +1663,7 @@ __metadata: react-ace: ^10.1.0 react-copy-to-clipboard: ^5.1.0 react-data-grid: 7.0.0-beta.40 + react-diff-viewer: ^3.1.1 react-dom: latest react-flow-renderer: ^10.3.17 react-hotkeys: ^2.0.0 @@ -1691,6 +1703,18 @@ __metadata: languageName: node linkType: hard +"@emotion/cache@npm:^10.0.27": + version: 10.0.29 + resolution: "@emotion/cache@npm:10.0.29" + dependencies: + "@emotion/sheet": 0.9.4 + "@emotion/stylis": 0.8.5 + "@emotion/utils": 0.11.3 + "@emotion/weak-memoize": 0.2.5 + checksum: 78b37fb0c2e513c90143a927abef229e995b6738ef8a92ce17abe2ed409b38859ddda7c14d7f4854d6f4e450b6db50231532f53a7fec4903d7ae775b2ae3fd64 + languageName: node + linkType: hard + "@emotion/cache@npm:^11.11.0, @emotion/cache@npm:^11.4.0": version: 11.11.0 resolution: "@emotion/cache@npm:11.11.0" @@ -1704,6 +1728,13 @@ __metadata: languageName: node linkType: hard +"@emotion/hash@npm:0.8.0": + version: 0.8.0 + resolution: "@emotion/hash@npm:0.8.0" + checksum: 4b35d88a97e67275c1d990c96d3b0450451d089d1508619488fc0acb882cb1ac91e93246d471346ebd1b5402215941ef4162efe5b51534859b39d8b3a0e3ffaa + languageName: node + linkType: hard + "@emotion/hash@npm:^0.9.1": version: 0.9.1 resolution: "@emotion/hash@npm:0.9.1" @@ -1711,6 +1742,13 @@ __metadata: languageName: node linkType: hard +"@emotion/memoize@npm:0.7.4": + version: 0.7.4 + resolution: "@emotion/memoize@npm:0.7.4" + checksum: 4e3920d4ec95995657a37beb43d3f4b7d89fed6caa2b173a4c04d10482d089d5c3ea50bbc96618d918b020f26ed6e9c4026bbd45433566576c1f7b056c3271dc + languageName: node + linkType: hard + "@emotion/memoize@npm:^0.8.1": version: 0.8.1 resolution: "@emotion/memoize@npm:0.8.1" @@ -1739,6 +1777,19 @@ __metadata: languageName: node linkType: hard +"@emotion/serialize@npm:^0.11.15, @emotion/serialize@npm:^0.11.16": + version: 0.11.16 + resolution: "@emotion/serialize@npm:0.11.16" + dependencies: + "@emotion/hash": 0.8.0 + "@emotion/memoize": 0.7.4 + "@emotion/unitless": 0.7.5 + "@emotion/utils": 0.11.3 + csstype: ^2.5.7 + checksum: 2949832fab9d803e6236f2af6aad021c09c6b6722ae910b06b4ec3bfb84d77cbecfe3eab9a7dcc269ac73e672ef4b696c7836825931670cb110731712e331438 + languageName: node + linkType: hard + "@emotion/serialize@npm:^1.1.2": version: 1.1.2 resolution: "@emotion/serialize@npm:1.1.2" @@ -1752,6 +1803,13 @@ __metadata: languageName: node linkType: hard +"@emotion/sheet@npm:0.9.4": + version: 0.9.4 + resolution: "@emotion/sheet@npm:0.9.4" + checksum: 53bb833b4bb69ea2af04e1ecad164f78fb2614834d2820f584c909686a8e047c44e96a6e824798c5c558e6d95e10772454a9e5c473c5dbe0d198e50deb2815bc + languageName: node + linkType: hard + "@emotion/sheet@npm:^1.2.2": version: 1.2.2 resolution: "@emotion/sheet@npm:1.2.2" @@ -1759,6 +1817,20 @@ __metadata: languageName: node linkType: hard +"@emotion/stylis@npm:0.8.5": + version: 0.8.5 + resolution: "@emotion/stylis@npm:0.8.5" + checksum: 67ff5958449b2374b329fb96e83cb9025775ffe1e79153b499537c6c8b2eb64b77f32d7b5d004d646973662356ceb646afd9269001b97c54439fceea3203ce65 + languageName: node + linkType: hard + +"@emotion/unitless@npm:0.7.5": + version: 0.7.5 + resolution: "@emotion/unitless@npm:0.7.5" + checksum: f976e5345b53fae9414a7b2e7a949aa6b52f8bdbcc84458b1ddc0729e77ba1d1dfdff9960e0da60183877873d3a631fa24d9695dd714ed94bcd3ba5196586a6b + languageName: node + linkType: hard + "@emotion/unitless@npm:^0.8.1": version: 0.8.1 resolution: "@emotion/unitless@npm:0.8.1" @@ -1775,6 +1847,13 @@ __metadata: languageName: node linkType: hard +"@emotion/utils@npm:0.11.3": + version: 0.11.3 + resolution: "@emotion/utils@npm:0.11.3" + checksum: 9c4204bda84f9acd153a9be9478a83f9baa74d5d7a4c21882681c4d1b86cd113b84540cb1f92e1c30313b5075f024da2658dbc553f5b00776ef9b6ec7991c0c9 + languageName: node + linkType: hard + "@emotion/utils@npm:^1.2.1": version: 1.2.1 resolution: "@emotion/utils@npm:1.2.1" @@ -1782,6 +1861,13 @@ __metadata: languageName: node linkType: hard +"@emotion/weak-memoize@npm:0.2.5": + version: 0.2.5 + resolution: "@emotion/weak-memoize@npm:0.2.5" + checksum: 27d402b0c683b94658220b6d47840346ee582329ca2a15ec9c233492e0f1a27687ccb233b76eedc922f2e185e444cc89f7b97a81a1d3e5ae9f075bab08e965ea + languageName: node + linkType: hard + "@emotion/weak-memoize@npm:^0.3.1": version: 0.3.1 resolution: "@emotion/weak-memoize@npm:0.3.1" @@ -4100,6 +4186,13 @@ __metadata: languageName: node linkType: hard +"@types/diff@npm:^5.0.7": + version: 5.0.7 + resolution: "@types/diff@npm:5.0.7" + checksum: 38cad7b1e19789a11bfab2428409476ea204705e92c39dd13a9ba26b604930efcffb553352512296cb179adfb028e2d95e51f9df2b7443bb0a6ba306c8bc84d0 + languageName: node + linkType: hard + "@types/eslint-scope@npm:^3.7.3": version: 3.7.4 resolution: "@types/eslint-scope@npm:3.7.4" @@ -5539,6 +5632,24 @@ __metadata: languageName: node linkType: hard +"babel-plugin-emotion@npm:^10.0.27": + version: 10.2.2 + resolution: "babel-plugin-emotion@npm:10.2.2" + dependencies: + "@babel/helper-module-imports": ^7.0.0 + "@emotion/hash": 0.8.0 + "@emotion/memoize": 0.7.4 + "@emotion/serialize": ^0.11.16 + babel-plugin-macros: ^2.0.0 + babel-plugin-syntax-jsx: ^6.18.0 + convert-source-map: ^1.5.0 + escape-string-regexp: ^1.0.5 + find-root: ^1.1.0 + source-map: ^0.5.7 + checksum: 763f38c67ffbe7d091691d68c74686ba478296cc24716699fb5b0feddce1b1b47878a20b0bbe2aa4dea17f41074ead4deae7935d2cf6823638766709812c5b40 + languageName: node + linkType: hard + "babel-plugin-istanbul@npm:^6.1.1": version: 6.1.1 resolution: "babel-plugin-istanbul@npm:6.1.1" @@ -5564,6 +5675,17 @@ __metadata: languageName: node linkType: hard +"babel-plugin-macros@npm:^2.0.0": + version: 2.8.0 + resolution: "babel-plugin-macros@npm:2.8.0" + dependencies: + "@babel/runtime": ^7.7.2 + cosmiconfig: ^6.0.0 + resolve: ^1.12.0 + checksum: 59b09a21cf3ae1e14186c1b021917d004b49b953824b24953a54c6502da79e8051d4ac31cfd4a0ae7f6ea5ddf1f7edd93df4895dd3c3982a5b2431859c2889ac + languageName: node + linkType: hard + "babel-plugin-macros@npm:^3.1.0": version: 3.1.0 resolution: "babel-plugin-macros@npm:3.1.0" @@ -5575,6 +5697,13 @@ __metadata: languageName: node linkType: hard +"babel-plugin-syntax-jsx@npm:^6.18.0": + version: 6.18.0 + resolution: "babel-plugin-syntax-jsx@npm:6.18.0" + checksum: 0c7ce5b81d6cfc01a7dd7a76a9a8f090ee02ba5c890310f51217ef1a7e6163fb7848994bbc14fd560117892e82240df9c7157ad0764da67ca5f2afafb73a7d27 + languageName: node + linkType: hard + "babel-plugin-syntax-trailing-function-commas@npm:^7.0.0-beta.0": version: 7.0.0-beta.0 resolution: "babel-plugin-syntax-trailing-function-commas@npm:7.0.0-beta.0" @@ -6188,7 +6317,7 @@ __metadata: languageName: node linkType: hard -"classnames@npm:^2.3.0, classnames@npm:^2.3.2": +"classnames@npm:^2.2.6, classnames@npm:^2.3.0, classnames@npm:^2.3.2": version: 2.3.2 resolution: "classnames@npm:2.3.2" checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e @@ -6609,6 +6738,19 @@ __metadata: languageName: node linkType: hard +"cosmiconfig@npm:^6.0.0": + version: 6.0.0 + resolution: "cosmiconfig@npm:6.0.0" + dependencies: + "@types/parse-json": ^4.0.0 + import-fresh: ^3.1.0 + parse-json: ^5.0.0 + path-type: ^4.0.0 + yaml: ^1.7.2 + checksum: 8eed7c854b91643ecb820767d0deb038b50780ecc3d53b0b19e03ed8aabed4ae77271198d1ae3d49c3b110867edf679f5faad924820a8d1774144a87cb6f98fc + languageName: node + linkType: hard + "cosmiconfig@npm:^7.0.0, cosmiconfig@npm:^7.0.1": version: 7.1.0 resolution: "cosmiconfig@npm:7.1.0" @@ -6639,6 +6781,18 @@ __metadata: languageName: node linkType: hard +"create-emotion@npm:^10.0.14, create-emotion@npm:^10.0.27": + version: 10.0.27 + resolution: "create-emotion@npm:10.0.27" + dependencies: + "@emotion/cache": ^10.0.27 + "@emotion/serialize": ^0.11.15 + "@emotion/sheet": 0.9.4 + "@emotion/utils": 0.11.3 + checksum: 6838f6fe0a3e8d6a7a354685f1ffed6a23f28e74ce4bcdd045882f8e007715dc61596bddc05937cdfd67581eb66a697621a6d01c1a7357b85a816fddc22203fe + languageName: node + linkType: hard + "create-jest@npm:^29.7.0": version: 29.7.0 resolution: "create-jest@npm:29.7.0" @@ -6917,6 +7071,13 @@ __metadata: languageName: node linkType: hard +"csstype@npm:^2.5.7": + version: 2.6.21 + resolution: "csstype@npm:2.6.21" + checksum: 2ce8bc832375146eccdf6115a1f8565a27015b74cce197c35103b4494955e9516b246140425ad24103864076aa3e1257ac9bab25a06c8d931dd87a6428c9dccf + languageName: node + linkType: hard + "csstype@npm:^3.0.2": version: 3.1.2 resolution: "csstype@npm:3.1.2" @@ -7315,6 +7476,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^5.1.0": + version: 5.1.0 + resolution: "diff@npm:5.1.0" + checksum: c7bf0df7c9bfbe1cf8a678fd1b2137c4fb11be117a67bc18a0e03ae75105e8533dbfb1cda6b46beb3586ef5aed22143ef9d70713977d5fb1f9114e21455fba90 + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -7517,6 +7685,16 @@ __metadata: languageName: node linkType: hard +"emotion@npm:^10.0.14": + version: 10.0.27 + resolution: "emotion@npm:10.0.27" + dependencies: + babel-plugin-emotion: ^10.0.27 + create-emotion: ^10.0.27 + checksum: e925a6ae323e77df7fd3064004c16d37a150c838d5fbf96b50075e5176315d0e4b8b6d60ca036fa767c0eabecbe941ec79382f4d3a392d9a06f84b6039d0af49 + languageName: node + linkType: hard + "encodeurl@npm:~1.0.2": version: 1.0.2 resolution: "encodeurl@npm:1.0.2" @@ -9495,7 +9673,7 @@ __metadata: languageName: node linkType: hard -"import-fresh@npm:^3.2.1, import-fresh@npm:^3.3.0": +"import-fresh@npm:^3.1.0, import-fresh@npm:^3.2.1, import-fresh@npm:^3.3.0": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" dependencies: @@ -11424,6 +11602,13 @@ __metadata: languageName: node linkType: hard +"memoize-one@npm:^5.0.4": + version: 5.2.1 + resolution: "memoize-one@npm:5.2.1" + checksum: a3cba7b824ebcf24cdfcd234aa7f86f3ad6394b8d9be4c96ff756dafb8b51c7f71320785fbc2304f1af48a0467cbbd2a409efc9333025700ed523f254cb52e3d + languageName: node + linkType: hard + "memoize-one@npm:^6.0.0": version: 6.0.0 resolution: "memoize-one@npm:6.0.0" @@ -13792,6 +13977,23 @@ __metadata: languageName: node linkType: hard +"react-diff-viewer@npm:^3.1.1": + version: 3.1.1 + resolution: "react-diff-viewer@npm:3.1.1" + dependencies: + classnames: ^2.2.6 + create-emotion: ^10.0.14 + diff: ^4.0.1 + emotion: ^10.0.14 + memoize-one: ^5.0.4 + prop-types: ^15.6.2 + peerDependencies: + react: ^15.3.0 || ^16.0.0 + react-dom: ^15.3.0 || ^16.0.0 + checksum: 073445730ecc617768107ca6ccdd0cc93a1b42fa472d4d20aee13de9ddcf1bbcae00b64a350fb07ad1da619ca04ed1c5111d9b170b325e40e44920145170c7c1 + languageName: node + linkType: hard + "react-dom@npm:^18.2.0, react-dom@npm:latest": version: 18.2.0 resolution: "react-dom@npm:18.2.0" @@ -14278,6 +14480,19 @@ __metadata: languageName: node linkType: hard +"resolve@npm:^1.12.0": + version: 1.22.8 + resolution: "resolve@npm:1.22.8" + dependencies: + is-core-module: ^2.13.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: f8a26958aa572c9b064562750b52131a37c29d072478ea32e129063e2da7f83e31f7f11e7087a18225a8561cfe8d2f0df9dbea7c9d331a897571c0a2527dbb4c + languageName: node + linkType: hard + "resolve@npm:^2.0.0-next.4": version: 2.0.0-next.4 resolution: "resolve@npm:2.0.0-next.4" @@ -14304,6 +14519,19 @@ __metadata: languageName: node linkType: hard +"resolve@patch:resolve@^1.12.0#~builtin": + version: 1.22.8 + resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin::version=1.22.8&hash=c3c19d" + dependencies: + is-core-module: ^2.13.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: 5479b7d431cacd5185f8db64bfcb7286ae5e31eb299f4c4f404ad8aa6098b77599563ac4257cb2c37a42f59dfc06a1bec2bcf283bb448f319e37f0feb9a09847 + languageName: node + linkType: hard + "resolve@patch:resolve@^2.0.0-next.4#~builtin": version: 2.0.0-next.4 resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin::version=2.0.0-next.4&hash=c3c19d" @@ -16882,7 +17110,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^1.10.0": +"yaml@npm:^1.10.0, yaml@npm:^1.7.2": version: 1.10.2 resolution: "yaml@npm:1.10.2" checksum: ce4ada136e8a78a0b08dc10b4b900936912d15de59905b2bf415b4d33c63df1d555d23acb2a41b23cf9fb5da41c256441afca3d6509de7247daa062fd2c5ea5f