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 && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+}
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?