Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pages/Run/index.tsx: Add kill run button #51

Merged
merged 15 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/components/Alert/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Snackbar autoHideDuration={3000} open={isOpen} onClose={handleClose}>
<Alert onClose={handleClose} severity={props.severity} sx={{ width: "100%" }}>
{props.message}
</Alert>
</Snackbar>
);
}
157 changes: 157 additions & 0 deletions src/components/KillButton/index.tsx
Original file line number Diff line number Diff line change
@@ -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<RunResponse>;
};

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 (
<div>
<div style={{ display: "flex" }}>
<Tooltip arrow title={getHelperMessage()}>
<span>
<Button
variant="contained"
color="error"
size="large"
onClick={refreshAndtoggle}
disabled={isButtonDisabled}
sx={{ marginBottom: "12px" }}
>
{(isOwner) ? "Kill Run" : "Kill Run As Admin"}
</Button>
</span>
</Tooltip>
<KillButtonDialog
mutation={killMutation}
payload={killPayload}
open={open}
handleClose={refreshAndtoggle}
/>
</div>
{ (killMutation.isError) ? <Alert severity="error" message="Unable to kill run" /> : null }
{ (killMutation.isSuccess) ? <Alert severity="success" message={`Run killed successfully! \n`} /> : null }
</div>
);
};

function KillButtonDialog({mutation, open, handleClose, payload}: KillButtonDialogProps) {
return (
<div>
<Dialog onClose={handleClose} open={open} scroll="paper" fullWidth={true} maxWidth="md">
<DialogTitle variant="h6">KILL CONFIRMATION</DialogTitle>
<DialogContent dividers>
{ (mutation.isSuccess && mutation.data ) ?
<div>
<Typography variant="h6" display="block" color="green" gutterBottom>
Successful!
</Typography>
<Paper>
<CodeBlock value={mutation.data?.data?.logs || ""} language="python" />
</Paper>
</div> :
(mutation.isLoading) ? (
<div style={{display: "flex", alignItems: "center"}}>
<CircularProgress size={20} color="inherit" style={{marginRight: "12px"}} />
<Typography variant="subtitle1" display="block">
Killing run...
</Typography>
</div>
) :
(mutation.isError) ? (
<div>
<Typography variant="h6" display="block" color="red" gutterBottom>
Failed!
</Typography>
<Paper>
<CodeBlock value={(mutation.error?.response?.data?.detail) || ""} language="python" />
</Paper>
</div>
) :
<div>
<Typography variant="overline" display="block" gutterBottom>
Are you sure you want to kill this run/job?
</Typography>
<Button
variant="contained"
color="error"
size="large"
onClick={() => mutation.mutate(payload)}
>
Yes, I'm sure
</Button>
</div>
}
</DialogContent>
</Dialog>
</div>
)
}
24 changes: 9 additions & 15 deletions src/components/Login/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,26 +26,21 @@ export default function Login() {
{sessionQuery.data?.session
? <div>
<Avatar
alt={userData.get("username") || ""}
src={userData.get("avatar_url") || ""}
alt={sessionQuery.data?.session?.username || ""}
src={sessionQuery.data?.session?.avatar_url || ""}
onClick={handleClick}
aria-controls={open ? 'basic-menu' : undefined}
aria-expanded={open ? 'true' : undefined}
/>
<Menu
id="basic-menu"
anchorEl={anchorEl}
anchorEl={dropMenuAnchor}
open={open}
onClose={handleClose}
MenuListProps={{
'aria-labelledby': 'basic-button',
}}
>
<MenuItem onClick={doLogout}>Logout</MenuItem>
</Menu>
</div>
: <Button
variant="contained"
color="success"
onClick={doLogin}
startIcon={<GitHubIcon fontSize="small" /> }
disabled={sessionQuery.isError}
Expand Down
2 changes: 2 additions & 0 deletions src/lib/paddles.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type Job = {
roles: NodeRoles[];
os_type: string;
os_version: string;
owner: string;
};

export type NodeRoles = string[];
Expand Down Expand Up @@ -76,6 +77,7 @@ export type Run = {
results: RunResults;
machine_type: string;
status: RunStatus;
user: string;
};

export type Node = {
Expand Down
15 changes: 15 additions & 0 deletions src/lib/teuthologyAPI.d.ts
Original file line number Diff line number Diff line change
@@ -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,
}
26 changes: 21 additions & 5 deletions src/lib/teuthologyAPI.ts
Original file line number Diff line number Diff line change
@@ -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 || "";
Expand All @@ -25,9 +26,9 @@ function doLogout() {
window.location.href = url;
}

function useSession(): UseQueryResult {
function useSession(): UseQueryResult<Session> {
const url = getURL("/");
const query = useQuery({
const query = useQuery<Session, Error>({
queryKey: ['ping-api', { url }],
queryFn: () => (
axios.get(url, {
Expand Down Expand Up @@ -56,9 +57,24 @@ function useUserData(): Map<string, string> {
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,
}
2 changes: 2 additions & 0 deletions src/pages/Run/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -72,6 +73,7 @@ export default function Run() {
date
</FilterLink>
</div>
<KillButton query={query} />
<JobList query={query} params={params} setter={setParams} />
</Root>
);
Expand Down
Loading