diff --git a/src/App.tsx b/src/App.tsx index af26dd9..60e2c0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,7 @@ import Nodes from "./pages/Nodes"; import Node from "./pages/Node"; import StatsNodesLock from "./pages/StatsNodesLock"; import StatsNodesJobs from "./pages/StatsNodesJobs"; -import { ErrorBoundary } from "react-error-boundary"; +import Schedule from "./pages/Schedule"; import "./App.css"; import ErrorFallback from "./components/ErrorFallback"; @@ -39,21 +39,20 @@ function App(props: AppProps) {
- - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> +
); diff --git a/src/components/Drawer/index.jsx b/src/components/Drawer/index.jsx index 1b971fb..0339a51 100644 --- a/src/components/Drawer/index.jsx +++ b/src/components/Drawer/index.jsx @@ -77,7 +77,6 @@ export default function Drawer(props) { Runs - Nodes @@ -87,7 +86,9 @@ export default function Drawer(props) { Node Jobs Stats - + + Schedule + ); } diff --git a/src/lib/teuthologyAPI.ts b/src/lib/teuthologyAPI.ts index 41f6651..6ca8c74 100644 --- a/src/lib/teuthologyAPI.ts +++ b/src/lib/teuthologyAPI.ts @@ -3,28 +3,46 @@ import { useQuery } from "@tanstack/react-query"; import { Cookies } from "react-cookie"; import type { UseQueryResult } from "@tanstack/react-query"; -const TEUTHOLOGY_API_SERVER = +const TEUTHOLOGY_API_SERVER = import.meta.env.VITE_TEUTHOLOGY_API || ""; const GH_USER_COOKIE = "GH_USER"; -function getURL(relativeURL: URL|string): string { - if ( ! TEUTHOLOGY_API_SERVER ) return ""; +function getURL(relativeURL: URL | string): string { + if (!TEUTHOLOGY_API_SERVER) return ""; return new URL(relativeURL, TEUTHOLOGY_API_SERVER).toString(); } function doLogin() { const url = getURL("/login/"); - if ( url ) window.location.href = url; + if (url) window.location.href = url; } function doLogout() { const cookies = new Cookies(); cookies.remove(GH_USER_COOKIE); - + const url = getURL("/logout/"); window.location.href = url; } +async function useSchedule(commandValue: any) { + const url = getURL("/suite?logs=true"); + const username = useUserData().get("username"); + if (username) { + commandValue['--owner'] = username; + } + return await axios.post(url, commandValue, { + withCredentials: true, + headers: { "Content-Type": "application/json" }, + }).then((resp) => { + console.log(resp); + return resp; + }, (error) => { + console.log(error); + throw error; + }); +} + function useSession(): UseQueryResult { const url = getURL("/"); const query = useQuery({ @@ -59,6 +77,7 @@ function useUserData(): Map { export { doLogin, doLogout, + useSchedule, useSession, - useUserData + useUserData, } diff --git a/src/pages/Schedule/index.jsx b/src/pages/Schedule/index.jsx new file mode 100644 index 0000000..b055dfb --- /dev/null +++ b/src/pages/Schedule/index.jsx @@ -0,0 +1,502 @@ +import { useEffect, useState } from 'react'; +import { Helmet } from "react-helmet"; +import { useLocalStorage } from "usehooks-ts"; +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import Fab from '@mui/material/Fab'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from "@mui/icons-material/Delete"; +import LockIcon from "@mui/icons-material/Lock"; +import LockOpenIcon from "@mui/icons-material/LockOpen"; +import Paper from '@mui/material/Paper'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import TableCell from '@mui/material/TableCell'; +import TableBody from '@mui/material/TableBody'; +import Table from '@mui/material/Table'; +import TextField from '@mui/material/TextField'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import Checkbox from '@mui/material/Checkbox'; +import Tooltip from '@mui/material/Tooltip'; +import InfoIcon from '@mui/icons-material/Info'; +import Alert from '@mui/material/Alert'; +import Snackbar from '@mui/material/Snackbar'; +import CircularProgress from '@mui/material/CircularProgress'; +import { useUserData, useSchedule } from '../../lib/teuthologyAPI'; +import { useMutation } from "@tanstack/react-query"; +import Editor from "react-simple-code-editor"; +import { highlight, languages } from "prismjs/components/prism-core"; +import Accordion from "@mui/material/Accordion"; +import AccordionSummary from "@mui/material/AccordionSummary"; +import AccordionDetails from "@mui/material/AccordionDetails"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +export default function Schedule() { + const keyOptions = + [ + "--ceph", + "--ceph-repo", + "--suite-repo", + "--suite-branch", + "--suite", + "--subset", + "--sha1", + "--email", + "--machine-type", + "--filter", + "--filter-out", + "--filter-all", + "--kernal", + "--flavor", + "--distro", + "--distro-version", + "--newest", + "--num", + "--limit", + "--priority", + ]; + const OptionsInfo = { + "--ceph": "The ceph branch to run against [default: main]", + "--ceph-repo": "Query this repository for Ceph branch and \ + SHA1 values [default: https://github.com/ceph/ceph-ci.git]", + "--suite-repo": "Use tasks and suite definition in this \ + repository [default: https://github.com/ceph/ceph-ci.git]", + "--suite-branch": "Use this suite branch instead of the ceph branch", + "--suite": "The suite to schedule", + "--subset": "Instead of scheduling the entire suite, break the \ + set of jobs into pieces (each of which will \ + contain each facet at least once) and schedule \ + piece . Scheduling 0/, 1/, \ + 2/ ... -1/ will schedule all \ + jobs in the suite (many more than once). If specified, \ + this value can be found in results.log.", + "--sha1": "The ceph sha1 to run against (overrides -c) \ + If both -S and -c are supplied, -S wins, and \ + there is no validation that sha1 is contained \ + in branch", + "--email": "When tests finish or time out, send an email \ + here. May also be specified in ~/.teuthology.yaml \ + as 'results_email'", + "--machine-type": "Machine type e.g., smithi, mira, gibba.", + "--filter": "Only run jobs whose description contains at least one \ + of the keywords in the comma separated keyword string specified.", + "--filter-out": "Do not run jobs whose description contains any of \ + the keywords in the comma separated keyword \ + string specified.", + "--filter-all": "Only run jobs whose description contains each one \ + of the keywords in the comma separated keyword \ + string specified.", + "--kernal": "The kernel branch to run against, \ + use 'none' to bypass kernel task. \ + [default: distro]", + "--flavor": "The ceph packages shaman flavor to run with: \ + ('default', 'crimson', 'notcmalloc', 'jaeger') \ + [default: default]", + "--distro": "Distribution to run against", + "--distro-version": "Distro version to run against", + "--newest": "Search for the newest revision built on all \ + required distro/versions, starting from \ + either --ceph or --sha1, backtracking \ + up to commits [default: 0]", + "--num": "Number of times to run/queue the job [default: 1]", + "--limit": "Queue at most this many jobs [default: 0]", + "--priority": "Job priority (lower is sooner) 0 - 1000", + } + + const [rowData, setRowData] = useLocalStorage("rowData", []); + const [rowIndex, setRowIndex] = useLocalStorage("rowIndex", -1); + const [commandBarValue, setCommandBarValue] = useState([]); + const username = useUserData().get("username"); + + const [open, setOpenSuccess] = useState(false); + const [openWrn, setOpenWrn] = useState(false); + const [openErr, setOpenErr] = useState(false); + const [logText, setLogText] = useLocalStorage("logText", ""); + + const handleOpenSuccess = (data) => { + if (data && data.data) { + const code = data.data.logs.join(""); + setLogText(code); + } + + if (data.data.job_count < 1) { + setOpenWrn(true) + } else { + setOpenSuccess(true); + } + }; + const handleOpenErr = (data) => { + console.log("handleOpenErr"); + if (data && data.response.data.detail) { + const code = data.response.data.detail; + setLogText(code); + } + setOpenErr(true); + }; + + const handleCloseSuccess = () => { + setOpenSuccess(false); + }; + const handleCloseErr = () => { + setOpenErr(false); + }; + const handleCloseWrn = () => { + setOpenWrn(false); + }; + const clickRun = useMutation({ + mutationFn: async (commandValue) => { + return await useSchedule(commandValue); + }, + onSuccess: (data) => { + handleOpenSuccess(data); + }, + onError: (err) => { + console.log(err); + handleOpenErr(err); + } + }) + + const clickDryRun = useMutation({ + mutationFn: async (commandValue) => { + return await useSchedule(commandValue); + }, + onSuccess: (data) => { + handleOpenSuccess(data); + }, + onError: (err) => { + handleOpenErr(err); + } + }) + + const clickForcePriority = useMutation({ + mutationFn: async (commandValue) => { + commandValue['--force-priority'] = true; + return await useSchedule(commandValue); + }, + onSuccess: (data) => { + handleOpenSuccess(data); + }, + onError: (err) => { + handleOpenErr(err); + } + }) + + useEffect(() => { + setCommandBarValue(rowData); + }, [rowData]) + + function getCommandValue(dry_run) { + setLogText(""); + let retCommandValue = {}; + commandBarValue.map((data) => { + if (data.checked) { + retCommandValue[data.key] = data.value; + } + }) + if (!username) { + console.log("User is not logged in"); + return {}; + } else { + retCommandValue['--user'] = username; + } + if (dry_run) { + retCommandValue['--dry-run'] = true; + } else { + retCommandValue['--dry-run'] = false; + } + return retCommandValue; + } + + const addNewRow = () => { + console.log("addNewRow"); + const updatedRowIndex = rowIndex + 1; + setRowIndex(updatedRowIndex); + const index = (updatedRowIndex % keyOptions.length); + const object = { + key: keyOptions[index], + value: "", + lock: false, + checked: true, + } + const updatedRowData = [...rowData]; + updatedRowData.push(object); + setRowData(updatedRowData); + }; + + const handleCheckboxChange = (index, event) => { + console.log("handleCheckboxChange"); + const newRowData = [...rowData]; + if (event.target.checked) { + newRowData[index].checked = true; + } else { + newRowData[index].checked = false; + } + setRowData(newRowData); + }; + + const handleKeySelectChange = (index, event) => { + console.log("handleKeySelectChange"); + const newRowData = [...rowData]; + newRowData[index].key = event.target.value; + setRowData(newRowData); + }; + + const handleValueChange = (index, event) => { + console.log("handleValueChange"); + const newRowData = [...rowData]; + newRowData[index].value = event.target.value; + setRowData(newRowData); + }; + + const handleDeleteRow = (index) => { + console.log("handleDeleteRow"); + let newRowData = [...rowData]; + newRowData.splice(index, 1) + setRowData(newRowData); + const updatedRowIndex = rowIndex - 1; + setRowIndex(updatedRowIndex); + }; + + const toggleRowLock = (index) => { + console.log("toggleRowLock"); + const newRowData = [...rowData]; + newRowData[index].lock = !newRowData[index].lock; + setRowData(newRowData); + }; + + return ( +
+ {username ? <> : User is not logged in ... feature disabled.} + + Schedule - Pulpito + + + Schedule a run + +
+ + { + if (data.checked) { + return `${data.key} ${data.value}`; + } + }) + .join(" ")}`} + placeholder="teuthology-suite" + disabled={true} + /> + +
+ + + Schedule Success! + + + + + Schedule Failed! + + + + + Warning! 0 Jobs Scheduled + + + + {clickRun.isLoading ? ( + + ) : } + + + {clickForcePriority.isLoading ? ( + + ) : + } + + + {clickDryRun.isLoading ? : ()} + +
+
+
+ + + + + + Key + Value + + + + { + {rowData.map((data, index) => ( + + + handleCheckboxChange(index, event)} /> + + +
+ + + + +
+
+ + handleValueChange(index, event)} + disabled={data.lock} + /> + + +
+ +
toggleRowLock(index)} + > + {data.lock ? : } +
+
+ +
{ + handleDeleteRow(index); + }} + > + +
+
+
+
+
+ ))} +
} +
+
+
+ + + + + + + + }> + Logs + + + {clickDryRun.isLoading | clickRun.isLoading | clickForcePriority.isLoading ? : highlight(logText, languages.yaml)} + style={{ + fontFamily: [ + "ui-monospace", + "SFMono-Regular", + '"SF Mono"', + "Menlo", + "Consolas", + "Liberation Mono", + '"Lucida Console"', + "Courier", + "monospace", + ].join(","), + textAlign: "initial", + }} + />} + + +
+ ); +}