Job Menu available on pipeline display
This commit is contained in:
parent
bb934ee859
commit
fd4dba7140
13 changed files with 460 additions and 70 deletions
|
@ -4,14 +4,14 @@ import JobContext from "@qltr/jobctx";
|
||||||
import Initiator from "@qltr/initiator";
|
import Initiator from "@qltr/initiator";
|
||||||
import { useOneshotCore } from "./OneshotCore.jsx";
|
import { useOneshotCore } from "./OneshotCore.jsx";
|
||||||
import { usePipelineCore } from "./PipelineCore.jsx";
|
import { usePipelineCore } from "./PipelineCore.jsx";
|
||||||
import { useJobExtra } from "./JobExtra.jsx";
|
import { useJobExtras } from "./JobExtras.jsx";
|
||||||
import { jobStatus, socketUrl } from "./job-config.js";
|
import { jobStatus, socketUrl } from "./job-config.js";
|
||||||
|
|
||||||
export function useJobCore() {
|
export function useJobCore() {
|
||||||
const { state, jobUpdate, jobCreate, jobDelete } = useContext(JobContext);
|
const { state, jobUpdate, jobCreate, jobDelete } = useContext(JobContext);
|
||||||
const { pipelineStart, pipelineCancel, pipelineDestroy } = usePipelineCore();
|
const { pipelineStart, pipelineCancel, pipelineDestroy } = usePipelineCore();
|
||||||
const { oneshotStart, oneshotCancel, oneshotDestroy } = useOneshotCore();
|
const { oneshotStart, oneshotCancel, oneshotDestroy } = useOneshotCore();
|
||||||
const jobExtra = useJobExtra();
|
const jobExtras = useJobExtras();
|
||||||
|
|
||||||
function retryAll(failing) {
|
function retryAll(failing) {
|
||||||
console.log("Would retry all failing tests!");
|
console.log("Would retry all failing tests!");
|
||||||
|
@ -37,7 +37,7 @@ export function useJobCore() {
|
||||||
pipelineDestroy,
|
pipelineDestroy,
|
||||||
pipelineStart,
|
pipelineStart,
|
||||||
// Job Extra
|
// Job Extra
|
||||||
...jobExtra,
|
...jobExtras,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ function statusIcon(status) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useJobExtra() {
|
export function useJobExtras() {
|
||||||
const { state, jobUpdate, jobCreate, jobDelete } = useContext(JobContext);
|
const { state, jobUpdate, jobCreate, jobDelete } = useContext(JobContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
import moment from "moment";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import JobContext from "@qltr/jobctx";
|
import JobContext from "@qltr/jobctx";
|
||||||
import Initiator from "@qltr/initiator";
|
import Initiator from "@qltr/initiator";
|
||||||
|
@ -35,30 +36,7 @@ export function usePipelineCore() {
|
||||||
jobUpdate({ ...job }, jobId);
|
jobUpdate({ ...job }, jobId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPipelineTrigger = (p) => {
|
const onPipelineTrigger = applyOnPipelineTrigger(pl, plReq);
|
||||||
const { triggers } = p;
|
|
||||||
for (const t in triggers) {
|
|
||||||
if (t === "__testDelay") continue;
|
|
||||||
const delay = triggers[t].__testDelay ?? 0;
|
|
||||||
delete triggers[t].__testDelay;
|
|
||||||
const plTrigger = { ...p, triggers: triggers[t], __test: t };
|
|
||||||
const jobReq = { ...plReq, pipeline: plTrigger };
|
|
||||||
const triggerAt = Date.now() + delay;
|
|
||||||
const testName = t;
|
|
||||||
function removeTrigger() {
|
|
||||||
const i = pl.pendingTriggers.findIndex((pt) => pt.testName === t);
|
|
||||||
if (i < 0) return;
|
|
||||||
pl.pendingTriggers.splice(i, 1);
|
|
||||||
}
|
|
||||||
function launchTrigger() {
|
|
||||||
pipelineJob(pl, jobReq);
|
|
||||||
removeTrigger();
|
|
||||||
}
|
|
||||||
const doTrigger = { removeTrigger, launchTrigger };
|
|
||||||
const timer = setTimeout(launchTrigger, delay);
|
|
||||||
pl.pendingTriggers.push({ testName, timer, triggerAt, ...doTrigger });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const started = initiator.newPipelineJob(
|
const started = initiator.newPipelineJob(
|
||||||
plReq,
|
plReq,
|
||||||
onLog,
|
onLog,
|
||||||
|
@ -117,5 +95,34 @@ export function usePipelineCore() {
|
||||||
state.pipelines.splice(pipelineIndex, 1);
|
state.pipelines.splice(pipelineIndex, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyOnPipelineTrigger(pl, plReq) {
|
||||||
|
return function onPipelineTrigger(p) {
|
||||||
|
const { triggers } = p;
|
||||||
|
for (const t in triggers) {
|
||||||
|
if (t === "__testDelay") continue;
|
||||||
|
const delay = triggers[t].__testDelay ?? 0;
|
||||||
|
delete triggers[t].__testDelay;
|
||||||
|
const plTrigger = { ...p, triggers: triggers[t], __test: t };
|
||||||
|
const jobReq = { ...plReq, pipeline: plTrigger };
|
||||||
|
const triggerQueued = moment();
|
||||||
|
const triggerAt = moment().add(delay, "ms");
|
||||||
|
const testName = t;
|
||||||
|
function removeTrigger() {
|
||||||
|
const i = pl.pendingTriggers.findIndex((pt) => pt.testName === t);
|
||||||
|
if (i < 0) return;
|
||||||
|
pl.pendingTriggers.splice(i, 1);
|
||||||
|
}
|
||||||
|
function launchTrigger() {
|
||||||
|
pipelineJob(pl, jobReq);
|
||||||
|
removeTrigger();
|
||||||
|
}
|
||||||
|
const timer = setTimeout(launchTrigger, delay);
|
||||||
|
const triggerTimings = { timer, triggerQueued, triggerAt };
|
||||||
|
const doTrigger = { removeTrigger, launchTrigger, jobReq };
|
||||||
|
pl.pendingTriggers.push({ testName, ...triggerTimings, ...doTrigger });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return { pipelineStart, pipelineCancel, pipelineDestroy };
|
return { pipelineStart, pipelineCancel, pipelineDestroy };
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,23 +17,23 @@ export default function CatalogItemDetails(props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Typography component={"div"} style={{ wordBreak: "break-word" }}>
|
<Typography component={"div"} style={{ wordBreak: "break-word" }}>
|
||||||
<div sx={{ display: "inline-flex" }}>
|
<div>
|
||||||
<span style={{ fontWeight: "bold" }}>Image: </span>
|
<span style={{ fontWeight: "bold" }}>Image: </span>
|
||||||
{image}
|
{image}
|
||||||
</div>
|
</div>
|
||||||
<div sx={{ display: "inline-flex" }}>
|
<div>
|
||||||
<span style={{ fontWeight: "bold" }}>Env: </span>
|
<span style={{ fontWeight: "bold" }}>Env: </span>
|
||||||
{env.join(", ")}
|
{env.join(", ")}
|
||||||
</div>
|
</div>
|
||||||
<div sx={{ display: "inline-flex" }}>
|
<div>
|
||||||
<span style={{ fontWeight: "bold" }}>Regions: </span>
|
<span style={{ fontWeight: "bold" }}>Regions: </span>
|
||||||
{regions.join(", ")}
|
{(regions ?? []).join(", ")}
|
||||||
</div>
|
</div>
|
||||||
<div sx={{ display: "inline-flex" }}>
|
<div>
|
||||||
<span style={{ fontWeight: "bold" }}>Crons: </span>
|
<span style={{ fontWeight: "bold" }}>Crons: </span>
|
||||||
{JSON.stringify(crons)}
|
{(crons ?? []).join(", ")}
|
||||||
</div>
|
</div>
|
||||||
<div sx={{ display: "inline-flex" }}>
|
<div>
|
||||||
<span style={{ fontWeight: "bold" }}>Projects: </span>
|
<span style={{ fontWeight: "bold" }}>Projects: </span>
|
||||||
{projects.join(", ")}
|
{projects.join(", ")}
|
||||||
</div>
|
</div>
|
||||||
|
|
128
src/views/jobs/JobActionMenu.jsx
Normal file
128
src/views/jobs/JobActionMenu.jsx
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import React, { useContext, useState, useEffect } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useJobCore, jobStatus } from "@qltr/jobcore";
|
||||||
|
import StoreContext from "@qltr/store";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import AppBar from "@mui/material/AppBar";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Menu from "@mui/material/Menu";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
|
||||||
|
import JobLogView from "./JobLogView.jsx";
|
||||||
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
|
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||||
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
|
import ReplayIcon from "@mui/icons-material/Replay";
|
||||||
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
|
import DoNotDisturbIcon from "@mui/icons-material/DoNotDisturb";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
|
import ViewColumnIcon from "@mui/icons-material/ViewColumn";
|
||||||
|
|
||||||
|
export function useJobActionMenu(baseAnchor = null) {
|
||||||
|
const [menuJob, setMenuJob] = useState({});
|
||||||
|
const [anchorEl, setAnchorEl] = useState(baseAnchor);
|
||||||
|
const menuOpen = (event) => setAnchorEl(event.currentTarget);
|
||||||
|
const menuClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
setMenuJob(null);
|
||||||
|
};
|
||||||
|
return [anchorEl, menuJob, setMenuJob, menuOpen, menuClose];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function JobActionMenu(props) {
|
||||||
|
const { job, anchorEl, menuClose } = props;
|
||||||
|
const { jobCompose, oneshotCancel, oneshotDestroy, toPipeline, toJob } =
|
||||||
|
useJobCore();
|
||||||
|
const { state: store } = useContext(StoreContext);
|
||||||
|
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
|
function download(filename, text) {
|
||||||
|
var element = document.createElement("a");
|
||||||
|
element.setAttribute(
|
||||||
|
"href",
|
||||||
|
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
|
||||||
|
);
|
||||||
|
element.setAttribute("download", filename);
|
||||||
|
element.style.display = "none";
|
||||||
|
document.body.appendChild(element);
|
||||||
|
element.click();
|
||||||
|
document.body.removeChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
function retryJob() {
|
||||||
|
const jobId = jobCompose(job.builderCache);
|
||||||
|
if (store.focusJob) toJob(jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadLog() {
|
||||||
|
if (job.status === jobStatus.PENDING) return;
|
||||||
|
download(`${job.jobId}.txt`, job.log.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelJob() {
|
||||||
|
oneshotCancel(job.jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteJob() {
|
||||||
|
oneshotDestroy(job.jobId);
|
||||||
|
navigateToJobs();
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuSelect = (cb) => () => {
|
||||||
|
menuClose();
|
||||||
|
cb();
|
||||||
|
};
|
||||||
|
|
||||||
|
function navigateToJobs() {
|
||||||
|
if (job.isPipeline) return toPipeline(job.pipelineId);
|
||||||
|
toJobs();
|
||||||
|
}
|
||||||
|
if (!job) return;
|
||||||
|
return (
|
||||||
|
<Menu anchorEl={anchorEl} open={open} onClose={menuClose}>
|
||||||
|
<MenuItem onClick={menuSelect(downloadLog)}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<DownloadIcon fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>Download Log</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
{!job.isPipeline && (
|
||||||
|
<MenuItem onClick={menuSelect(retryJob)}>
|
||||||
|
<ListItemIcon>
|
||||||
|
{job.status === jobStatus.OK || job.status === jobStatus.ERROR ? (
|
||||||
|
<ReplayIcon fontSize="small" />
|
||||||
|
) : (
|
||||||
|
<PlayArrowIcon fontSize="small" />
|
||||||
|
)}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>
|
||||||
|
{job.status === jobStatus.ERROR ? "Retry" : "Duplicate"}
|
||||||
|
</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{job.status === jobStatus.OK ||
|
||||||
|
job.status === jobStatus.ERROR ||
|
||||||
|
job.status === jobStatus.CANCELED ? null : (
|
||||||
|
<MenuItem onClick={menuSelect(cancelJob)}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<DoNotDisturbIcon fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>Cancel</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{!job.isPipeline && (
|
||||||
|
<MenuItem onClick={menuSelect(deleteJob)}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<DeleteIcon fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>Delete</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
|
@ -6,8 +6,8 @@ import AccordionSummary from "@mui/material/AccordionSummary";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
|
import JobActionMenu from "./JobActionMenu";
|
||||||
|
|
||||||
export default function JobBox(props) {
|
export default function JobBox(props) {
|
||||||
const { jobIcon } = useJobCore();
|
const { jobIcon } = useJobCore();
|
||||||
|
@ -16,6 +16,7 @@ export default function JobBox(props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion expanded={false} disableGutters={true} square>
|
<Accordion expanded={false} disableGutters={true} square>
|
||||||
|
<JobActionMenu job={job} />
|
||||||
<AccordionSummary
|
<AccordionSummary
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "rgba(0, 0, 0, .03)",
|
backgroundColor: "rgba(0, 0, 0, .03)",
|
||||||
|
|
|
@ -105,7 +105,7 @@ export default function JobView(props) {
|
||||||
{job.jobId}
|
{job.jobId}
|
||||||
</Typography>
|
</Typography>
|
||||||
{job.isPipeline && (
|
{job.isPipeline && (
|
||||||
<IconButton>
|
<IconButton onClick={navigateToJobs}>
|
||||||
<ViewColumnIcon />
|
<ViewColumnIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -3,9 +3,9 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import JobContext from "@qltr/jobctx";
|
import JobContext from "@qltr/jobctx";
|
||||||
import JobBox from "./JobBox.jsx";
|
import JobBox from "./JobBox.jsx";
|
||||||
import JobPipelineBox from "./JobPipelineBox.jsx";
|
import JobPipelineBox from "./pipeline/JobPipelineBox.jsx";
|
||||||
import JobView from "./JobView.jsx";
|
import JobView from "./JobView.jsx";
|
||||||
import JobPipelineDisplay from "./JobPipelineDisplay.jsx";
|
import JobPipelineDisplay from "./pipeline/JobPipelineDisplay.jsx";
|
||||||
import JobBuilder from "./builder/JobBuilder.jsx";
|
import JobBuilder from "./builder/JobBuilder.jsx";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
|
|
|
@ -1,21 +1,10 @@
|
||||||
import React, { useState, useContext } from "react";
|
import React from "react";
|
||||||
import StoreContext from "@qltr/store";
|
import { useJobCore } from "@qltr/jobcore";
|
||||||
import { useJobCore, jobStatus } from "@qltr/jobcore";
|
|
||||||
|
|
||||||
import Accordion from "@mui/material/Accordion";
|
import Accordion from "@mui/material/Accordion";
|
||||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
|
||||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import CheckIcon from "@mui/icons-material/Check";
|
|
||||||
import ClearIcon from "@mui/icons-material/Clear";
|
|
||||||
import ViewColumnIcon from "@mui/icons-material/ViewColumn";
|
|
||||||
import PendingIcon from "@mui/icons-material/Pending";
|
|
||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
|
||||||
import DoNotDisturbIcon from "@mui/icons-material/DoNotDisturb";
|
|
||||||
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
|
|
||||||
export default function JobPipelineBox(props) {
|
export default function JobPipelineBox(props) {
|
|
@ -2,7 +2,11 @@ import React, { useContext, useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useJobCore, jobStatus } from "@qltr/jobcore";
|
import { useJobCore, jobStatus } from "@qltr/jobcore";
|
||||||
|
import PipelineTriggerDialog, {
|
||||||
|
usePipelineTriggerDialog,
|
||||||
|
} from "./PipelineTriggerDialog";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import AppBar from "@mui/material/AppBar";
|
import AppBar from "@mui/material/AppBar";
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
@ -22,12 +26,13 @@ import ListItemText from "@mui/material/ListItemText";
|
||||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import JobActionMenu, { useJobActionMenu } from "../JobActionMenu";
|
||||||
|
|
||||||
|
const stopPropagation = (e) => e.stopPropagation() && e.preventDefault();
|
||||||
function JobPipelineDisplay(props) {
|
function JobPipelineDisplay(props) {
|
||||||
const { pipeline } = props;
|
const { pipeline } = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: jobState,
|
|
||||||
pipelineCancel,
|
pipelineCancel,
|
||||||
pipelineDestroy,
|
pipelineDestroy,
|
||||||
selectedPipelineBranches,
|
selectedPipelineBranches,
|
||||||
|
@ -37,8 +42,15 @@ function JobPipelineDisplay(props) {
|
||||||
jobIcon,
|
jobIcon,
|
||||||
} = useJobCore();
|
} = useJobCore();
|
||||||
|
|
||||||
const nav = useNavigate();
|
const [tdOpen, tdToggle, trigger, setTrigger, tdClose] =
|
||||||
|
usePipelineTriggerDialog();
|
||||||
|
|
||||||
|
const [jobAnchor, menuJob, setMenuJob, menuOpen, menuClose] =
|
||||||
|
useJobActionMenu();
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const minifyActions = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
|
const nav = useNavigate();
|
||||||
const [time, setTime] = useState(Date.now());
|
const [time, setTime] = useState(Date.now());
|
||||||
const [anchorEl, setAnchorEl] = useState(null);
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
|
@ -46,9 +58,6 @@ function JobPipelineDisplay(props) {
|
||||||
const handleClose = () => setAnchorEl(null);
|
const handleClose = () => setAnchorEl(null);
|
||||||
|
|
||||||
const branches = selectedPipelineBranches(pipeline);
|
const branches = selectedPipelineBranches(pipeline);
|
||||||
const pipelineTriggers = branches.map(({ name }) =>
|
|
||||||
pipeline.pendingTriggers.find(({ testName }) => testName === name)
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => setTime(Date.now()), 1000);
|
const interval = setInterval(() => setTime(Date.now()), 1000);
|
||||||
|
@ -57,7 +66,7 @@ function JobPipelineDisplay(props) {
|
||||||
|
|
||||||
const selectJob = (testName) => () => {
|
const selectJob = (testName) => () => {
|
||||||
const pt = pipeline.pendingTriggers.find(
|
const pt = pipeline.pendingTriggers.find(
|
||||||
({ testName }) => testName === name
|
(ppt) => ppt.testName === testName
|
||||||
);
|
);
|
||||||
if (pt) return selectTimer(pt);
|
if (pt) return selectTimer(pt);
|
||||||
const job = findPipelineJobByTestName(pipeline, testName);
|
const job = findPipelineJobByTestName(pipeline, testName);
|
||||||
|
@ -65,17 +74,24 @@ function JobPipelineDisplay(props) {
|
||||||
toJob(job.jobId);
|
toJob(job.jobId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectTimer = (pt) => {
|
const selectJobMenu = (testName) => (e) => {
|
||||||
console.log("Selected timer:", pt);
|
const pt = pipeline.pendingTriggers.find(
|
||||||
|
(ppt) => ppt.testName === testName
|
||||||
|
);
|
||||||
|
if (pt) return selectTimer(pt);
|
||||||
|
const job = findPipelineJobByTestName(pipeline, testName);
|
||||||
|
if (!job) return;
|
||||||
|
menuOpen(e);
|
||||||
|
setMenuJob(job);
|
||||||
};
|
};
|
||||||
|
|
||||||
function cancelPipeline() {
|
const selectTimer = (pt) => {
|
||||||
pipelineCancel(pipeline.id);
|
setTrigger(pt);
|
||||||
}
|
tdToggle();
|
||||||
|
};
|
||||||
|
|
||||||
function deletePipeline() {
|
const cancelPipeline = () => pipelineCancel(pipeline.id);
|
||||||
pipelineDestroy(pipeline.id);
|
const deletePipeline = () => pipelineDestroy(pipeline.id);
|
||||||
}
|
|
||||||
|
|
||||||
const menuSelect = (cb) => () => {
|
const menuSelect = (cb) => () => {
|
||||||
handleClose();
|
handleClose();
|
||||||
|
@ -102,11 +118,26 @@ function JobPipelineDisplay(props) {
|
||||||
({ testName }) => testName === name
|
({ testName }) => testName === name
|
||||||
);
|
);
|
||||||
if (!pt) return;
|
if (!pt) return;
|
||||||
return moment(moment(pt.triggerAt).diff(moment())).format("mm:ss");
|
const now = moment();
|
||||||
|
const diff = moment(pt.triggerAt.diff(now)).valueOf();
|
||||||
|
const timeString = new Date(diff).toISOString().substring(11, 19);
|
||||||
|
return timeString; // .split(":").filter((s) => s != "00").join(":");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
<PipelineTriggerDialog
|
||||||
|
keepMounted
|
||||||
|
open={tdOpen}
|
||||||
|
onClose={tdClose}
|
||||||
|
trigger={trigger}
|
||||||
|
/>
|
||||||
|
<JobActionMenu
|
||||||
|
keepMounted
|
||||||
|
anchorEl={jobAnchor}
|
||||||
|
menuClose={menuClose}
|
||||||
|
job={menuJob}
|
||||||
|
/>
|
||||||
<AppBar
|
<AppBar
|
||||||
position="fixed"
|
position="fixed"
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -173,10 +204,29 @@ function JobPipelineDisplay(props) {
|
||||||
>
|
>
|
||||||
{timerDisplay(test.name)}
|
{timerDisplay(test.name)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack sx={{ ml: "auto" }}>
|
<Stack
|
||||||
<IconButton aria-label="retry" component="span">
|
onClick={stopPropagation}
|
||||||
|
direction={minifyActions ? "column" : "row"}
|
||||||
|
sx={{
|
||||||
|
ml: "auto",
|
||||||
|
mb: "auto",
|
||||||
|
mt: "auto",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
aria-label="retry"
|
||||||
|
component="span"
|
||||||
|
onClick={selectJob(test.name)}
|
||||||
|
>
|
||||||
{boxIcon(test.name)}
|
{boxIcon(test.name)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
component="span"
|
||||||
|
onClick={selectJobMenu(test.name)}
|
||||||
|
>
|
||||||
|
<MoreVertIcon />
|
||||||
|
</IconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
91
src/views/jobs/pipeline/PipelineTriggerDialog.jsx
Normal file
91
src/views/jobs/pipeline/PipelineTriggerDialog.jsx
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import PipelineTriggerWidget from "./PipelineTriggerWidget.jsx";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
|
||||||
|
export function usePipelineTriggerDialog(isOpen = false) {
|
||||||
|
const [open, setOpen] = useState(isOpen);
|
||||||
|
const [trigger, setTrigger] = useState({});
|
||||||
|
const dialogToggle = () => setOpen(!open);
|
||||||
|
const dialogClose = (confirmedTrigger) => {
|
||||||
|
setOpen(false);
|
||||||
|
if (!confirmedTrigger) return;
|
||||||
|
setTrigger({});
|
||||||
|
};
|
||||||
|
return [open, dialogToggle, trigger, setTrigger, dialogClose];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PipelineTriggerDialog(props) {
|
||||||
|
const { trigger, open, onClose } = props;
|
||||||
|
const theme = useTheme();
|
||||||
|
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
|
useEffect(() => {
|
||||||
|
if (!trigger.triggerAt) return;
|
||||||
|
const expiredInterval = setInterval(() => {
|
||||||
|
if (Date.now() < trigger.triggerAt.valueOf()) return;
|
||||||
|
onClose();
|
||||||
|
clearInterval(expiredInterval);
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(expiredInterval);
|
||||||
|
}, [trigger, open]);
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
sx={
|
||||||
|
fullScreen
|
||||||
|
? {}
|
||||||
|
: { "& .MuiDialog-paper": { width: "80%", maxHeight: 525 } }
|
||||||
|
}
|
||||||
|
maxWidth="xs"
|
||||||
|
open={open}
|
||||||
|
fullScreen={fullScreen}
|
||||||
|
>
|
||||||
|
<Toolbar sx={{ display: { sm: "none" } }} />
|
||||||
|
<DialogTitle>Pipeline Timeout</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
margin="normal"
|
||||||
|
component={"div"}
|
||||||
|
style={{
|
||||||
|
margin: "auto",
|
||||||
|
width: "100%",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontWeight: "bold" }}>Test: </span>
|
||||||
|
{trigger.testName}
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
label="Expires"
|
||||||
|
value={JSON.stringify(trigger.jobReq)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<PipelineTriggerWidget trigger={trigger} close={onClose} />
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onClose}>Close</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
124
src/views/jobs/pipeline/PipelineTriggerWidget.jsx
Normal file
124
src/views/jobs/pipeline/PipelineTriggerWidget.jsx
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import moment from "moment";
|
||||||
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
|
||||||
|
const happyGreen = "#64dd17";
|
||||||
|
const happyGrey = "#e0e0e0";
|
||||||
|
export default function PipelineTriggerWidget(props) {
|
||||||
|
const { trigger, close } = props;
|
||||||
|
const [time, setTime] = useState(Date.now());
|
||||||
|
const [hovering, setHovering] = useState(false);
|
||||||
|
const hoverStop = () => setHovering(false);
|
||||||
|
const hoverStart = () => setHovering(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => setTime(Date.now()), 200);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function runNow() {
|
||||||
|
close();
|
||||||
|
if (!trigger.launchTrigger) return;
|
||||||
|
trigger.launchTrigger();
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerMomentRemaining() {
|
||||||
|
if (!trigger || !trigger.triggerAt) return null;
|
||||||
|
const now = moment();
|
||||||
|
const diff = moment(trigger.triggerAt.diff(now)).valueOf();
|
||||||
|
const timeString = new Date(diff).toISOString().substring(11, 19);
|
||||||
|
return timeString; // .split(":").filter((s) => s != "00").join(":");
|
||||||
|
}
|
||||||
|
const normalizeValue = () => {
|
||||||
|
const { triggerQueued: start, triggerAt: end } = trigger;
|
||||||
|
const total = end - start;
|
||||||
|
// TODO The below line causes issues because close is called during a render.
|
||||||
|
// This causes MASSIVE issues, and needs to be resolved
|
||||||
|
//if (((trigger.triggerAt - Date.now()) / total) * 100 > 100) return close();
|
||||||
|
return ((total - (trigger.triggerAt - Date.now())) / total) * 100;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "flex" }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
display: "inline-flex",
|
||||||
|
backgroundColor: hovering ? happyGreen : "white",
|
||||||
|
borderRadius: "50%",
|
||||||
|
padding: 0.5,
|
||||||
|
margin: "auto",
|
||||||
|
}}
|
||||||
|
onMouseEnter={hoverStart}
|
||||||
|
onMouseLeave={hoverStop}
|
||||||
|
>
|
||||||
|
<CircularProgress
|
||||||
|
size="8rem"
|
||||||
|
variant="determinate"
|
||||||
|
sx={{ color: happyGrey, position: "absolute" }}
|
||||||
|
thickness={2.5}
|
||||||
|
value={100}
|
||||||
|
/>
|
||||||
|
<CircularProgress
|
||||||
|
size="8rem"
|
||||||
|
variant="determinate"
|
||||||
|
sx={{ color: hovering ? "gray" : happyGreen }}
|
||||||
|
thickness={2.5}
|
||||||
|
value={normalizeValue()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
position: "absolute",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
component="div"
|
||||||
|
color="black"
|
||||||
|
sx={{
|
||||||
|
// fontWeight: "bold",
|
||||||
|
cursor: !hovering ? "inherit" : "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!hovering && triggerMomentRemaining()}
|
||||||
|
{hovering && (
|
||||||
|
<React.Fragment>
|
||||||
|
<Button
|
||||||
|
component="div"
|
||||||
|
sx={{ color: "white", fontWeight: "bold" }}
|
||||||
|
onClick={runNow}
|
||||||
|
>
|
||||||
|
Run Now
|
||||||
|
</Button>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
component="div"
|
||||||
|
color="white"
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{triggerMomentRemaining()}
|
||||||
|
</Typography>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue