From 4097c121e3f2da6eddaea1aee013adb1147785a3 Mon Sep 17 00:00:00 2001 From: Elijah Dunemask Date: Sat, 13 Aug 2022 20:43:35 +0000 Subject: [PATCH] Job navigation rewrite --- src/util/JobTools.jsx | 12 ++ src/views/catalog/Catalog.jsx | 2 +- src/views/catalog/CatalogBox.jsx | 20 +-- src/views/failing/Failing.jsx | 10 +- src/views/failing/FailingBox.jsx | 19 +-- src/views/jobs/JobPipelineDisplay.jsx | 17 +-- src/views/jobs/JobPiplinePendingView.jsx | 155 +++++++++++++++++++++++ src/views/jobs/JobView.jsx | 10 +- src/views/jobs/Jobs.jsx | 56 ++++---- src/views/jobs/builder/JobBuilder.jsx | 8 +- 10 files changed, 242 insertions(+), 67 deletions(-) create mode 100644 src/views/jobs/JobPiplinePendingView.jsx diff --git a/src/util/JobTools.jsx b/src/util/JobTools.jsx index 6875804..e9697b2 100644 --- a/src/util/JobTools.jsx +++ b/src/util/JobTools.jsx @@ -1,5 +1,6 @@ import { useContext } from "react"; import JobContext, { jobStatus } from "@qltr/jobs"; +import { useNavigate } from "react-router-dom"; import CheckIcon from "@mui/icons-material/Check"; import ClearIcon from "@mui/icons-material/Clear"; @@ -55,3 +56,14 @@ export const selectedPipelineBranches = (pipeline) => export const findPipelineJobByTestName = (pipeline, jobs, testName) => pipelineJobs(pipeline, jobs).find((j) => j.branchId === testName); + +export function useJobNav() { + const navigate = useNavigate(); + + const toJob = (jobId) => navigate(`/qualiteer/jobs#job-${jobId}`); + const toPipeline = (pipelineId) => + navigate(`/qualiteer/jobs#pipeline-${pipelineId}`); + + const toJobs = () => navigate(`/qualiteer/jobs`); + return { toJob, toPipeline, toJobs }; +} diff --git a/src/views/catalog/Catalog.jsx b/src/views/catalog/Catalog.jsx index d0f0388..380e2e8 100644 --- a/src/views/catalog/Catalog.jsx +++ b/src/views/catalog/Catalog.jsx @@ -25,7 +25,7 @@ export default function Catalog() { for (var test of tests) { if (test.isPipeline) { const pipeline = jobState.pipelines.find((p) => - p.selectedBranches.includes(test.name) + p.selectedBranches.find((b) => b.name === test.name) ); if (!pipeline) continue; const pipelineJob = jobState.jobs.find( diff --git a/src/views/catalog/CatalogBox.jsx b/src/views/catalog/CatalogBox.jsx index 422f3f0..0acf315 100644 --- a/src/views/catalog/CatalogBox.jsx +++ b/src/views/catalog/CatalogBox.jsx @@ -1,9 +1,13 @@ import React, { useState, useContext } from "react"; -import { useNavigate } from "react-router-dom"; + import { usePipelineMappings } from "@qltr/queries"; import StoreContext from "@qltr/store"; import JobContext, { jobStatus } from "@qltr/jobs"; -import { useJobIconState, usePipelineIconState } from "@qltr/util/JobTools"; +import { + useJobIconState, + usePipelineIconState, + useJobNav, +} from "@qltr/util/JobTools"; import Accordion from "@mui/material/Accordion"; import AccordionDetails from "@mui/material/AccordionDetails"; @@ -31,25 +35,23 @@ export default function CatalogBox(props) { pipeline, } = catalogTest; - const navigate = useNavigate(); const { data: pipelineMappings, isLoading } = usePipelineMappings(); const { state: store } = useContext(StoreContext); - const { jobFactory } = useContext(JobContext); - + const jobNav = useJobNav(); const [open, setOpen] = useState(false); const toggleOpen = () => setOpen(!open); const navigateToJob = () => { - if (pipeline) return navigate(`/qualiteer/jobs#p${pipeline.id}`); - navigate(`/qualiteer/jobs#${job.jobId}`); + if (pipeline) return jobNav.toPipeline(pipeline.id); + jobNav.toJob(job.jobId); }; const runTest = () => { if (isPipeline) return runPipelineTest(); const jobId = jobFactory({ testNames: [testName], isTriage: true }); - if (store.focusJob) navigate(`/qualiteer/jobs#${jobId}`); + if (store.focusJob) jobNav.toJob(jobId); }; const runPipelineTest = () => { @@ -67,7 +69,7 @@ export default function CatalogBox(props) { isTriage: true, }; const pipeline = jobFactory(builderCache); - if (store.focusJob) navigate(`/qualiteer/jobs#p${pipeline.id}`); + if (store.focusJob) jobNav.toPipeline(pipeline.id); }; const jobOnClick = (e) => { diff --git a/src/views/failing/Failing.jsx b/src/views/failing/Failing.jsx index 53e748f..862743e 100644 --- a/src/views/failing/Failing.jsx +++ b/src/views/failing/Failing.jsx @@ -1,8 +1,8 @@ import { useState, useContext } from "react"; -import { useNavigate } from "react-router-dom"; import { useCurrentlyFailing, useSilencedAlerts } from "@qltr/queries"; import StoreContext from "@qltr/store"; import JobContext from "@qltr/jobs"; +import { useJobNav } from "@qltr/util/JobTools"; import SilenceDialog from "../alerting/SilenceDialog.jsx"; import FailingBox from "./FailingBox.jsx"; @@ -20,10 +20,10 @@ import Typography from "@mui/material/Typography"; export default function Failing() { const { state: jobState, retryAll } = useContext(JobContext); - const navigate = useNavigate(); const { state: store, silenceRequest } = useContext(StoreContext); const { isLoading, data: failing } = useCurrentlyFailing(); const { isSilencedLoading, data: silencedAlerts } = useSilencedAlerts(); + const jobNav = useJobNav(); const [silenceEntry, setSilenceEntry] = useState({ open: false }); const closeSilence = () => setSilenceEntry({ ...silenceEntry, open: false }); @@ -47,11 +47,11 @@ export default function Failing() { const jobId = retryAll(store.failing); if (!store.focusJob) return; - navigate(`/qualiteer/jobs#${jobId}`); + jobNav.toJob(jobId); }; const failingTestsWithJobs = () => { - if(isLoading) return []; + if (isLoading) return []; const silences = silencedAlerts ?? []; for (var test of failing) { const silence = silences.find( @@ -61,7 +61,7 @@ export default function Failing() { if (silence) test.silencedUntil = silence; if (test.isPipeline) { const pipeline = jobState.pipelines.find((p) => - p.selectedBranches.includes(test.name) + p.selectedBranches.find((b) => b.name === test.name) ); if (!pipeline) continue; const pipelineJob = jobState.jobs.find( diff --git a/src/views/failing/FailingBox.jsx b/src/views/failing/FailingBox.jsx index f5810cf..c4e1987 100644 --- a/src/views/failing/FailingBox.jsx +++ b/src/views/failing/FailingBox.jsx @@ -1,9 +1,12 @@ import React, { useState, useContext } from "react"; -import { useNavigate } from "react-router-dom"; import { usePipelineMappings } from "@qltr/queries"; import StoreContext from "@qltr/store"; import JobContext, { jobStatus } from "@qltr/jobs"; -import { useJobIconState, usePipelineIconState } from "@qltr/util/JobTools"; +import { + useJobIconState, + usePipelineIconState, + useJobNav, +} from "@qltr/util/JobTools"; import Accordion from "@mui/material/Accordion"; import AccordionDetails from "@mui/material/AccordionDetails"; @@ -51,16 +54,14 @@ export default function FailingBox(props) { recentResults, failedMessage, isPipeline, - jobStatus: testJobStatus, job, pipeline, } = failingTest; - const navigate = useNavigate(); const { data: pipelineMappings, isLoading } = usePipelineMappings(); const { jobFactory } = useContext(JobContext); - const { state: store, updateStore, removeFailure } = useContext(StoreContext); + const jobNav = useJobNav(); const [open, setOpen] = useState(false); const toggleOpen = () => setOpen(!open); @@ -93,18 +94,18 @@ export default function FailingBox(props) { isTriage: true, }; const pipeline = jobFactory(builderCache); - if (store.focusJob) navigate(`/qualiteer/jobs#p${pipeline.id}`); + if (store.focusJob) jobNav.toPipeline(pipeline.id); }; const retryTest = () => { if (isPipeline) return retryPipelineTest(); const jobId = jobFactory({ testNames: [testName], isTriage: true }); - if (store.focusJob) navigate(`/qualiteer/jobs#${jobId}`); + if (store.focusJob) jobNav.toJob(jobId); }; const navigateToJob = () => { - if (pipeline) return navigate(`/qualiteer/jobs#p${pipeline.id}`); - navigate(`/qualiteer/jobs#${job.jobId}`); + if (pipeline) return jobNav.toPipeline(pipeline.id); + jobNav.toJob(job.jobId); }; const jobOnClick = () => { diff --git a/src/views/jobs/JobPipelineDisplay.jsx b/src/views/jobs/JobPipelineDisplay.jsx index 574d3eb..ebaae99 100644 --- a/src/views/jobs/JobPipelineDisplay.jsx +++ b/src/views/jobs/JobPipelineDisplay.jsx @@ -1,11 +1,11 @@ import React, { useContext } from "react"; -import { useNavigate } from "react-router-dom"; import JobContext, { jobStatus } from "@qltr/jobs"; import { selectedPipelineBranches, pipelineJobs, findPipelineJobByTestName, useJobIconState, + useJobNav, } from "@qltr/util/JobTools"; import Box from "@mui/material/Box"; @@ -18,11 +18,7 @@ import AccordionSummary from "@mui/material/AccordionSummary"; import Stack from "@mui/material/Stack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; 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 Menu from "@mui/material/Menu"; @@ -39,7 +35,8 @@ function JobPipelineDisplay(props) { pipelineCancel, pipelineDestroy, } = useContext(JobContext); - const navigate = useNavigate(); + + const jobNav = useJobNav(); const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); @@ -53,13 +50,9 @@ function JobPipelineDisplay(props) { const selectJob = (testName) => () => { const job = findPipelineJobByTestName(pipeline, jobState.jobs, testName); if (!job) return; - navigate(`/qualiteer/jobs#${job.jobId}`); + jobNav.toJob(job.jobId); }; - function navigateToJobs() { - navigate(`/qualiteer/jobs`); - } - function cancelPipeline() { pipelineCancel(pipeline.id); } @@ -99,7 +92,7 @@ function JobPipelineDisplay(props) { - + diff --git a/src/views/jobs/JobPiplinePendingView.jsx b/src/views/jobs/JobPiplinePendingView.jsx new file mode 100644 index 0000000..9fed45f --- /dev/null +++ b/src/views/jobs/JobPiplinePendingView.jsx @@ -0,0 +1,155 @@ +import React, { useContext, useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import JobContext, { jobStatus } from "@qltr/jobs"; +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 default function JobPipelinePendingView(props) { + const navigate = useNavigate(); + const { job } = props; + const { jobFactory, jobCancel, jobDestroy } = useContext(JobContext); + const { state: store } = useContext(StoreContext); + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + 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 = jobFactory(job.builderCache); + if (store.focusJob) navigate(`/qualiteer/jobs#${jobId}`); + } + + function downloadLog() { + if (job.status === jobStatus.PENDING) return; + download(`${job.jobId}.txt`, job.log.join("\n")); + } + + function cancelJob() { + jobCancel(job.jobId); + } + + function deleteJob() { + jobDestroy(job.jobId); + navigateToJobs(); + } + + const menuSelect = (cb) => () => { + handleClose(); + cb(); + }; + + function navigateToJobs() { + if (job.isPipeline) return navigate(`/qualiteer/jobs#p${job.pipelineId}`); + navigate("/qualiteer/jobs"); + } + + return ( + + + + + + + + + + {job.name} + + {job.isPipeline && ( + + + + )} + + + + + + + + + + + + + + Download Log + + {!job.isPipeline && ( + + + {job.status === jobStatus.OK || job.status === jobStatus.ERROR ? ( + + ) : ( + + )} + + + {job.status === jobStatus.ERROR ? "Retry" : "Duplicate"} + + + )} + {job.status === jobStatus.OK || + job.status === jobStatus.ERROR || + job.status === jobStatus.CANCELED ? null : ( + + + + + Cancel + + )} + {!job.isPipeline && ( + + + + + Delete + + )} + + + ); +} diff --git a/src/views/jobs/JobView.jsx b/src/views/jobs/JobView.jsx index af01fcd..bc214ee 100644 --- a/src/views/jobs/JobView.jsx +++ b/src/views/jobs/JobView.jsx @@ -20,6 +20,7 @@ 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 default function JobView(props) { const navigate = useNavigate(); @@ -93,10 +94,15 @@ export default function JobView(props) { - + {job.name} - + {job.isPipeline && ( + + + + )} + diff --git a/src/views/jobs/Jobs.jsx b/src/views/jobs/Jobs.jsx index 0d96e8e..18f63c4 100644 --- a/src/views/jobs/Jobs.jsx +++ b/src/views/jobs/Jobs.jsx @@ -1,4 +1,4 @@ -import React, { useState, useContext, useEffect } from "react"; +import React, { useContext, useEffect } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import JobContext from "@qltr/jobs"; @@ -15,14 +15,33 @@ export default function Jobs() { const location = useLocation(); const navigate = useNavigate(); + const jobViewPrefix = "#job-"; + const pipelineViewPrefix = "#pipeline-"; + const cachePrefix = "#cache-"; + + function jobHashLoader() { + const { hash } = location; + if (!hash) return {}; + const jobIndex = hash.indexOf(jobViewPrefix); + const pipelineIndex = hash.indexOf(pipelineViewPrefix); + const cacheIndex = hash.indexOf(cachePrefix); + const jobId = jobIndex !== 0 ? null : hash.slice(jobViewPrefix.length); + const pipelineId = + pipelineIndex !== 0 ? null : hash.slice(pipelineViewPrefix.length); + const cacheData = cacheIndex !== 0 ? null : hash.slice(cachePrefix.length); + const job = !jobId ? null : jobState.jobs.find((j) => j.jobId === jobId); + const pipeline = !pipelineId + ? null + : jobState.pipelines.find((p) => p.id === pipelineId); + const builderCache = !cacheData ? null : { fakedata: "yep" }; + return { job, pipeline, builderCache }; + } + + const jobHash = jobHashLoader(); + useEffect(() => { - const jobName = location.hash.slice(1); - const pipelineId = jobName.slice(1); - if (!jobName || !pipelineId) return; - const hasJob = jobState.pipelines.find((p) => p.id === pipelineId); - const hasPipeline = jobState.jobs.find((job) => job.name === jobName); - if (hasPipeline || hasJob) return; - if (jobName || pipelineId) navigate("/qualiteer/jobs"); + const { job, pipeline, builderCache } = jobHash; + if (!job && !pipeline && !builderCache) navigate("/qualiteer/jobs"); }); return ( @@ -59,7 +78,7 @@ export default function Jobs() { .map((v, i) => ( @@ -69,7 +88,7 @@ export default function Jobs() { @@ -77,21 +96,8 @@ export default function Jobs() { )} - {location.hash[1] === "p" - ? jobState.pipelines.find((p) => p.id === location.hash.slice(2)) && ( - p.id === location.hash.slice(2) - )} - /> - ) - : jobState.jobs.find((job) => job.name === location.hash.slice(1)) && ( - job.name === location.hash.slice(1) - )} - /> - )} + {jobHash.pipeline && } + {jobHash.job && } ); } diff --git a/src/views/jobs/builder/JobBuilder.jsx b/src/views/jobs/builder/JobBuilder.jsx index b281d2d..0ab230a 100644 --- a/src/views/jobs/builder/JobBuilder.jsx +++ b/src/views/jobs/builder/JobBuilder.jsx @@ -1,7 +1,7 @@ -import React, { useContext, useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; +import React, { useContext, useState } from "react"; import StoreContext from "@qltr/store"; import JobContext from "@qltr/jobs"; +import { useJobNav } from "@qltr/util/JobTools"; import Dialog from "@mui/material/Dialog"; import Toolbar from "@mui/material/Toolbar"; @@ -27,9 +27,9 @@ import PipelineTrackSelector from "./PipelineTrackSelector.jsx"; import PipelineConfirm from "./PipelineConfirm.jsx"; export default function JobBuilder() { - const navigate = useNavigate(); const { state: store } = useContext(StoreContext); const { jobFactory } = useContext(JobContext); + const jobNav = useJobNav(); const [quickOpen, setQuickOpen] = useState(false); const [jobDialogOpen, setJobDialogOpen] = useState(false); @@ -53,7 +53,7 @@ export default function JobBuilder() { setJobDialogOpen(false); if (!confirmed) return; const jobId = jobFactory(cache); - if (store.focusJob) navigate(`/qualiteer/jobs#${jobId}`); + if (store.focusJob) jobNav.toJob(jobId); }; // Pull info from url if possible?