From fdd3805c795a93d34fa8b90cd556f82047b56e0a Mon Sep 17 00:00:00 2001
From: PaliC <>
Date: Fri, 13 Oct 2023 14:53:31 -0700
Subject: [PATCH] add buttons to rate logs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds buttons hidden under a feature flag which gives us the ability to rate logs
### 🤖 Generated by Copilot at e97f54f
This pull request adds the feature to annotate the log rating of a job in the `LogViewer` component. It introduces a new component `LogAnnotationToggle` that allows users to select an annotation and sends it to a new API handler `/api/log_annotation`. It also defines a new type `LogAnnotation` and stores the annotation data in a DynamoDB table.
---
torchci/components/LogAnnotationToggle.tsx | 60 +++++++++++++++++++
torchci/components/LogViewer.tsx | 22 ++++++-
torchci/lib/types.ts | 8 +++
.../[repoOwner]/[repoName]/[annotation].ts | 60 +++++++++++++++++++
4 files changed, 148 insertions(+), 2 deletions(-)
create mode 100644 torchci/components/LogAnnotationToggle.tsx
create mode 100644 torchci/pages/api/log_annotation/[repoOwner]/[repoName]/[annotation].ts
diff --git a/torchci/components/LogAnnotationToggle.tsx b/torchci/components/LogAnnotationToggle.tsx
new file mode 100644
index 0000000000..5671cccad9
--- /dev/null
+++ b/torchci/components/LogAnnotationToggle.tsx
@@ -0,0 +1,60 @@
+import React from "react";
+import { JobData, LogAnnotation } from "../lib/types";
+import { ToggleButtonGroup, ToggleButton } from "@mui/material";
+import { useSession } from "next-auth/react";
+
+export default function LogAnnotationToggle({
+ job,
+ annotation,
+ repo = null,
+ log_metadata,
+}: {
+ job: JobData;
+ annotation: LogAnnotation;
+ repo?: string | null;
+ log_metadata: Record;
+}) {
+ const [state, setState] = React.useState(
+ (annotation ?? "null") as LogAnnotation
+ );
+ const session = useSession();
+ async function handleChange(
+ _: React.MouseEvent,
+ newState: LogAnnotation
+ ) {
+ setState(newState);
+ const all_metadata = log_metadata;
+ all_metadata["job_id"] = job.id ?? "";
+ await fetch(`/api/log_annotation/${repo ?? job.repo}/${newState}`, {
+ method: "POST",
+ body: JSON.stringify(all_metadata),
+ });
+ }
+
+ return (
+ <>
+ Which log is preferable:{" "}
+
+ {Object.keys(LogAnnotation).map((annotation, ind) => {
+ return (
+
+ {
+ //@ts-ignore
+ LogAnnotation[annotation]
+ }
+
+ );
+ })}
+
+ >
+ );
+}
diff --git a/torchci/components/LogViewer.tsx b/torchci/components/LogViewer.tsx
index b5710996e7..9688cf35f9 100644
--- a/torchci/components/LogViewer.tsx
+++ b/torchci/components/LogViewer.tsx
@@ -17,9 +17,10 @@ import { parse } from "ansicolor";
import { oneDark } from "@codemirror/theme-one-dark";
import { isFailure } from "lib/JobClassifierUtil";
-import { JobData } from "lib/types";
+import { JobData, LogAnnotation } from "lib/types";
import { useEffect, useRef, useState } from "react";
import useSWRImmutable from "swr";
+import LogAnnotationToggle from "./LogAnnotationToggle";
const ESC_CHAR_REGEX = /\x1b\[[0-9;]*m/g;
// Based on the current editor view, produce a series of decorations that
@@ -190,7 +191,15 @@ function Log({ url, line }: { url: string; line: number | null }) {
return ;
}
-export default function LogViewer({ job }: { job: JobData }) {
+export default function LogViewer({
+ job,
+ logRating = LogAnnotation.NULL,
+ showAnnotationToggle = false,
+}: {
+ job: JobData;
+ logRating?: LogAnnotation;
+ showAnnotationToggle?: boolean;
+}) {
const [showLogViewer, setShowLogViewer] = useState(false);
useEffect(() => {
@@ -222,6 +231,15 @@ export default function LogViewer({ job }: { job: JobData }) {
{job.failureLine ?? "Show log"}
{showLogViewer && }
+ {showAnnotationToggle && (
+
+
+
+ )}
);
}
diff --git a/torchci/lib/types.ts b/torchci/lib/types.ts
index 2a431b240c..176a4d94ce 100644
--- a/torchci/lib/types.ts
+++ b/torchci/lib/types.ts
@@ -179,6 +179,14 @@ export enum JobAnnotation {
OTHER = "Other",
}
+export enum LogAnnotation {
+ NULL = "None",
+ PREFER_TOP_LOG = "Prefer Top Log",
+ PREFER_BOTTOM_LOG = "Prefer Bottom Log",
+ PREFER_NEITHER = "Prefer Neither",
+ SIMILAR_LOGS = "Similar Logs",
+}
+
export function packHudParams(input: any) {
return {
repoOwner: input.repoOwner as string,
diff --git a/torchci/pages/api/log_annotation/[repoOwner]/[repoName]/[annotation].ts b/torchci/pages/api/log_annotation/[repoOwner]/[repoName]/[annotation].ts
new file mode 100644
index 0000000000..5bfa5874a7
--- /dev/null
+++ b/torchci/pages/api/log_annotation/[repoOwner]/[repoName]/[annotation].ts
@@ -0,0 +1,60 @@
+import { NextApiRequest, NextApiResponse } from "next";
+import { getDynamoClient } from "lib/dynamo";
+import { getServerSession } from "next-auth";
+import { authOptions } from "pages/api/auth/[...nextauth]";
+import { getOctokit } from "lib/github";
+import { hasWritePermissionsUsingOctokit } from "lib/bot/utils";
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ if (req.method !== "POST") {
+ return res.status(504).end();
+ }
+ // @ts-ignore
+ const session = await getServerSession(req, res, authOptions);
+ if (session === undefined || session === null || session.user === undefined) {
+ return res.status(401).end();
+ }
+
+ const { repoOwner, repoName, annotation } = req.query;
+ const repoOwnerStr = Array.isArray(repoOwner) ? repoOwner[0] : repoOwner;
+ const repoNameStr = Array.isArray(repoName) ? repoName[0] : repoName;
+ const octokit = await getOctokit(repoOwnerStr, repoNameStr);
+ const user = await octokit.rest.users.getAuthenticated();
+ const hasPermission = hasWritePermissionsUsingOctokit(
+ octokit,
+ user.data.login,
+ repoOwnerStr,
+ repoNameStr
+ );
+ if (!hasPermission) {
+ return res.status(401).end();
+ }
+ const log_metadata = JSON.parse(req.body) ?? [];
+ const client = getDynamoClient();
+ const jobId = log_metadata[0].job_id;
+ const dynamoKey = `${repoOwner}/${repoName}/${jobId}`;
+
+ const item: any = {
+ dynamoKey,
+ repo: `${repoOwner}/${repoName}`,
+ jobID: parseInt(jobId as string),
+ };
+
+ // TODO: we encode annotations as a string, but probably we want to just
+ // serialize a JSON object instead to avoid this silly special case.
+ if (annotation !== "null") {
+ item["annotationDecision"] = annotation;
+ item["annotationTime"] = new Date().toISOString();
+ item["annotationAuthor"] = user.data.login;
+ item["annotationLogMetadata"] = log_metadata;
+ item["metricType"] = "log_annotation";
+ }
+
+ return client.put({
+ TableName: "torchci-job-annotation",
+ Item: item,
+ });
+}