diff --git a/src/components/Alert/index.tsx b/src/components/Alert/index.tsx new file mode 100644 index 0000000..bb8f86c --- /dev/null +++ b/src/components/Alert/index.tsx @@ -0,0 +1,29 @@ +import Snackbar from "@mui/material/Snackbar"; +import Alert from "@mui/material/Alert"; +import { useState } from "react"; + +type AlertProps = { + severity: "success" | "error", + message: string, +}; + +export default function AlertComponent(props: AlertProps) { + const [isOpen, setIsOpen] = useState(true); + const handleClose = ( + event?: React.SyntheticEvent | Event, + reason?: string + ) => { + if (reason === "clickaway") { + return; + } + setIsOpen(false); + }; + + return ( + + + {props.message} + + + ); +} \ No newline at end of file diff --git a/src/components/KillButton/index.tsx b/src/components/KillButton/index.tsx new file mode 100644 index 0000000..77479f2 --- /dev/null +++ b/src/components/KillButton/index.tsx @@ -0,0 +1,157 @@ +import { useState } from "react"; +import type { UseMutationResult, UseQueryResult } from "@tanstack/react-query"; +import Button from "@mui/material/Button"; +import Box from "@mui/material/Box"; +import CircularProgress from "@mui/material/CircularProgress"; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import Dialog from '@mui/material/Dialog'; +import Paper from "@mui/material/Paper"; +import Typography from "@mui/material/Typography"; +import Tooltip from '@mui/material/Tooltip'; + +import CodeBlock from "../CodeBlock"; +import type { Run as RunResponse } from "../../lib/paddles.d"; +import { KillRunPayload } from "../../lib/teuthologyAPI.d"; +import { useSession, useRunKill } from "../../lib/teuthologyAPI"; +import Alert from "../Alert"; + + +type KillButtonProps = { + query: UseQueryResult; +}; + +type KillButtonDialogProps = { + mutation: UseMutationResult; + payload: KillRunPayload; + open: boolean; + handleClose: () => void; +}; + +export default function KillButton({query: runQuery}: KillButtonProps) { + const killMutation = useRunKill(); + const [open, setOpen] = useState(false); + const sessionQuery = useSession(); + const data: RunResponse | undefined = runQuery.data; + const run_owner = data?.jobs[0].owner || ""; + const killPayload: KillRunPayload = { + "--run": data?.name || "", + "--owner": run_owner, + "--machine-type": data?.machine_type || "", + "--preserve-queue": true, + } + const loggedUser = sessionQuery.data?.session?.username; + const isUserAdmin = sessionQuery.data?.session?.isUserAdmin; + const owner = killPayload["--owner"].toLowerCase() + const isOwner = (loggedUser?.toLowerCase() == owner) || (`scheduled_${loggedUser?.toLowerCase()}@teuthology` == owner) + const isButtonDisabled = (!isOwner && !isUserAdmin) + + const getHelperMessage = () => { + if (isButtonDisabled) { + return `User (${loggedUser}) does not have admin privileges to kill runs owned by another user (${owner}). `; + } else { + if (!isOwner && isUserAdmin) return `Use admin privileges to kill run owned by '${owner}'. `; + return "Terminate all jobs in this run"; + } + } + + const toggleDialog = () => { + setOpen(!open); + }; + + const refreshAndtoggle = () => { + if (open && !killMutation.isIdle) { // on closing confirmation dialog after kill-run + runQuery.refetch(); + } + toggleDialog(); + killMutation.reset(); + } + + if ((data?.status.includes("finished")) || !(sessionQuery.data?.session?.username)) { + // run finished or user logged out + return null; + } + + + return ( + + + + + + {(isOwner) ? "Kill Run" : "Kill Run As Admin"} + + + + + + { (killMutation.isError) ? : null } + { (killMutation.isSuccess) ? : null } + + ); +}; + +function KillButtonDialog({mutation, open, handleClose, payload}: KillButtonDialogProps) { + return ( + + + KILL CONFIRMATION + + { (mutation.isSuccess && mutation.data ) ? + + + Successful! + + + + + : + (mutation.isLoading) ? ( + + + + Killing run... + + + ) : + (mutation.isError) ? ( + + + Failed! + + + + + + ) : + + + Are you sure you want to kill this run/job? + + mutation.mutate(payload)} + > + Yes, I'm sure + + + } + + + + ) +} diff --git a/src/components/Login/index.jsx b/src/components/Login/index.jsx index 1fcf82f..fe07671 100644 --- a/src/components/Login/index.jsx +++ b/src/components/Login/index.jsx @@ -5,19 +5,18 @@ import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import GitHubIcon from '@mui/icons-material/GitHub'; -import { doLogin, doLogout, useSession, useUserData } from "../../lib/teuthologyAPI"; +import { doLogin, doLogout, useSession } from "../../lib/teuthologyAPI"; export default function Login() { const sessionQuery = useSession(); - const userData = useUserData(); - const [anchorEl, setAnchorEl] = useState(null); - const open = Boolean(anchorEl); + const [dropMenuAnchor, setDropMenuAnchor] = useState(null); + const open = Boolean(dropMenuAnchor); const handleClick = (event) => { - setAnchorEl(event.currentTarget); + setDropMenuAnchor(event.currentTarget); }; const handleClose = () => { - setAnchorEl(null); + setDropMenuAnchor(null); }; if ( ! sessionQuery.isSuccess ) return null; @@ -27,26 +26,21 @@ export default function Login() { {sessionQuery.data?.session ? Logout : } disabled={sessionQuery.isError} diff --git a/src/lib/paddles.d.ts b/src/lib/paddles.d.ts index 1e3b1f3..6f6f9f8 100644 --- a/src/lib/paddles.d.ts +++ b/src/lib/paddles.d.ts @@ -48,6 +48,7 @@ export type Job = { roles: NodeRoles[]; os_type: string; os_version: string; + owner: string; }; export type NodeRoles = string[]; @@ -78,6 +79,7 @@ export type Run = { results: RunResults; machine_type: string; status: RunStatus; + user: string; }; export type Node = { diff --git a/src/lib/teuthologyAPI.d.ts b/src/lib/teuthologyAPI.d.ts new file mode 100644 index 0000000..5843b7e --- /dev/null +++ b/src/lib/teuthologyAPI.d.ts @@ -0,0 +1,15 @@ + +export type Session = { + session: { + id: int, + username: string, + isUserAdmin: boolean, + } +} + +export type KillRunPayload = { + "--run": string, + "--owner": string, + "--machine-type": string, + "--preserve-queue": boolean, +} diff --git a/src/lib/teuthologyAPI.ts b/src/lib/teuthologyAPI.ts index 41f6651..edde052 100644 --- a/src/lib/teuthologyAPI.ts +++ b/src/lib/teuthologyAPI.ts @@ -1,7 +1,8 @@ import axios from "axios"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import type { UseQueryResult, UseMutationResult } from "@tanstack/react-query"; import { Cookies } from "react-cookie"; -import type { UseQueryResult } from "@tanstack/react-query"; +import { Session } from "./teuthologyAPI.d" const TEUTHOLOGY_API_SERVER = import.meta.env.VITE_TEUTHOLOGY_API || ""; @@ -25,9 +26,9 @@ function doLogout() { window.location.href = url; } -function useSession(): UseQueryResult { +function useSession(): UseQueryResult { const url = getURL("/"); - const query = useQuery({ + const query = useQuery({ queryKey: ['ping-api', { url }], queryFn: () => ( axios.get(url, { @@ -56,9 +57,24 @@ function useUserData(): Map { return new Map(); } +function useRunKill(): UseMutationResult { + const url = getURL("/kill/?logs=true"); + const mutation: UseMutationResult = useMutation({ + mutationKey: ['run-kill', { url }], + mutationFn: (payload) => ( + axios.post(url, payload, { + withCredentials: true + }) + ), + retry: 0, + }); + return mutation; +} + export { doLogin, doLogout, useSession, - useUserData + useUserData, + useRunKill, } diff --git a/src/pages/Run/index.tsx b/src/pages/Run/index.tsx index a71382c..4827669 100644 --- a/src/pages/Run/index.tsx +++ b/src/pages/Run/index.tsx @@ -11,6 +11,7 @@ import type { Run as Run_, RunParams } from "../../lib/paddles.d"; import { useRun } from "../../lib/paddles"; import JobList from "../../components/JobList"; import Link from "../../components/Link"; +import KillButton from "../../components/KillButton"; const PREFIX = "index"; @@ -72,6 +73,7 @@ export default function Run() { date + );