Link K8S deps properly
This commit is contained in:
parent
0ac77cdb15
commit
f0260fc819
64 changed files with 4282 additions and 3069 deletions
|
@ -18,7 +18,7 @@ const ACTIONS = {
|
|||
PIPELINE: "p",
|
||||
};
|
||||
|
||||
const url = "https://qualiteer.elijahparker3.repl.co/";
|
||||
const url = "/";
|
||||
|
||||
const initialState = {
|
||||
jobs: [],
|
||||
|
@ -134,7 +134,7 @@ export const JobProvider = ({ children }) => {
|
|||
request,
|
||||
onLog,
|
||||
onClose,
|
||||
null,
|
||||
() => {},
|
||||
onPipelineTrigger
|
||||
);
|
||||
started.then(() => jobUpdate({ status: jobStatus.ACTIVE }, jobId));
|
||||
|
@ -146,6 +146,7 @@ export const JobProvider = ({ children }) => {
|
|||
const pipelineReq = {
|
||||
image: "node",
|
||||
pipeline: { __test, triggers: { ...tree[__test] } },
|
||||
isTriage: builderCache.triageFailing,
|
||||
};
|
||||
const id = `pij${Date.now()}`;
|
||||
const pipeline = { id, branches, pendingTriggers: [], selectedBranches };
|
||||
|
@ -232,6 +233,7 @@ export const JobProvider = ({ children }) => {
|
|||
image: "node",
|
||||
type: "single",
|
||||
name: jobId,
|
||||
isTriage: builderCache.isTriage,
|
||||
};
|
||||
|
||||
jobCreate(job);
|
||||
|
@ -250,7 +252,7 @@ export const JobProvider = ({ children }) => {
|
|||
jobUpdate({ ...job }, jobId);
|
||||
};
|
||||
|
||||
const started = i.newJob(request, onLog, onClose);
|
||||
const started = i.newJob(request, onLog, onClose, () => {});
|
||||
started.then(() => jobUpdate({ status: jobStatus.ACTIVE }, jobId));
|
||||
|
||||
return jobId;
|
||||
|
|
|
@ -5,7 +5,7 @@ const ACTIONS = {
|
|||
UPDATE: "u",
|
||||
};
|
||||
|
||||
const localStorage = { setItem: () => {}, getItem: () => {} };
|
||||
// const localStorage = { setItem: () => {}, getItem: () => {} };
|
||||
|
||||
const localSettings = localStorage.getItem("settings");
|
||||
const defaultSettings = {
|
||||
|
@ -14,6 +14,7 @@ const defaultSettings = {
|
|||
logAppDetails: true,
|
||||
defaultRegion: "us",
|
||||
defaultPage: "failing",
|
||||
triageFailing: true,
|
||||
};
|
||||
|
||||
const settings = localSettings ? JSON.parse(localSettings) : defaultSettings;
|
||||
|
@ -58,7 +59,7 @@ export const StoreProvider = ({ children }) => {
|
|||
name: silenceInfo.name ?? "*",
|
||||
class: silenceInfo.class ?? "*",
|
||||
method: silenceInfo.method ?? "*",
|
||||
silencedUntil: silenceInfo.silencedUntil,
|
||||
expires: silenceInfo.expires,
|
||||
};
|
||||
console.log("Would upsert silence", req);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { silencedMock } from "@qltr/mocks/alerting-mock.js";
|
||||
import { failingMock } from "@qltr/mocks/results-mock.js";
|
||||
import { testsMock, mappingsMock } from "@qltr/mocks/catalog-mock.js";
|
||||
|
||||
const QUALITEER_URL = "https://qualiteer.elijahparker3.repl.co/api";
|
||||
const QUALITEER_URL = "/api";
|
||||
|
||||
const useMock = true;
|
||||
const useMock = false;
|
||||
|
||||
const asMock = (data) => ({ data });
|
||||
|
||||
|
@ -31,3 +31,44 @@ export const useCurrentlyFailing = () =>
|
|||
useMock
|
||||
? asMock(failingMock())
|
||||
: useQuery(["failing"], fetchApi("/results/failing"));
|
||||
|
||||
export const useUpsertAlert = () => {
|
||||
const qc = useQueryClient();
|
||||
return async function upsertAlert(silenceRequest) {
|
||||
var { id, name, class: className, method, expires } = silenceRequest;
|
||||
const keepExpires = typeof expires === "string";
|
||||
name = name ? name : "*";
|
||||
className = className ? className : "*";
|
||||
method = method ? method : "*";
|
||||
const res = await fetch(`${QUALITEER_URL}/alerting/silence`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
name,
|
||||
class: className,
|
||||
method,
|
||||
expires,
|
||||
keepExpires,
|
||||
}),
|
||||
});
|
||||
qc.invalidateQueries(["silenced"]);
|
||||
};
|
||||
};
|
||||
|
||||
export const useIgnoreResult = () => {
|
||||
const qc = useQueryClient();
|
||||
return async function ignoreResult(result) {
|
||||
const { id } = result;
|
||||
const res = await fetch(`${QUALITEER_URL}/results/ignore`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ id }),
|
||||
});
|
||||
qc.invalidateQueries(["failing"]);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ export default function About() {
|
|||
</Typography>
|
||||
<Typography variant="body1">
|
||||
Qualiteer was designed to solve the issue of "on call". A state of
|
||||
being in which QA tests will fail, stiring everyone into a frenzy of
|
||||
being in which QA tests will fail, stirring everyone into a frenzy of
|
||||
what is broken in production! 🤯 Qualiteer gives users power to
|
||||
resolve and reattempt failing tests, run a particular suite of tests,
|
||||
and mute pesky alerts reminding you the navbar's color changed... 🤦♂️
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useState, useContext } from "react";
|
||||
import { useSilencedAlerts } from "@qltr/queries";
|
||||
import { useSilencedAlerts, useUpsertAlert } from "@qltr/queries";
|
||||
import StoreContext from "@qltr/store";
|
||||
import SilencedBox from "./SilencedBox.jsx";
|
||||
import SilenceDialog from "./SilenceDialog.jsx";
|
||||
import SilenceDialog, { useSilenceDialog } from "./SilenceDialog.jsx";
|
||||
|
||||
import SpeedDial from "@mui/material/SpeedDial";
|
||||
import SpeedDialAction from "@mui/material/SpeedDialAction";
|
||||
|
@ -18,41 +18,38 @@ import Typography from "@mui/material/Typography";
|
|||
import Box from "@mui/material/Box";
|
||||
|
||||
export default function Alerting() {
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const { updateStore, silenceRequest } = useContext(StoreContext);
|
||||
|
||||
const { isLoading, data: silenced } = useSilencedAlerts();
|
||||
const upsertAlert = useUpsertAlert();
|
||||
const [sdOpen, sdToggle, silence, setSilence, sdClose] = useSilenceDialog();
|
||||
const toggleDelete = () => setDeleteOpen(!deleteOpen);
|
||||
|
||||
const [silenceEntry, setSilenceEntry] = useState({
|
||||
open: false,
|
||||
deleteOpen: false,
|
||||
});
|
||||
|
||||
const closeSilence = () =>
|
||||
setSilenceEntry({ ...silenceEntry, open: false, deleteOpen: false });
|
||||
|
||||
const handleDeleteClose = (makeRequest) => () => {
|
||||
const silenceReq = { ...silenceEntry };
|
||||
closeSilence();
|
||||
if (!makeRequest) return;
|
||||
silenceRequest({ ...silenceReq, silencedUntil: null });
|
||||
const handleDeleteClose = (confirmed) => () => {
|
||||
toggleDelete();
|
||||
if (!confirmed) return;
|
||||
const silenceReq = { ...silence };
|
||||
upsertAlert({ ...silenceReq, expires: null });
|
||||
};
|
||||
|
||||
const handleClose = (silenceReq) => {
|
||||
closeSilence();
|
||||
if (!silenceReq) return;
|
||||
silenceRequest(silenceReq);
|
||||
upsertAlert(silenceReq);
|
||||
};
|
||||
|
||||
const quickAlertClick = () => {
|
||||
setSilenceEntry({ open: true, deleteOpen: false });
|
||||
setSilence({});
|
||||
sdToggle();
|
||||
};
|
||||
|
||||
const editSilence = (silence) => () => {
|
||||
setSilenceEntry({ ...silence, open: true, deleteOpen: false });
|
||||
sdToggle();
|
||||
setSilence(silence);
|
||||
};
|
||||
|
||||
const removeSilence = (silence) => () => {
|
||||
setSilenceEntry({ ...silence, deleteOpen: true, open: false });
|
||||
setSilence(silence);
|
||||
toggleDelete();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -93,7 +90,7 @@ export default function Alerting() {
|
|||
) : null}
|
||||
|
||||
<Dialog
|
||||
open={silenceEntry.deleteOpen}
|
||||
open={deleteOpen}
|
||||
onClose={handleDeleteClose()}
|
||||
sx={{ "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }}
|
||||
maxWidth="xs"
|
||||
|
@ -110,9 +107,9 @@ export default function Alerting() {
|
|||
|
||||
<SilenceDialog
|
||||
keepMounted
|
||||
open={silenceEntry.open}
|
||||
onClose={handleClose}
|
||||
silence={silenceEntry}
|
||||
open={sdOpen}
|
||||
onClose={sdClose}
|
||||
silence={silence}
|
||||
/>
|
||||
|
||||
<SpeedDial
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { useState, useContext, useEffect } from "react";
|
||||
import StoreContext from "@qltr/store";
|
||||
|
||||
import { useUpsertAlert } from "@qltr/queries";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Button from "@mui/material/Button";
|
||||
|
@ -11,25 +10,53 @@ import Dialog from "@mui/material/Dialog";
|
|||
import TextField from "@mui/material/TextField";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
|
||||
import SilenceSelector from "./SilenceSelector.jsx";
|
||||
export function useSilenceDialog(isOpen = false) {
|
||||
const [open, setOpen] = useState(isOpen);
|
||||
const [silence, setSilence] = useState({});
|
||||
const upsertAlert = useUpsertAlert();
|
||||
const dialogToggle = () => setOpen(!open);
|
||||
const dialogClose = (confirmedSilence) => {
|
||||
setOpen(false);
|
||||
if (!confirmedSilence) return;
|
||||
upsertAlert(confirmedSilence);
|
||||
setSilence({});
|
||||
};
|
||||
return [open, dialogToggle, silence, setSilence, dialogClose];
|
||||
}
|
||||
|
||||
export default function SilenceDialog(props) {
|
||||
const { silence, open, onClose } = props;
|
||||
const [duration, setDuration] = useState({ h: 0, m: 0 });
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const [silenceEntry, setSilenceEntry] = useState(silence);
|
||||
|
||||
const durationModified = duration.h !== 0 || duration.m !== 0;
|
||||
const silenceModified =
|
||||
silence.name !== silenceEntry.name ||
|
||||
silence.method !== silenceEntry.method ||
|
||||
silence.class !== silenceEntry.class;
|
||||
const modified =
|
||||
(Object.keys(silence).length > 0 && durationModified) || silenceModified;
|
||||
|
||||
useEffect(() => {
|
||||
setSilenceEntry(silence);
|
||||
setDuration({ h: 0, m: 0 });
|
||||
}, [silence, open]);
|
||||
|
||||
const { state: store, updateStore } = useContext(StoreContext);
|
||||
|
||||
const handleCancel = () => onClose();
|
||||
|
||||
const handleOk = () => onClose(silenceEntry);
|
||||
|
||||
const updateSilence = (silenceType) => (e) => {
|
||||
silenceEntry[silenceType] = e.target.value;
|
||||
setSilenceEntry({ ...silenceEntry });
|
||||
const s = { ...silenceEntry };
|
||||
s[silenceType] = e.target.value;
|
||||
setSilenceEntry({ ...s, expires: duration });
|
||||
};
|
||||
|
||||
const updateDuration = (dur) => {
|
||||
setDuration(dur);
|
||||
setSilenceEntry({ ...silenceEntry, expires: dur });
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -37,7 +64,7 @@ export default function SilenceDialog(props) {
|
|||
sx={
|
||||
fullScreen
|
||||
? {}
|
||||
: { "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }
|
||||
: { "& .MuiDialog-paper": { width: "80%", maxHeight: 525 } }
|
||||
}
|
||||
maxWidth="xs"
|
||||
open={open}
|
||||
|
@ -70,12 +97,19 @@ export default function SilenceDialog(props) {
|
|||
onChange={updateSilence("class")}
|
||||
margin="normal"
|
||||
/>
|
||||
<SilenceSelector
|
||||
formerExpires={silence.expires}
|
||||
duration={duration}
|
||||
setDuration={updateDuration}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk}>Ok</Button>
|
||||
<Button onClick={handleOk} disabled={!modified}>
|
||||
Ok
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
70
src/views/alerting/SilenceSelector.jsx
Normal file
70
src/views/alerting/SilenceSelector.jsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { useState } from "react";
|
||||
import moment from "moment";
|
||||
import Box from "@mui/material/Box";
|
||||
import TextField from "@mui/material/TextField";
|
||||
|
||||
const MAX_DURATION = { h: 72, m: 59 };
|
||||
const MIN_DURATION = 0;
|
||||
export default function SilenceSelector(props) {
|
||||
const [expires, setExpires] = useState(moment());
|
||||
const { formerExpires, duration, setDuration } = props;
|
||||
const formerExpirey =
|
||||
formerExpires && duration.h === 0 && duration.m === 0
|
||||
? moment(formerExpires)
|
||||
: null;
|
||||
const updateDuration = (t) => (e) => {
|
||||
const d = { ...duration };
|
||||
d[t] = Math.abs(parseInt(e.target.value));
|
||||
if (isNaN(d[t])) d[t] = 0;
|
||||
d[t] = d[t] <= MAX_DURATION[t] ? d[t] : MAX_DURATION[t];
|
||||
setDuration(d);
|
||||
const now = moment().add(d.h, "hours").add(d.m, "minutes");
|
||||
setExpires(now);
|
||||
};
|
||||
const onNumberInput = (e) =>
|
||||
(e.target.value =
|
||||
!!e.target.value && Math.abs(e.target.value) >= MIN_DURATION
|
||||
? Math.abs(e.target.value)
|
||||
: null);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box style={{ display: "flex" }}>
|
||||
<TextField
|
||||
margin="normal"
|
||||
label="Hours"
|
||||
type="number"
|
||||
onChange={updateDuration("h")}
|
||||
inputProps={{
|
||||
min: MIN_DURATION,
|
||||
max: MAX_DURATION.h,
|
||||
onInput: onNumberInput,
|
||||
}}
|
||||
value={duration.h}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
sx={{ marginLeft: 1 }}
|
||||
label="Minutes"
|
||||
type="number"
|
||||
onChange={updateDuration("m")}
|
||||
inputProps={{
|
||||
min: MIN_DURATION,
|
||||
max: MAX_DURATION.m,
|
||||
onInput: onNumberInput,
|
||||
}}
|
||||
value={duration.m}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Box>
|
||||
<TextField
|
||||
margin="normal"
|
||||
label="Expires"
|
||||
value={`${(formerExpirey ?? expires).format("L")} ${(
|
||||
formerExpirey ?? expires
|
||||
).format("LT")}`}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useContext } from "react";
|
||||
import moment from "moment";
|
||||
import StoreContext from "@qltr/store";
|
||||
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
|
@ -18,7 +19,7 @@ export default function SilencingBox(props) {
|
|||
method: testMethod,
|
||||
class: testClass,
|
||||
id: silenceId,
|
||||
silencedUntil,
|
||||
expires,
|
||||
} = silenceEntry;
|
||||
|
||||
const { state: store, updateStore } = useContext(StoreContext);
|
||||
|
@ -61,11 +62,19 @@ export default function SilencingBox(props) {
|
|||
<br />
|
||||
{`Test Class: ${testClass}`}
|
||||
<br />
|
||||
{`Silenced Until: ${silencedUntil} Remaining Time: 2:50`}
|
||||
{`Silenced Until: ${expires} Remaining Time: ${moment(
|
||||
moment(expires).diff(moment())
|
||||
).format("HH:mm")}`}
|
||||
</Typography>
|
||||
|
||||
<Stack
|
||||
sx={{ ml: "auto", display: { md: "none", lg: "none", xl: "none" } }}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
mb: "auto",
|
||||
mt: "auto",
|
||||
whiteSpace: "nowrap",
|
||||
display: { md: "none", lg: "none", xl: "none" },
|
||||
}}
|
||||
>
|
||||
<Actions />
|
||||
</Stack>
|
||||
|
|
|
@ -9,7 +9,7 @@ import TextField from "@mui/material/TextField";
|
|||
export default function Catalog() {
|
||||
const { state: store, updateStore } = useContext(StoreContext);
|
||||
const { state: jobState } = useContext(JobContext);
|
||||
const { isLoading, data: tests } = useCatalogTests();
|
||||
const { isLoading, data: rawTests } = useCatalogTests();
|
||||
const handleSearchChange = (e) =>
|
||||
updateStore({ catalogSearch: e.target.value });
|
||||
|
||||
|
@ -22,6 +22,8 @@ export default function Catalog() {
|
|||
}, []);
|
||||
|
||||
const catalogWithJobs = () => {
|
||||
const tests = rawTests.map((t) => ({ ...t, isPipeline: t.pipeline }));
|
||||
for (var t of tests) delete t.pipeline;
|
||||
for (var test of tests) {
|
||||
if (test.isPipeline) {
|
||||
const pipeline = jobState.pipelines.find((p) =>
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
useJobNav,
|
||||
} from "@qltr/util/JobTools";
|
||||
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
|
@ -40,8 +42,9 @@ export default function CatalogBox(props) {
|
|||
const { jobFactory } = useContext(JobContext);
|
||||
const jobNav = useJobNav();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const toggleOpen = () => setOpen(!open);
|
||||
const theme = useTheme();
|
||||
const minifyActions = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const navigateToJob = () => {
|
||||
if (pipeline) return jobNav.toPipeline(pipeline.id);
|
||||
|
@ -50,7 +53,10 @@ export default function CatalogBox(props) {
|
|||
|
||||
const runTest = () => {
|
||||
if (isPipeline) return runPipelineTest();
|
||||
const jobId = jobFactory({ testNames: [testName], isTriage: true });
|
||||
const jobId = jobFactory({
|
||||
testNames: [testName],
|
||||
isTriage: store.triageFailing,
|
||||
});
|
||||
if (store.focusJob) jobNav.toJob(jobId);
|
||||
};
|
||||
|
||||
|
@ -86,21 +92,6 @@ export default function CatalogBox(props) {
|
|||
return useJobIconState(job);
|
||||
}
|
||||
|
||||
function Actions() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<IconButton
|
||||
color="success"
|
||||
aria-label="play"
|
||||
component="span"
|
||||
onClick={jobOnClick}
|
||||
>
|
||||
{jobIcon()}
|
||||
</IconButton>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
expanded={open}
|
||||
|
@ -129,20 +120,18 @@ export default function CatalogBox(props) {
|
|||
</Box>
|
||||
<br />
|
||||
</Typography>
|
||||
|
||||
<Stack
|
||||
sx={{ ml: "auto", display: { md: "none", lg: "none", xl: "none" } }}
|
||||
direction={minifyActions ? "column" : "row"}
|
||||
sx={{ ml: "auto", mb: "auto", mt: "auto", whiteSpace: "nowrap" }}
|
||||
>
|
||||
<Actions />
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
ml: "auto",
|
||||
display: { xs: "none", sm: "none", md: "flex", lg: "flex" },
|
||||
}}
|
||||
>
|
||||
<Actions />
|
||||
<IconButton
|
||||
color="success"
|
||||
aria-label="play"
|
||||
component="span"
|
||||
onClick={jobOnClick}
|
||||
>
|
||||
{jobIcon()}
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
|
|
|
@ -1,135 +1,103 @@
|
|||
import { useState, useContext } from "react";
|
||||
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 SilenceDialog, { useSilenceDialog } from "../alerting/SilenceDialog.jsx";
|
||||
import FailingBox from "./FailingBox.jsx";
|
||||
import QuickSilence, { useQuickSilence } from "./QuickSilence.jsx";
|
||||
import FailingRetry from "./FailingRetry.jsx";
|
||||
|
||||
import SpeedDial from "@mui/material/SpeedDial";
|
||||
|
||||
import Button from "@mui/material/Button";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import ReplayIcon from "@mui/icons-material/Replay";
|
||||
// MaterialUI
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
export default function Failing() {
|
||||
const { state: jobState, retryAll } = useContext(JobContext);
|
||||
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 { state: jobState } = useContext(JobContext);
|
||||
const { isLoading: failsLoading, data: failing } = useCurrentlyFailing();
|
||||
const { isLoading: silencesLoading, data: silences } = useSilencedAlerts();
|
||||
const [qsAnchor, qsTest, openQs, closeQs] = useQuickSilence();
|
||||
const [sdOpen, sdToggle, silence, setSilence, sdClose] = useSilenceDialog();
|
||||
|
||||
const closeSilence = () => setSilenceEntry({ ...silenceEntry, open: false });
|
||||
|
||||
const handleSilenceClose = (silenceReq) => {
|
||||
closeSilence();
|
||||
if (!silenceReq) return;
|
||||
silenceRequest(silenceReq);
|
||||
const joinSilence = (result) => {
|
||||
if (!silences) return;
|
||||
const silence = silences.find((s) => s.name === result.name);
|
||||
if (silence) result.silence = silence;
|
||||
};
|
||||
|
||||
const editSilence = (silence) => () => {
|
||||
setSilenceEntry({ ...silence, open: true });
|
||||
const joinJob = (result) => {
|
||||
if (result.isPipeline) return;
|
||||
const job = jobState.jobs.find(
|
||||
(j) => !j.isPipeline && j.builderCache.testNames.includes(result.name)
|
||||
);
|
||||
if (job) result.job = job;
|
||||
};
|
||||
|
||||
const [retryAllOpen, setRetryAllOpen] = useState(false);
|
||||
const retryAllClick = () => setRetryAllOpen(!retryAllOpen);
|
||||
|
||||
const handleClose = (confirmed) => () => {
|
||||
retryAllClick();
|
||||
if (!confirmed) return;
|
||||
const jobId = retryAll(store.failing);
|
||||
|
||||
if (!store.focusJob) return;
|
||||
jobNav.toJob(jobId);
|
||||
const joinPipeline = (result) => {
|
||||
if (!result.isPipeline) return;
|
||||
const pipeline = jobState.pipelines.find((p) =>
|
||||
p.selectedBranches.find((b) => b.name === result.name)
|
||||
);
|
||||
if (!pipeline) return;
|
||||
const pipelineJob = jobState.jobs.find(
|
||||
(j) =>
|
||||
j.isPipeline &&
|
||||
j.pipelineId === pipeline.id &&
|
||||
j.branchId === result.name
|
||||
);
|
||||
if (!pipelineJob) result.pipeline = pipeline;
|
||||
else result.job = pipelineJob;
|
||||
};
|
||||
|
||||
const failingTestsWithJobs = () => {
|
||||
if (isLoading) return [];
|
||||
const silences = silencedAlerts ?? [];
|
||||
for (var test of failing) {
|
||||
const silence = silences.find(
|
||||
(s) => s.name === test.name || s.class === test.class
|
||||
);
|
||||
|
||||
if (silence) test.silencedUntil = silence;
|
||||
if (test.isPipeline) {
|
||||
const pipeline = jobState.pipelines.find((p) =>
|
||||
p.selectedBranches.find((b) => b.name === test.name)
|
||||
);
|
||||
if (!pipeline) continue;
|
||||
const pipelineJob = jobState.jobs.find(
|
||||
(j) =>
|
||||
j.isPipeline &&
|
||||
j.pipelineId === pipeline.id &&
|
||||
j.branchId === test.name
|
||||
);
|
||||
if (!pipelineJob) test.pipeline = pipeline;
|
||||
test.job = pipelineJob;
|
||||
continue;
|
||||
}
|
||||
const job = jobState.jobs.find(
|
||||
(j) => !j.isPipeline && j.builderCache.testNames.includes(test.name)
|
||||
);
|
||||
if (job) test.job = job;
|
||||
const resultsCompiled = () => {
|
||||
if (failsLoading) return [];
|
||||
const compiled = [];
|
||||
for (var r of failing) {
|
||||
const rc = { ...r };
|
||||
joinSilence(rc);
|
||||
// Find associated job and skip pipeline
|
||||
joinJob(rc);
|
||||
// Find associated pipeline
|
||||
joinPipeline(rc);
|
||||
compiled.push(rc);
|
||||
}
|
||||
return failing;
|
||||
return compiled;
|
||||
};
|
||||
|
||||
const alertClick = (result) => (e) => {
|
||||
if (e) e.preventDefault();
|
||||
if (e && e.type === "contextmenu" && result.silence)
|
||||
return openQs(e, result);
|
||||
const { name, class: className, method } = result;
|
||||
const testInfo = { name, class: className, method };
|
||||
const silenceInfo = result.silence ?? testInfo;
|
||||
sdToggle();
|
||||
setSilence(silenceInfo);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="failing">
|
||||
{isLoading
|
||||
? null
|
||||
: failingTestsWithJobs().map((v, i) => (
|
||||
<FailingBox key={i} failingTest={v} silenceClick={editSilence(v)} />
|
||||
))}
|
||||
|
||||
<Dialog
|
||||
open={retryAllOpen}
|
||||
onClose={handleClose()}
|
||||
sx={{ "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<DialogTitle>Retry all failing tests?</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
This will create x jobs and run y tests
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose()}>Cancel</Button>
|
||||
<Button onClick={handleClose(true)} autoFocus>
|
||||
Yes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<SilenceDialog
|
||||
keepMounted
|
||||
open={silenceEntry.open}
|
||||
onClose={handleSilenceClose}
|
||||
silence={silenceEntry}
|
||||
open={sdOpen}
|
||||
onClose={sdClose}
|
||||
silence={silence}
|
||||
/>
|
||||
{(failing ?? []).length === 0 ? (
|
||||
<QuickSilence
|
||||
anchorEl={qsAnchor}
|
||||
handleClose={closeQs}
|
||||
test={qsTest}
|
||||
editSilence={alertClick(qsTest)}
|
||||
/>
|
||||
<FailingRetry failing={failsLoading ? [] : failing} />
|
||||
{!failsLoading && failing.length === 0 && (
|
||||
<Box display="flex" alignItems="center" justifyContent="center">
|
||||
<Typography variant="h4">No tests failing!</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{(failing ?? []).length === 0 ? null : (
|
||||
<SpeedDial
|
||||
ariaLabel="Retry All"
|
||||
sx={{ position: "fixed", bottom: 16, right: 16 }}
|
||||
icon={<ReplayIcon />}
|
||||
onClick={retryAllClick}
|
||||
open={false}
|
||||
/>
|
||||
)}
|
||||
{!failsLoading &&
|
||||
resultsCompiled().map((v, i) => (
|
||||
<FailingBox key={i} failingTest={v} alertClick={alertClick(v)} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useContext } from "react";
|
||||
import { usePipelineMappings } from "@qltr/queries";
|
||||
import { usePipelineMappings, useIgnoreResult } from "@qltr/queries";
|
||||
import StoreContext from "@qltr/store";
|
||||
import JobContext, { jobStatus } from "@qltr/jobs";
|
||||
import {
|
||||
|
@ -7,7 +7,8 @@ import {
|
|||
usePipelineIconState,
|
||||
useJobNav,
|
||||
} from "@qltr/util/JobTools";
|
||||
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
|
@ -42,30 +43,33 @@ import { asTree, asBranches, as1d } from "@qltr/util/pipelines.js";
|
|||
const stopPropagation = (e) => e.stopPropagation() && e.preventDefault();
|
||||
|
||||
export default function FailingBox(props) {
|
||||
const { failingTest, silenceClick } = props;
|
||||
const { failingTest, alertClick } = props;
|
||||
const {
|
||||
class: testClass,
|
||||
name: testName,
|
||||
timestamp,
|
||||
silencedUntil,
|
||||
silence,
|
||||
type,
|
||||
dailyFails,
|
||||
screenshot: screenshotUrl,
|
||||
console: cs,
|
||||
recentResults,
|
||||
failedMessage,
|
||||
message,
|
||||
isPipeline,
|
||||
job,
|
||||
pipeline,
|
||||
} = failingTest;
|
||||
const ignoreResult = useIgnoreResult();
|
||||
const runHistory = recentResults ? [...recentResults].reverse() : null;
|
||||
|
||||
const { data: pipelineMappings, isLoading } = usePipelineMappings();
|
||||
const { jobFactory } = useContext(JobContext);
|
||||
const { state: store, updateStore, removeFailure } = useContext(StoreContext);
|
||||
const jobNav = useJobNav();
|
||||
|
||||
const theme = useTheme();
|
||||
const minifyActions = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const [open, setOpen] = useState(false);
|
||||
const toggleOpen = () => setOpen(!open);
|
||||
|
||||
const [removeOpen, setRemoveOpen] = useState(false);
|
||||
const removeClick = () => setRemoveOpen(!removeOpen);
|
||||
|
||||
|
@ -73,7 +77,7 @@ export default function FailingBox(props) {
|
|||
stopPropagation(e);
|
||||
setRemoveOpen(false);
|
||||
if (!confirmed) return;
|
||||
removeFailure(failingTest);
|
||||
ignoreResult(failingTest);
|
||||
};
|
||||
|
||||
function badgeColor() {
|
||||
|
@ -91,7 +95,7 @@ export default function FailingBox(props) {
|
|||
branches: asBranches(primaries),
|
||||
tree: asTree(primaries),
|
||||
selectedBranches: as1d(primaries),
|
||||
isTriage: true,
|
||||
isTriage: store.triageFailing,
|
||||
};
|
||||
const pipeline = jobFactory(builderCache);
|
||||
if (store.focusJob) jobNav.toPipeline(pipeline.id);
|
||||
|
@ -99,7 +103,10 @@ export default function FailingBox(props) {
|
|||
|
||||
const retryTest = () => {
|
||||
if (isPipeline) return retryPipelineTest();
|
||||
const jobId = jobFactory({ testNames: [testName], isTriage: true });
|
||||
const jobId = jobFactory({
|
||||
testNames: [testName],
|
||||
isTriage: store.triageFailing,
|
||||
});
|
||||
if (store.focusJob) jobNav.toJob(jobId);
|
||||
};
|
||||
|
||||
|
@ -120,39 +127,6 @@ export default function FailingBox(props) {
|
|||
return useJobIconState(job);
|
||||
}
|
||||
|
||||
function Actions() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<a href={screenshotUrl}>
|
||||
<IconButton aria-label="photo" component="span">
|
||||
<PhotoCameraIcon />
|
||||
</IconButton>
|
||||
</a>
|
||||
|
||||
<IconButton aria-label="retry" component="span" onClick={jobOnClick}>
|
||||
{jobIcon()}
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
aria-label="silence"
|
||||
component="span"
|
||||
color={silencedUntil ? "primary" : "default"}
|
||||
onClick={silenceClick}
|
||||
>
|
||||
<NotificationsIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="error"
|
||||
aria-label="delete"
|
||||
component="span"
|
||||
onClick={removeClick}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
expanded={open}
|
||||
|
@ -212,12 +186,23 @@ export default function FailingBox(props) {
|
|||
<br />
|
||||
<div>
|
||||
<span className="recent-results">
|
||||
{recentResults.map(
|
||||
(v, i) =>
|
||||
(v && <CheckIcon key={i} color="success" />) || (
|
||||
<ClearIcon key={i} color="error" />
|
||||
)
|
||||
)}
|
||||
{runHistory &&
|
||||
runHistory.map(
|
||||
(v, i) =>
|
||||
(!v.failed && (
|
||||
<CheckIcon
|
||||
key={i}
|
||||
color="success"
|
||||
titleAccess={v.timestamp}
|
||||
/>
|
||||
)) || (
|
||||
<ClearIcon
|
||||
key={i}
|
||||
color="error"
|
||||
titleAccess={v.timestamp}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
{isPipeline && <ViewColumnIcon />}
|
||||
</div>
|
||||
|
@ -225,27 +210,41 @@ export default function FailingBox(props) {
|
|||
|
||||
<Stack
|
||||
onClick={stopPropagation}
|
||||
sx={{ ml: "auto", display: { md: "none", lg: "none", xl: "none" } }}
|
||||
direction={minifyActions ? "column" : "row"}
|
||||
sx={{ ml: "auto", mb: "auto", mt: "auto", whiteSpace: "nowrap" }}
|
||||
>
|
||||
<Actions />
|
||||
</Stack>
|
||||
<Stack
|
||||
onClick={stopPropagation}
|
||||
direction="row"
|
||||
sx={{
|
||||
ml: "auto",
|
||||
mb: "auto",
|
||||
mt: "auto",
|
||||
whiteSpace: "nowrap",
|
||||
display: { xs: "none", sm: "none", md: "block", lg: "block" },
|
||||
}}
|
||||
>
|
||||
<Actions />
|
||||
<a href={screenshotUrl}>
|
||||
<IconButton aria-label="photo" component="span">
|
||||
<PhotoCameraIcon />
|
||||
</IconButton>
|
||||
</a>
|
||||
|
||||
<IconButton aria-label="retry" component="span" onClick={jobOnClick}>
|
||||
{jobIcon()}
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
aria-label="silence"
|
||||
component="span"
|
||||
color={silence && silence.expires ? "primary" : "default"}
|
||||
onClick={alertClick}
|
||||
onContextMenu={alertClick}
|
||||
>
|
||||
<NotificationsIcon titleAccess={silence && silence.expires} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="error"
|
||||
aria-label="delete"
|
||||
component="span"
|
||||
onClick={removeClick}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography component={"span"} style={{ wordBreak: "break-word" }}>
|
||||
{failedMessage}
|
||||
{message}
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
|
64
src/views/failing/FailingRetry.jsx
Normal file
64
src/views/failing/FailingRetry.jsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
// React
|
||||
import React, { useState, useContext } from "react";
|
||||
import JobContext from "@qltr/jobs";
|
||||
import StoreContext from "@qltr/store";
|
||||
import { useJobNav } from "@qltr/util/JobTools";
|
||||
|
||||
// Components
|
||||
import Button from "@mui/material/Button";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import SpeedDial from "@mui/material/SpeedDial";
|
||||
|
||||
// Icons
|
||||
import ReplayIcon from "@mui/icons-material/Replay";
|
||||
|
||||
export default function FailingRetry(props) {
|
||||
const { failing } = props;
|
||||
const { state: jobState, retryAll } = useContext(JobContext);
|
||||
const { state: store } = useContext(StoreContext);
|
||||
const [open, setOpen] = useState(false);
|
||||
const jobNav = useJobNav();
|
||||
const toggleOpen = () => setOpen(!open);
|
||||
const dialogClose = (confirmed) => () => {
|
||||
toggleOpen();
|
||||
if (!confirmed) return;
|
||||
const jobId = retryAll(failing);
|
||||
if (!store.focusJob) return;
|
||||
jobNav.toJob(jobId);
|
||||
};
|
||||
if (!failing || failing.length === 0) return;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={dialogClose()}
|
||||
sx={{ "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<DialogTitle>Retry all failing tests?</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
This will create x jobs and run y tests
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={dialogClose()}>Cancel</Button>
|
||||
<Button onClick={dialogClose(true)} autoFocus>
|
||||
Yes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<SpeedDial
|
||||
ariaLabel="Retry All"
|
||||
sx={{ position: "fixed", bottom: 16, right: 16 }}
|
||||
icon={<ReplayIcon />}
|
||||
onClick={toggleOpen}
|
||||
open={false}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
58
src/views/failing/QuickSilence.jsx
Normal file
58
src/views/failing/QuickSilence.jsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
// React
|
||||
import { useState, useContext } from "react";
|
||||
import { useUpsertAlert } from "@qltr/queries";
|
||||
|
||||
// Components
|
||||
import Popover from "@mui/material/Popover";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
|
||||
//Icons
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
|
||||
export function useQuickSilence() {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [test, setTest] = useState(null);
|
||||
const openMenuAt = (e, t) => {
|
||||
e.preventDefault();
|
||||
setTest(t);
|
||||
setAnchorEl(e.currentTarget);
|
||||
};
|
||||
const closeMenu = () => setAnchorEl(null);
|
||||
return [anchorEl, test, openMenuAt, closeMenu];
|
||||
}
|
||||
|
||||
export default function QuickSilence(props) {
|
||||
const { anchorEl, test, handleClose, editSilence } = props;
|
||||
const upsertAlert = useUpsertAlert();
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
async function deleteClick() {
|
||||
await upsertAlert({ id: test.silence.id, expires: null });
|
||||
handleClose();
|
||||
}
|
||||
|
||||
async function editClick() {
|
||||
editSilence();
|
||||
handleClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
<IconButton color="primary" onClick={editClick}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton color="error" onClick={deleteClick}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Popover>
|
||||
);
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useContext } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import JobContext, { jobStatus } from "@qltr/jobs";
|
||||
import {
|
||||
selectedPipelineBranches,
|
||||
|
@ -37,6 +38,7 @@ function JobPipelineDisplay(props) {
|
|||
} = useContext(JobContext);
|
||||
|
||||
const jobNav = useJobNav();
|
||||
const nav = useNavigate();
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
@ -92,7 +94,7 @@ function JobPipelineDisplay(props) {
|
|||
<Toolbar disableGutters />
|
||||
<Box sx={{ flexGrow: 1, margin: "0 10px" }}>
|
||||
<Toolbar disableGutters>
|
||||
<IconButton onClick={jobNav.toJobs}>
|
||||
<IconButton onClick={() => nav(-1)}>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" sx={{ ml: "auto", mr: "auto" }}>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { useJobNav } from "@qltr/util/JobTools";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import JobContext, { jobStatus } from "@qltr/jobs";
|
||||
import StoreContext from "@qltr/store";
|
||||
import Box from "@mui/material/Box";
|
||||
|
@ -27,6 +28,7 @@ export default function JobView(props) {
|
|||
const { jobFactory, jobCancel, jobDestroy } = useContext(JobContext);
|
||||
const { state: store } = useContext(StoreContext);
|
||||
const jobNav = useJobNav();
|
||||
const nav = useNavigate();
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClick = (event) => {
|
||||
|
@ -91,7 +93,7 @@ export default function JobView(props) {
|
|||
<Toolbar disableGutters />
|
||||
<Box sx={{ flexGrow: 1, margin: "0 10px" }}>
|
||||
<Toolbar disableGutters>
|
||||
<IconButton onClick={navigateToJobs}>
|
||||
<IconButton onClick={() => nav(-1)}>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" component="span" sx={{ ml: "auto" }}>
|
||||
|
|
|
@ -52,7 +52,7 @@ export default function JobBuilder() {
|
|||
const handleClose = (confirmed) => () => {
|
||||
setJobDialogOpen(false);
|
||||
if (!confirmed) return;
|
||||
const jobId = jobFactory(cache);
|
||||
const jobId = jobFactory({ ...cache, isTriage: store.triageFailing });
|
||||
if (store.focusJob) jobNav.toJob(jobId);
|
||||
};
|
||||
|
||||
|
|
|
@ -115,6 +115,11 @@ export default function Settings(props) {
|
|||
<Switch edge="end" checked={store.focusJob} />
|
||||
</ListItem>
|
||||
|
||||
<ListItem button divider onClick={handleToggle("triageFailing")}>
|
||||
<ListItemText primary="Triage Failing" />
|
||||
<Switch edge="end" checked={store.triageFailing} />
|
||||
</ListItem>
|
||||
|
||||
<ListItem button divider onClick={handleToggle("logAppDetails")}>
|
||||
<ListItemText primary="Log App Details" />
|
||||
<Switch edge="end" checked={store.logAppDetails} />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue