diff --git a/api/prisma/migrations/20230819031410_add_y_doc_snapshot_model/migration.sql b/api/prisma/migrations/20230819031410_add_y_doc_snapshot_model/migration.sql new file mode 100644 index 00000000..043c0161 --- /dev/null +++ b/api/prisma/migrations/20230819031410_add_y_doc_snapshot_model/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "YDocSnapshot" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "message" TEXT, + "yDocBlob" BYTEA NOT NULL, + "repoId" TEXT NOT NULL, + + CONSTRAINT "YDocSnapshot_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "YDocSnapshot" ADD CONSTRAINT "YDocSnapshot_repoId_fkey" FOREIGN KEY ("repoId") REFERENCES "Repo"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 45aecf1e..dcc15a20 100755 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -67,6 +67,15 @@ model UserRepoData { @@id([userId, repoId]) } +model YDocSnapshot { + id String @id + createdAt DateTime @default(now()) + message String? + yDocBlob Bytes + repo Repo @relation(fields: [repoId], references: [id]) + repoId String +} + model Repo { id String @id name String? @@ -83,6 +92,8 @@ model Repo { UserRepoData UserRepoData[] stargazers User[] @relation("STAR") yDocBlob Bytes? + yDocSnapshots YDocSnapshot[] + } enum PodType { diff --git a/api/src/resolver_repo.ts b/api/src/resolver_repo.ts index 17a2ca7f..b0afdf13 100644 --- a/api/src/resolver_repo.ts +++ b/api/src/resolver_repo.ts @@ -515,10 +515,50 @@ async function copyRepo(_, { repoId }, { userId }) { return id; } +/** + * Create yDoc snapshot upon request. + */ +async function addRepoSnapshot(_, { repoId, message }) { + const repo = await prisma.repo.findFirst({ + where: { id: repoId }, + include: { + owner: true, + collaborators: true, + }, + }); + if (!repo) throw Error("Repo not exists."); + if (!repo.yDocBlob) throw Error(`yDocBlob on ${repoId} not found`); + const snapshot = await prisma.yDocSnapshot.create({ + data: { + id: await nanoid(), + yDocBlob: repo.yDocBlob, + message: message, + repo: { connect: { id: repoId } }, + }, + }); + return snapshot.id; +} + +/** + * Fetch yDoc snapshots for a repo. + */ +async function getRepoSnapshots(_, { repoId }) { + const snapshots = await prisma.yDocSnapshot.findMany({ + where: { repo: { id: repoId } }, + }); + if (!snapshots) throw Error(`No snapshot exists for repo ${repoId}.`); + + return snapshots.map((snapshot) => ({ + ...snapshot, + yDocBlob: JSON.stringify(snapshot.yDocBlob), + })); +} + export default { Query: { repo, getDashboardRepos, + getRepoSnapshots, }, Mutation: { createRepo, @@ -531,6 +571,7 @@ export default { addCollaborator, updateVisibility, deleteCollaborator, + addRepoSnapshot, star, unstar, }, diff --git a/api/src/typedefs.ts b/api/src/typedefs.ts index 78354554..fd94c04d 100644 --- a/api/src/typedefs.ts +++ b/api/src/typedefs.ts @@ -94,6 +94,12 @@ export const typeDefs = gql` ttl: Int } + type YDocSnapshot { + id: String + createdAt: String + message: String + } + input RunSpecInput { code: String podId: String @@ -107,6 +113,7 @@ export const typeDefs = gql` repo(id: String!): Repo pod(id: ID!): Pod getDashboardRepos: [Repo] + getRepoSnapshots(repoId: String!): [YDocSnapshot] activeSessions: [String] listAllRuntimes: [RuntimeInfo] infoRuntime(sessionId: String!): RuntimeInfo @@ -142,6 +149,7 @@ export const typeDefs = gql` exportJSON(repoId: String!): String! exportFile(repoId: String!): String! + addRepoSnapshot(repoId: String!, message: String!): String! updateCodeiumAPIKey(apiKey: String!): Boolean connectRuntime(runtimeId: String, repoId: String): Boolean diff --git a/ui/src/components/Sidebar.tsx b/ui/src/components/Sidebar.tsx index bbd58dc0..b0081c9b 100644 --- a/ui/src/components/Sidebar.tsx +++ b/ui/src/components/Sidebar.tsx @@ -17,6 +17,7 @@ import List from "@mui/material/List"; import ListItem from "@mui/material/ListItem"; import ListItemText from "@mui/material/ListItemText"; import ListItemButton from "@mui/material/ListItemButton"; +import AddIcon from "@mui/icons-material/Add"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import RestartAltIcon from "@mui/icons-material/RestartAlt"; @@ -1337,6 +1338,105 @@ function TableofPods() { ); } +function SnapshotItem({ id, message }) { + const [anchorEl, setAnchorEl] = useState(null); + + const open = Boolean(anchorEl); + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + return ( + + + + + + + + Restore + Delete + + + + ); +} + +function RepoSnapshots() { + const { id: repoId } = useParams(); + const { error: queryError, data: queryResult } = useQuery(gql` + query GetRepoSnapshots { + getRepoSnapshots(repoId: "${repoId}") { + id + createdAt + message + } + } + `); + const [addRepoSnapshot, { error: mutationError, data: mutationResult }] = + useMutation( + gql` + mutation addRepoSnapshot($repoId: String!, $message: String!) { + addRepoSnapshot(repoId: $repoId, message: $message) + } + `, + { + refetchQueries: ["GetRepoSnapshots"], + } + ); + + if (queryError) { + return ERROR: {queryError.message}; + } + + const snapshots = queryResult?.getRepoSnapshots.slice(); + return ( + + + + Snapshots + + { + addRepoSnapshot({ + variables: { repoId: repoId, message: "placeholder" }, + }); + }} + > + + + + + {snapshots && + snapshots.length > 0 && + snapshots.map((snapshot) => ( + + ))} + + + ); +} + export const Sidebar: React.FC = ({ width, open, @@ -1422,6 +1522,9 @@ export const Sidebar: React.FC = ({ Table of Pods + + +