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",
+ }}
+ />}
+
+
+
+ );
+}