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 Screenshot 2023-10-11 at 4 55 55 PM ### 🤖 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, + }); +}