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 (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+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
+
+
+