Merge branch 'job-nav' into 'master'

Job navigation rewrite

See merge request Dunemask/qualiteer!2
This commit is contained in:
Elijah Dunemask 2022-08-13 20:43:35 +00:00
commit 311cc229e9
10 changed files with 242 additions and 67 deletions

View file

@ -1,5 +1,6 @@
import { useContext } from "react"; import { useContext } from "react";
import JobContext, { jobStatus } from "@qltr/jobs"; import JobContext, { jobStatus } from "@qltr/jobs";
import { useNavigate } from "react-router-dom";
import CheckIcon from "@mui/icons-material/Check"; import CheckIcon from "@mui/icons-material/Check";
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
@ -55,3 +56,14 @@ export const selectedPipelineBranches = (pipeline) =>
export const findPipelineJobByTestName = (pipeline, jobs, testName) => export const findPipelineJobByTestName = (pipeline, jobs, testName) =>
pipelineJobs(pipeline, jobs).find((j) => j.branchId === 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 };
}

View file

@ -25,7 +25,7 @@ export default function Catalog() {
for (var test of tests) { for (var test of tests) {
if (test.isPipeline) { if (test.isPipeline) {
const pipeline = jobState.pipelines.find((p) => const pipeline = jobState.pipelines.find((p) =>
p.selectedBranches.includes(test.name) p.selectedBranches.find((b) => b.name === test.name)
); );
if (!pipeline) continue; if (!pipeline) continue;
const pipelineJob = jobState.jobs.find( const pipelineJob = jobState.jobs.find(

View file

@ -1,9 +1,13 @@
import React, { useState, useContext } from "react"; import React, { useState, useContext } from "react";
import { useNavigate } from "react-router-dom";
import { usePipelineMappings } from "@qltr/queries"; import { usePipelineMappings } from "@qltr/queries";
import StoreContext from "@qltr/store"; import StoreContext from "@qltr/store";
import JobContext, { jobStatus } from "@qltr/jobs"; 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 Accordion from "@mui/material/Accordion";
import AccordionDetails from "@mui/material/AccordionDetails"; import AccordionDetails from "@mui/material/AccordionDetails";
@ -31,25 +35,23 @@ export default function CatalogBox(props) {
pipeline, pipeline,
} = catalogTest; } = catalogTest;
const navigate = useNavigate();
const { data: pipelineMappings, isLoading } = usePipelineMappings(); const { data: pipelineMappings, isLoading } = usePipelineMappings();
const { state: store } = useContext(StoreContext); const { state: store } = useContext(StoreContext);
const { jobFactory } = useContext(JobContext); const { jobFactory } = useContext(JobContext);
const jobNav = useJobNav();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const toggleOpen = () => setOpen(!open); const toggleOpen = () => setOpen(!open);
const navigateToJob = () => { const navigateToJob = () => {
if (pipeline) return navigate(`/qualiteer/jobs#p${pipeline.id}`); if (pipeline) return jobNav.toPipeline(pipeline.id);
navigate(`/qualiteer/jobs#${job.jobId}`); jobNav.toJob(job.jobId);
}; };
const runTest = () => { const runTest = () => {
if (isPipeline) return runPipelineTest(); if (isPipeline) return runPipelineTest();
const jobId = jobFactory({ testNames: [testName], isTriage: true }); const jobId = jobFactory({ testNames: [testName], isTriage: true });
if (store.focusJob) navigate(`/qualiteer/jobs#${jobId}`); if (store.focusJob) jobNav.toJob(jobId);
}; };
const runPipelineTest = () => { const runPipelineTest = () => {
@ -67,7 +69,7 @@ export default function CatalogBox(props) {
isTriage: true, isTriage: true,
}; };
const pipeline = jobFactory(builderCache); const pipeline = jobFactory(builderCache);
if (store.focusJob) navigate(`/qualiteer/jobs#p${pipeline.id}`); if (store.focusJob) jobNav.toPipeline(pipeline.id);
}; };
const jobOnClick = (e) => { const jobOnClick = (e) => {

View file

@ -1,8 +1,8 @@
import { useState, useContext } from "react"; import { useState, useContext } from "react";
import { useNavigate } from "react-router-dom";
import { useCurrentlyFailing, useSilencedAlerts } from "@qltr/queries"; import { useCurrentlyFailing, useSilencedAlerts } from "@qltr/queries";
import StoreContext from "@qltr/store"; import StoreContext from "@qltr/store";
import JobContext from "@qltr/jobs"; import JobContext from "@qltr/jobs";
import { useJobNav } from "@qltr/util/JobTools";
import SilenceDialog from "../alerting/SilenceDialog.jsx"; import SilenceDialog from "../alerting/SilenceDialog.jsx";
import FailingBox from "./FailingBox.jsx"; import FailingBox from "./FailingBox.jsx";
@ -20,10 +20,10 @@ import Typography from "@mui/material/Typography";
export default function Failing() { export default function Failing() {
const { state: jobState, retryAll } = useContext(JobContext); const { state: jobState, retryAll } = useContext(JobContext);
const navigate = useNavigate();
const { state: store, silenceRequest } = useContext(StoreContext); const { state: store, silenceRequest } = useContext(StoreContext);
const { isLoading, data: failing } = useCurrentlyFailing(); const { isLoading, data: failing } = useCurrentlyFailing();
const { isSilencedLoading, data: silencedAlerts } = useSilencedAlerts(); const { isSilencedLoading, data: silencedAlerts } = useSilencedAlerts();
const jobNav = useJobNav();
const [silenceEntry, setSilenceEntry] = useState({ open: false }); const [silenceEntry, setSilenceEntry] = useState({ open: false });
const closeSilence = () => setSilenceEntry({ ...silenceEntry, open: false }); const closeSilence = () => setSilenceEntry({ ...silenceEntry, open: false });
@ -47,7 +47,7 @@ export default function Failing() {
const jobId = retryAll(store.failing); const jobId = retryAll(store.failing);
if (!store.focusJob) return; if (!store.focusJob) return;
navigate(`/qualiteer/jobs#${jobId}`); jobNav.toJob(jobId);
}; };
const failingTestsWithJobs = () => { const failingTestsWithJobs = () => {
@ -61,7 +61,7 @@ export default function Failing() {
if (silence) test.silencedUntil = silence; if (silence) test.silencedUntil = silence;
if (test.isPipeline) { if (test.isPipeline) {
const pipeline = jobState.pipelines.find((p) => const pipeline = jobState.pipelines.find((p) =>
p.selectedBranches.includes(test.name) p.selectedBranches.find((b) => b.name === test.name)
); );
if (!pipeline) continue; if (!pipeline) continue;
const pipelineJob = jobState.jobs.find( const pipelineJob = jobState.jobs.find(

View file

@ -1,9 +1,12 @@
import React, { useState, useContext } from "react"; import React, { useState, useContext } from "react";
import { useNavigate } from "react-router-dom";
import { usePipelineMappings } from "@qltr/queries"; import { usePipelineMappings } from "@qltr/queries";
import StoreContext from "@qltr/store"; import StoreContext from "@qltr/store";
import JobContext, { jobStatus } from "@qltr/jobs"; 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 Accordion from "@mui/material/Accordion";
import AccordionDetails from "@mui/material/AccordionDetails"; import AccordionDetails from "@mui/material/AccordionDetails";
@ -51,16 +54,14 @@ export default function FailingBox(props) {
recentResults, recentResults,
failedMessage, failedMessage,
isPipeline, isPipeline,
jobStatus: testJobStatus,
job, job,
pipeline, pipeline,
} = failingTest; } = failingTest;
const navigate = useNavigate();
const { data: pipelineMappings, isLoading } = usePipelineMappings(); const { data: pipelineMappings, isLoading } = usePipelineMappings();
const { jobFactory } = useContext(JobContext); const { jobFactory } = useContext(JobContext);
const { state: store, updateStore, removeFailure } = useContext(StoreContext); const { state: store, updateStore, removeFailure } = useContext(StoreContext);
const jobNav = useJobNav();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const toggleOpen = () => setOpen(!open); const toggleOpen = () => setOpen(!open);
@ -93,18 +94,18 @@ export default function FailingBox(props) {
isTriage: true, isTriage: true,
}; };
const pipeline = jobFactory(builderCache); const pipeline = jobFactory(builderCache);
if (store.focusJob) navigate(`/qualiteer/jobs#p${pipeline.id}`); if (store.focusJob) jobNav.toPipeline(pipeline.id);
}; };
const retryTest = () => { const retryTest = () => {
if (isPipeline) return retryPipelineTest(); if (isPipeline) return retryPipelineTest();
const jobId = jobFactory({ testNames: [testName], isTriage: true }); const jobId = jobFactory({ testNames: [testName], isTriage: true });
if (store.focusJob) navigate(`/qualiteer/jobs#${jobId}`); if (store.focusJob) jobNav.toJob(jobId);
}; };
const navigateToJob = () => { const navigateToJob = () => {
if (pipeline) return navigate(`/qualiteer/jobs#p${pipeline.id}`); if (pipeline) return jobNav.toPipeline(pipeline.id);
navigate(`/qualiteer/jobs#${job.jobId}`); jobNav.toJob(job.jobId);
}; };
const jobOnClick = () => { const jobOnClick = () => {

View file

@ -1,11 +1,11 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import { useNavigate } from "react-router-dom";
import JobContext, { jobStatus } from "@qltr/jobs"; import JobContext, { jobStatus } from "@qltr/jobs";
import { import {
selectedPipelineBranches, selectedPipelineBranches,
pipelineJobs, pipelineJobs,
findPipelineJobByTestName, findPipelineJobByTestName,
useJobIconState, useJobIconState,
useJobNav,
} from "@qltr/util/JobTools"; } from "@qltr/util/JobTools";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
@ -18,11 +18,7 @@ import AccordionSummary from "@mui/material/AccordionSummary";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
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 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 DoNotDisturbIcon from "@mui/icons-material/DoNotDisturb";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
@ -39,7 +35,8 @@ function JobPipelineDisplay(props) {
pipelineCancel, pipelineCancel,
pipelineDestroy, pipelineDestroy,
} = useContext(JobContext); } = useContext(JobContext);
const navigate = useNavigate();
const jobNav = useJobNav();
const [anchorEl, setAnchorEl] = React.useState(null); const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
@ -53,13 +50,9 @@ function JobPipelineDisplay(props) {
const selectJob = (testName) => () => { const selectJob = (testName) => () => {
const job = findPipelineJobByTestName(pipeline, jobState.jobs, testName); const job = findPipelineJobByTestName(pipeline, jobState.jobs, testName);
if (!job) return; if (!job) return;
navigate(`/qualiteer/jobs#${job.jobId}`); jobNav.toJob(job.jobId);
}; };
function navigateToJobs() {
navigate(`/qualiteer/jobs`);
}
function cancelPipeline() { function cancelPipeline() {
pipelineCancel(pipeline.id); pipelineCancel(pipeline.id);
} }
@ -99,7 +92,7 @@ function JobPipelineDisplay(props) {
<Toolbar disableGutters /> <Toolbar disableGutters />
<Box sx={{ flexGrow: 1, margin: "0 10px" }}> <Box sx={{ flexGrow: 1, margin: "0 10px" }}>
<Toolbar disableGutters> <Toolbar disableGutters>
<IconButton onClick={navigateToJobs}> <IconButton onClick={jobNav.toJobs}>
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButton>
<Typography variant="h6" sx={{ ml: "auto", mr: "auto" }}> <Typography variant="h6" sx={{ ml: "auto", mr: "auto" }}>

View file

@ -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 (
<Box>
<AppBar
position="fixed"
sx={{
backgroundColor: "white",
boxShadow: "none",
color: "black",
}}
>
<Toolbar disableGutters />
<Box sx={{ flexGrow: 1, margin: "0 10px" }}>
<Toolbar disableGutters>
<IconButton onClick={navigateToJobs}>
<ArrowBackIcon />
</IconButton>
<Typography variant="h6" component="span" sx={{ ml: "auto" }}>
{job.name}
</Typography>
{job.isPipeline && (
<IconButton>
<ViewColumnIcon />
</IconButton>
)}
<IconButton onClick={handleClick} sx={{ ml: "auto" }}>
<MoreVertIcon />
</IconButton>
</Toolbar>
</Box>
</AppBar>
<Toolbar disableGutters />
<JobLogView log={job.log} status={job.status} />
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
<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>
</Box>
);
}

View file

@ -20,6 +20,7 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import DoNotDisturbIcon from "@mui/icons-material/DoNotDisturb"; import DoNotDisturbIcon from "@mui/icons-material/DoNotDisturb";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import ViewColumnIcon from "@mui/icons-material/ViewColumn";
export default function JobView(props) { export default function JobView(props) {
const navigate = useNavigate(); const navigate = useNavigate();
@ -93,10 +94,15 @@ export default function JobView(props) {
<IconButton onClick={navigateToJobs}> <IconButton onClick={navigateToJobs}>
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButton>
<Typography variant="h6" sx={{ ml: "auto", mr: "auto" }}> <Typography variant="h6" component="span" sx={{ ml: "auto" }}>
{job.name} {job.name}
</Typography> </Typography>
<IconButton onClick={handleClick}> {job.isPipeline && (
<IconButton>
<ViewColumnIcon />
</IconButton>
)}
<IconButton onClick={handleClick} sx={{ ml: "auto" }}>
<MoreVertIcon /> <MoreVertIcon />
</IconButton> </IconButton>
</Toolbar> </Toolbar>

View file

@ -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 { useLocation, useNavigate } from "react-router-dom";
import JobContext from "@qltr/jobs"; import JobContext from "@qltr/jobs";
@ -15,14 +15,33 @@ export default function Jobs() {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); 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(() => { useEffect(() => {
const jobName = location.hash.slice(1); const { job, pipeline, builderCache } = jobHash;
const pipelineId = jobName.slice(1); if (!job && !pipeline && !builderCache) navigate("/qualiteer/jobs");
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");
}); });
return ( return (
@ -59,7 +78,7 @@ export default function Jobs() {
.map((v, i) => ( .map((v, i) => (
<a <a
key={i} key={i}
href={`/qualiteer/jobs#${v.name}`} href={`/qualiteer/jobs#job-${v.jobId}`}
style={{ textDecoration: "none" }} style={{ textDecoration: "none" }}
> >
<JobBox job={v} /> <JobBox job={v} />
@ -69,7 +88,7 @@ export default function Jobs() {
<a <a
key={i} key={i}
style={{ textDecoration: "none" }} style={{ textDecoration: "none" }}
href={`/qualiteer/jobs#p${p.id}`} href={`/qualiteer/jobs#pipeline-${p.id}`}
> >
<JobPipelineBox pipeline={p} /> <JobPipelineBox pipeline={p} />
</a> </a>
@ -77,21 +96,8 @@ export default function Jobs() {
</React.Fragment> </React.Fragment>
)} )}
{location.hash[1] === "p" {jobHash.pipeline && <JobPipelineDisplay pipeline={jobHash.pipeline} />}
? jobState.pipelines.find((p) => p.id === location.hash.slice(2)) && ( {jobHash.job && <JobView job={jobHash.job} />}
<JobPipelineDisplay
pipeline={jobState.pipelines.find(
(p) => p.id === location.hash.slice(2)
)}
/>
)
: jobState.jobs.find((job) => job.name === location.hash.slice(1)) && (
<JobView
job={jobState.jobs.find(
(job) => job.name === location.hash.slice(1)
)}
/>
)}
</div> </div>
); );
} }

View file

@ -1,7 +1,7 @@
import React, { useContext, useState, useEffect } from "react"; import React, { useContext, useState } from "react";
import { useNavigate } from "react-router-dom";
import StoreContext from "@qltr/store"; import StoreContext from "@qltr/store";
import JobContext from "@qltr/jobs"; import JobContext from "@qltr/jobs";
import { useJobNav } from "@qltr/util/JobTools";
import Dialog from "@mui/material/Dialog"; import Dialog from "@mui/material/Dialog";
import Toolbar from "@mui/material/Toolbar"; import Toolbar from "@mui/material/Toolbar";
@ -27,9 +27,9 @@ import PipelineTrackSelector from "./PipelineTrackSelector.jsx";
import PipelineConfirm from "./PipelineConfirm.jsx"; import PipelineConfirm from "./PipelineConfirm.jsx";
export default function JobBuilder() { export default function JobBuilder() {
const navigate = useNavigate();
const { state: store } = useContext(StoreContext); const { state: store } = useContext(StoreContext);
const { jobFactory } = useContext(JobContext); const { jobFactory } = useContext(JobContext);
const jobNav = useJobNav();
const [quickOpen, setQuickOpen] = useState(false); const [quickOpen, setQuickOpen] = useState(false);
const [jobDialogOpen, setJobDialogOpen] = useState(false); const [jobDialogOpen, setJobDialogOpen] = useState(false);
@ -53,7 +53,7 @@ export default function JobBuilder() {
setJobDialogOpen(false); setJobDialogOpen(false);
if (!confirmed) return; if (!confirmed) return;
const jobId = jobFactory(cache); const jobId = jobFactory(cache);
if (store.focusJob) navigate(`/qualiteer/jobs#${jobId}`); if (store.focusJob) jobNav.toJob(jobId);
}; };
// Pull info from url if possible? // Pull info from url if possible?