Skip to content

Commit

Permalink
feat: ability to approve and reject completions
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelovicentegc committed Oct 22, 2023
1 parent 13a6d77 commit 83fa3ac
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 28 deletions.
6 changes: 3 additions & 3 deletions app/api/data/completions/review/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ export async function POST(req) {

const body = await req.json();

if (!body._id && !body.direction) {
if (!body?.data?._id && !body.direction) {
return new NextResponse(JSON.stringify("Bad Request"), { status: 400 });
}

try {
const { _id, direction } = body;
const { direction, data } = body;
validateDirection(direction);

const result = await reviewCompletion(_id, direction);
const result = await reviewCompletion(data, direction);

return new NextResponse(JSON.stringify(result), {
status: 200,
Expand Down
13 changes: 11 additions & 2 deletions app/api/data/completions/route.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CHIRON_FOREIGN_KEY, CHIRON_VENDOR_ID } from "@/lib/config";
import { getApiKey } from "@/lib/db/reads";
import { saveCompletion } from "@/lib/db/writes";
import { decrypt } from "@/lib/encryption";
Expand Down Expand Up @@ -45,14 +46,22 @@ export async function POST(req) {

const body = await req.json();

if (!body) {
if (!body || !body?._id) {
return new NextResponse("Bad Request", {
status: 400,
});
}

const data = {
...body,
[CHIRON_VENDOR_ID]: vendorId,
[CHIRON_FOREIGN_KEY]: body._id,
};

delete data._id;

try {
await saveCompletion(body);
await saveCompletion(data);

return new NextResponse("Created", {
status: 201,
Expand Down
21 changes: 20 additions & 1 deletion app/completions/pending/page.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Empty from "@/components/empty";
import { CompletionsContainer } from "@/containers/completions";
import { CHIRON_FOREIGN_KEY, CHIRON_VENDOR_ID } from "@/lib/config";
import { f } from "@/lib/fetch";

export default async function PendingCompletionsReviewPage() {
Expand All @@ -9,5 +11,22 @@ export default async function PendingCompletionsReviewPage() {
return <Empty empty={{ description: "No pending reviews available" }} />;
}

return <></>;
const completions = pendingReviews.map(
({
[CHIRON_FOREIGN_KEY]: fk,
[CHIRON_VENDOR_ID]: vendorId,
...completion
}) => {
return [
completion,
{
[CHIRON_FOREIGN_KEY]: fk,
[CHIRON_VENDOR_ID]: vendorId,
...completion,
},
];
},
);

return <CompletionsContainer completions={completions} />;
}
Binary file modified bun.lockb
Binary file not shown.
40 changes: 40 additions & 0 deletions components/editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { DiffEditor } from "@monaco-editor/react";
import { useEffect } from "react";

export function ScriptEditor(props) {
const {
code,
originalCode,
editorOptions,
onInitializePane,
monacoEditorRef,
editorRef,
} = props;

useEffect(() => {
if (monacoEditorRef?.current) {
const model = monacoEditorRef.current.getModels();

if (model?.length > 0) {
onInitializePane(monacoEditorRef, editorRef, model);
}
}
}, []);

return (
<DiffEditor
height="42.9rem"
language="json"
onMount={(editor, monaco) => {
monacoEditorRef.current = monaco.editor;
editorRef.current = editor;
}}
options={editorOptions}
theme="light"
modified={code}
original={originalCode}
/>
);
}

export default ScriptEditor;
1 change: 1 addition & 0 deletions containers/api-management.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export default function ApiManagementContainer(props) {
<>
<Heading>API Management</Heading>
<Button label="Add" onClick={onOpen} />
<br />
<List
data={vendors}
pad={{ left: "small", right: "none" }}
Expand Down
126 changes: 126 additions & 0 deletions containers/completions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"use client";

import { Box, Button, List } from "grommet";
import { useRef, useState } from "react";
import { Like, Dislike } from "grommet-icons";
import ScriptEditor from "@/components/editor";
import {
CHIRON_PREFIX,
CHIRON_FOREIGN_KEY,
CHIRON_VENDOR_ID,
} from "@/lib/config";

const chironIdxKey = CHIRON_PREFIX + "idx";

export function CompletionsContainer(props) {
const { completions } = props;
const [selected, setSelected] = useState();
const [reviewing, setReviewing] = useState(false);
const monacoEditorRef = useRef(null);
const editorRef = useRef(null);

const onInitializePane = (_, __, model) => {
editorRef.current.focus();
monacoEditorRef.current.setModelMarkers(model[0], "owner", null);
};

const onReview = async (direction) => {
setReviewing(true);
const data = editorRef.current.getModifiedEditor().getValue();

const completionWithForeignKey = completions.find((completion) => {
return completion[0]._id === JSON.parse(selected.item)._id;
})[1];

if (
!completionWithForeignKey?.[CHIRON_VENDOR_ID] ||
!completionWithForeignKey?.[CHIRON_FOREIGN_KEY]
) {
setReviewing(false);
return alert("Missing data");
}

const res = await fetch("/api/data/completions/review", {
method: "POST",
body: JSON.stringify({
direction,
data: {
...JSON.parse(data),
[CHIRON_VENDOR_ID]: selected[CHIRON_VENDOR_ID],
[CHIRON_FOREIGN_KEY]: selected[CHIRON_FOREIGN_KEY],
},
}),
});

if (res.status !== 200) {
alert(res.statusText);
} else {
alert("Sucess");
}

setReviewing(false);
};

return (
<Box gap="medium">
<List
data={completions.map((completion) => completion[0])}
itemProps={
selected?.[chironIdxKey] >= 0
? { [selected?.[chironIdxKey]]: { background: "brand" } }
: undefined
}
onClickItem={(event) => {
const currentlySelected =
selected?.[chironIdxKey] === event.index
? undefined
: {
[chironIdxKey]: event.index,
item: JSON.stringify(event.item, null, 2),
};
setSelected(currentlySelected);
}}
pad={{ left: "small", right: "none", top: "small", bottom: "small" }}
/>
{selected?.item ? (
<Box
height={{
min: "42.9rem",
}}
>
<ScriptEditor
code={selected.item}
originalCode={selected.item}
onInitializePane={onInitializePane}
editorRef={editorRef}
monacoEditorRef={monacoEditorRef}
/>
<Box
align="center"
justify="center"
direction="row"
pad="large"
gap="medium"
>
<Button
label="Approve"
icon={<Like />}
disabled={reviewing}
primary
color="neutral-1"
onClick={() => onReview("pending2approve")}
/>
<Button
label="Reject"
icon={<Dislike />}
disabled={reviewing}
primary
color="neutral-4"
onClick={() => onReview("pending2reject")}
/>
</Box>
</Box>
) : null}
</Box>
);
}
6 changes: 6 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export const BRAND_HEX = "#7E4CDB";

export const CHIRON_PREFIX = "____CHIRON____";

// Chiron database fields
export const CHIRON_VENDOR_ID = CHIRON_PREFIX + "vendorId";
export const CHIRON_FOREIGN_KEY = CHIRON_PREFIX + "_id";
52 changes: 30 additions & 22 deletions lib/db/writes.js
Original file line number Diff line number Diff line change
@@ -1,74 +1,82 @@
import { ObjectId } from "mongodb";
import { ObjectId } from "bson";
import {
getApiKeysCollection,
getApprovedCompletionsCollection,
getCompletionsPendingReviewCollection,
getRejectedCompletionsCollection,
} from "../mongodb";

async function approveCompletion(completionId) {
async function approveCompletion(data) {
const [collection, approvedCollection] = await Promise.all([
getCompletionsPendingReviewCollection(),
getApprovedCompletionsCollection(),
]);

const completion = await collection.findOne({ _id: ObjectId(completionId) });
const approvedResult = await approvedCollection.insertOne(completion);
await collection.deleteOne({ _id: ObjectId(completionId) });
const id = data._id;
delete data._id;

const approvedResult = await approvedCollection.insertOne(data);
await collection.deleteOne({ _id: new ObjectId(id) });

return approvedResult;
}

async function rejectCompletion(completionId) {
async function rejectCompletion(data) {
const [collection, rejectedCollection] = await Promise.all([
getCompletionsPendingReviewCollection(),
getRejectedCompletionsCollection(),
]);

const completion = await collection.findOne({ _id: ObjectId(completionId) });
const rejectedResult = await rejectedCollection.insertOne(completion);
await collection.deleteOne({ _id: ObjectId(completionId) });
const id = data._id;
delete data._id;

const rejectedResult = await rejectedCollection.insertOne(data);
await collection.deleteOne({ _id: new ObjectId(id) });

return rejectedResult;
}

async function approve2Reject(completionId) {
async function approve2Reject(data) {
const [collection, rejectedCollection] = await Promise.all([
getApprovedCompletionsCollection(),
getRejectedCompletionsCollection(),
]);

const completion = await collection.findOne({ _id: ObjectId(completionId) });
const rejectedResult = await rejectedCollection.insertOne(completion);
await collection.deleteOne({ _id: ObjectId(completionId) });
const id = data._id;
delete data._id;

const rejectedResult = await rejectedCollection.insertOne(data);
await collection.deleteOne({ _id: new ObjectId(id) });

return rejectedResult;
}

async function reject2Approve(completionId) {
async function reject2Approve(data) {
const [collection, approvedCollection] = await Promise.all([
getRejectedCompletionsCollection(),
getApprovedCompletionsCollection(),
]);

const completion = await collection.findOne({ _id: ObjectId(completionId) });
const approvedResult = await approvedCollection.insertOne(completion);
await collection.deleteOne({ _id: ObjectId(completionId) });
const id = data._id;
delete data._id;
const approvedResult = await approvedCollection.insertOne(data);
await collection.deleteOne({ _id: new ObjectId(id) });

return approvedResult;
}

export async function reviewCompletion(completionId, direction) {
export async function reviewCompletion(data, direction) {
// TODO: Does approve2pending; reject2pending make sense?
switch (direction) {
case "pending2approve":
return approveCompletion(completionId);
return approveCompletion(data);
case "pending2reject":
return rejectCompletion(completionId);
return rejectCompletion(data);
// TODO: Support these use cases on the front-end, eventually
case "approve2reject":
return approve2Reject(completionId);
return approve2Reject(data);
case "reject2approve":
return reject2Approve(completionId);
return reject2Approve(data);
default:
throw new Error("Invalid direction");
}
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
},
"dependencies": {
"@auth/mongodb-adapter": "^2.0.1",
"@monaco-editor/react": "^4.6.0",
"grommet": "^2.33.2",
"monaco-editor": "^0.44.0",
"mongodb": "^6.1.0",
"next": "13.5.4",
"next-auth": "^4.23.2",
"nodemailer": "^6.9.6",
"react": "^18",
"react-dom": "^18",
"react-monaco-editor": "^0.54.0",
"styled-components": "5"
},
"devDependencies": {
Expand All @@ -27,6 +30,7 @@
"eslint-config-prettier": "^9.0.0",
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"monaco-editor-webpack-plugin": "^7.1.0",
"prettier": "^3.0.3",
"semantic-release": "^22.0.5"
}
Expand Down
Loading

0 comments on commit 83fa3ac

Please sign in to comment.