Microsave

This commit is contained in:
Dunemask 2022-05-23 00:24:21 +00:00
parent 02c483950c
commit d94796173e
17 changed files with 735 additions and 228 deletions

View file

@ -1,3 +1,4 @@
<<<<<<< HEAD
CREATE SEQUENCE test_results_id_seq;
CREATE TABLE test_results (
id bigint NOT NULL DEFAULT nextval('test_results_seq') PRIMARY KEY,
@ -16,15 +17,29 @@ weblog_url varchar(255) DEFAULT NULL,
);
ALTER SEQUENCE test_results_id_seq OWNED BY test_results.id;
=======
> > > > > > > b023d8910c89d80573499890a958c0df649849e1
# Tables
PG Database Tables Mapped Out
<<<<<<< HEAD
## `test_results`
| id | test_name | test_class | test_method | test_path | test_type | test_timestamp | test_retry | origin | failed | failed_message | screenshot_url | weblog_url |
| int | string | string | string | string | string | timestamp | boolean | string | boolean | string | string | string |
| 1 | My Test | My Test Class | My Failing Test Method | My Test Class Path | API | Date.now() | false | Test Suite A | true | Some Failure Messsage | screenshotUrl | weblogUrl |
=======
Table `test_results`
| id | test_name | test_class | test_method | test_path | test_type | test_timestamp | test_retry | origin | failed | failed_message | screenshot_url | weblog_url |
|-----|-----------|------------|-------------|---------------------|---------------|----------------|------------|--------------|---------|------------------------|---------------------|--------------------|
| int | string | string | string | string | string | timestamp | boolean | string | boolean | string | string | string |
| 1 | My Test | My Class | My Method | /path/to/test_class | API/UI/MOBILE | Date.now() | false | Test Suite A | true | I am a test that fails | https://example.com | http://example.com |
> > > > > > > b023d8910c89d80573499890a958c0df649849e1
- id Automatically Generated
- test_name\* Name of test

View file

@ -23,7 +23,7 @@
"start:dev": "nodemon dist/app.js",
"start:dev:replit": "npm run start:dev & npm run start:react:replit",
"start:react": "react-scripts start",
"start:react:replit": "DANGEROUSLY_DISABLE_HOST_CHECK=true npm run start:react",
"start:react:replit": "DANGEROUSLY_DISABLE_HOST_CHECK=true node --max-old-space-size=512 node_modules/.bin/react-scripts start",
"test": "node tests/index.js",
"test:api": "node tests/api.js",
"test:dev": "nodemon tests/index.js"

View file

@ -127,7 +127,11 @@ export default function Views() {
>
{navHeader()}
</Typography>
<Avatar alt="Remy Sharp" src="/assets/QA.png" onClick={reloadPage}/>
<Avatar
alt="Remy Sharp"
src="/assets/QA.png"
onClick={reloadPage}
/>
</Toolbar>
</Box>
</AppBar>

View file

@ -1,6 +1,15 @@
import React, { useReducer, createContext, useMemo } from "react";
const JobContext = createContext();
export const jobStatus = {
OK: "o",
QUEUED: "q",
PENDING: "p",
CANCELED: "c",
ACTIVE: "a",
ERROR: "e",
};
const ACTIONS = {
CREATE: "c",
UPDATE: "u",
@ -10,6 +19,22 @@ const ACTIONS = {
const initialState = {
jobs: [],
};
/*
{
name: "Job1",
test: "someTestName",
status: JOB_STATUS.SUCCESS,
exitcode: 0
}
OR
{
compound: true,
name: "Compound Job",
pipeline: [{}]
}
*/
const reducer = (state, action) => {
// Current Jobs
@ -36,32 +61,35 @@ const reducer = (state, action) => {
}
};
export const JobProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const jobUpdate = (job, jobId) => dispatch({ type: ACTIONS.UPDATE, jobId, job });
const jobUpdate = (job, jobId) =>
dispatch({ type: ACTIONS.UPDATE, jobId, job });
const jobCreate = (job) =>
dispatch({ type: ACTIONS.CREATE, job: { ...job, log: [] } });
dispatch({ type: ACTIONS.CREATE, job: { ...job, log: [] } });
const jobDelete = (jobId) => dispatch({ type: ACTIONS.DELETE, jobId });
function retryAll(failing){
// Query Full Locator
console.log("Would retry all failing tests!");
}
function retryAll(failing) {
// Query Full Locator
console.log("Would retry all failing tests!");
}
function activeJobStates() {
const jobs = { state };
console.log("Would return all active job states");
}
function jobBuilder() {}
function jobBuilder(){
}
const context = {
state,
dispatch,
jobUpdate,
jobCreate,
jobDelete,
retryAll
retryAll,
activeJobStates,
};
const contextValue = useMemo(() => context, [state, dispatch]);

View file

@ -1,14 +1,50 @@
import React, { useReducer, createContext, useMemo } from "react";
import { jobStatus } from "./JobContext.jsx";
const StoreContext = createContext();
const ACTIONS = {
UPDATE: "u",
};
const failingMock = new Array(10).fill(0).map((v, i) => ({
class: `SomeTestClass${i % 2 ? i - 1 : i / 2}`,
name: `TestThatDoesOneThing${i + 1}`,
timestamp: `2022-05-10T16:${2 + i}:33.810Z`,
silencedUntil: i % 4 ? null : `2022-05-10T16:${2 + i}:33.810Z`,
frequency: "1hour",
type: i % 3 ? "api" : "ui",
dailyFails: i + 1,
screenshot: "https://example.com",
recentResults: [1, 0, 0, 1, 0],
isCompound: i % 5 ? false : true,
failedMessage: `Some Test FailureMessage ${i}`,
jobStatus: (() => {
switch (i) {
case 1:
return jobStatus.OK;
case 3:
return jobStatus.ERROR;
case 4:
return jobStatus.PENDING;
case 5:
return jobStatus.ACTIVE;
case 6:
return jobStatus.CANCELED;
case 8:
return jobStatus.QUEUED;
default:
return null;
}
})(),
}));
const initialState = {
intervals: [],
failing: [],
catalog: [],
failing: failngMock,
regions: [],
catalogSearch: "",
focusJob: false,
simplifiedControls: false,
defaultRegion: "us", // Local Store

View file

@ -11,22 +11,36 @@ export default function About() {
<div className="about">
<Container maxWidth="sm">
<Typography variant="h6" gutterBottom component="div">
<Box fontWeight='bold' display='inline'>Why?</Box>
</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 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... 🤦
</Typography>
<br/>
<Typography variant="subtitle1" style={{ wordWrap: "break-word", whiteSpace:"normal" }}>
<Box fontWeight='bold' display='inline'>{"Repository: "} </Box>
<Link href={repoUrl} >{repoUrl}</Link>
</Typography>
<br/>
<div style={{justifyContent:"center", width:"100%", display:"flex"}}>
<Link href={memeUrl} variant="h6" underline="none">Qualiteer</Link>
</div>
</Container>
</div>
);
<Box fontWeight="bold" display="inline">
Why?
</Box>
</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
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... 🤦
</Typography>
<br />
<Typography
variant="subtitle1"
style={{ wordWrap: "break-word", whiteSpace: "normal" }}
>
<Box fontWeight="bold" display="inline">
{"Repository: "}
</Box>
<Link href={repoUrl}>{repoUrl}</Link>
</Typography>
<br />
<div
style={{ justifyContent: "center", width: "100%", display: "flex" }}
>
<Link href={memeUrl} variant="h6" underline="none">
Qualiteer
</Link>
</div>
</Container>
</div>
);
}

View file

@ -1,16 +1,16 @@
import { useState, useContext } from "react";
import StoreContext from "../ctx/StoreContext.jsx";
import SpeedDial from '@mui/material/SpeedDial';
import SpeedDialAction from '@mui/material/SpeedDialAction';
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
import SpeedDial from "@mui/material/SpeedDial";
import SpeedDialAction from "@mui/material/SpeedDialAction";
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
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 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";
export default function Alerting() {
const { state: store, updateStore } = useContext(StoreContext);
@ -18,29 +18,23 @@ export default function Alerting() {
const [alertDialogOpen, setAlertDialogOpen] = useState(false);
const quickAlertClick = () => setAlertDialogOpen(!alertDialogOpen);
function silenceAlert(){
}
function silenceAlert() {}
const handleClose = (confirmed) => () => {
quickAlertClick();
if(!confirmed) return;
silenceAlert();
}
quickAlertClick();
if (!confirmed) return;
silenceAlert();
};
return (
<div className="alerting">
<Dialog
open={alertDialogOpen}
onClose={handleClose()}
sx={{ '& .MuiDialog-paper': { width: '80%', maxHeight: 435 } }}
maxWidth="xs"
sx={{ "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }}
maxWidth="xs"
>
<DialogTitle>
Silence Alert
</DialogTitle>
<DialogContent>
</DialogContent>
<DialogTitle>Silence Alert</DialogTitle>
<DialogContent></DialogContent>
<DialogActions>
<Button onClick={handleClose()}>Cancel</Button>
<Button onClick={handleClose(true)} autoFocus>
@ -50,8 +44,8 @@ export default function Alerting() {
</Dialog>
<SpeedDial
ariaLabel="Silence Alert"
sx={{ position: 'absolute', bottom: 16, right: 16 }}
ariaLabel="Silence Alert"
sx={{ position: "fixed", bottom: 16, right: 16 }}
icon={<SpeedDialIcon />}
onClick={quickAlertClick}
open={false}

View file

@ -1,4 +1,4 @@
import { useContext } from "react";
import { useEffect, useContext } from "react";
import StoreContext from "../ctx/StoreContext.jsx";
import JobContext from "../ctx/JobContext.jsx";
@ -16,14 +16,25 @@ export default function Catalog() {
const { state: store, updateStore } = useContext(StoreContext);
const handleSearchChange = (e) =>
updateStore({ catalogSearch: e.target.value });
const handleSearchClear = () => updateStore({ catalogSearch: "" });
useEffect(() => {
return function unmount() {
handleSearchClear();
};
}, []);
return (
<div className="catalog">
<CatalogSearch />
<TextField
label="Search Catalog"
type="search"
variant="filled"
/>
<CatalogSearch
onChange={handleSearchChange}
onClear={handleSearchClear}
clearOnUnmount
/>
<h6>{store.catalogSearch}</h6>
</div>
);
}

View file

@ -2,48 +2,55 @@ import { useState, useContext } from "react";
import StoreContext from "../ctx/StoreContext.jsx";
import JobContext from "../ctx/JobContext.jsx";
import SpeedDial from '@mui/material/SpeedDial';
import SpeedDialAction from '@mui/material/SpeedDialAction';
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
import SpeedDial from "@mui/material/SpeedDial";
import SpeedDialAction from "@mui/material/SpeedDialAction";
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
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 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';
import ReplayIcon from "@mui/icons-material/Replay";
import FailingBox from "./components/FailingBox.jsx";
export default function Failing() {
const {
state: jobState,
retryAll
} = useContext(JobContext);
const { state: jobState, retryAll, activeJobStates } = useContext(JobContext);
const { state: store, updateStore } = useContext(StoreContext);
const { failing } = store;
/* TODO
for(var j of activeJobStates()){
const failingTest = failing.find((f)=>f.name===j.testName);
if(!failingTest) continue;
failingTest.jobStatus= j.status;
}*/
const [retryAllOpen, setRetryAllOpen] = useState(false);
const retryAllClick = () => setRetryAllOpen(!retryAllOpen);
const handleClose = (confirmed) => ()=> {
const handleClose = (confirmed) => () => {
retryAllClick();
if(!confirmed) return;
retryAll(store.failing);
}
if (!confirmed) return;
retryAll(store.failing);
};
return (
<div className="failing">
{failing.map((v, i) => (
<FailingBox key={i} failingTest={v} />
))}
<Dialog
open={retryAllOpen}
onClose={handleClose()}
sx={{ '& .MuiDialog-paper': { width: '80%', maxHeight: 435 } }}
maxWidth="xs"
sx={{ "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }}
maxWidth="xs"
>
<DialogTitle>
Retry all failing tests?
</DialogTitle>
<DialogTitle>Retry all failing tests?</DialogTitle>
<DialogContent>
<DialogContentText>
This will create x jobs and run y tests
@ -56,15 +63,14 @@ export default function Failing() {
</Button>
</DialogActions>
</Dialog>
<SpeedDial
<SpeedDial
ariaLabel="Retry All"
sx={{ position: 'absolute', bottom: 16, right: 16 }}
sx={{ position: "fixed", bottom: 16, right: 16 }}
icon={<ReplayIcon />}
onClick={retryAllClick}
open={false}
open={false}
/>
</div>
);
}

View file

@ -2,15 +2,14 @@ import { useState, useContext } from "react";
import StoreContext from "../ctx/StoreContext.jsx";
import JobContext from "../ctx/JobContext.jsx";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import SpeedDial from "@mui/material/SpeedDial";
import SpeedDialAction from "@mui/material/SpeedDialAction";
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
import ClickAwayListener from '@mui/material/ClickAwayListener';
import SpeedDial from '@mui/material/SpeedDial';
import SpeedDialAction from '@mui/material/SpeedDialAction';
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
import PageviewIcon from '@mui/icons-material/Pageview';
import ViewColumnIcon from '@mui/icons-material/ViewColumn';
import ViewCarouselIcon from '@mui/icons-material/ViewCarousel';
import PageviewIcon from "@mui/icons-material/Pageview";
import ViewColumnIcon from "@mui/icons-material/ViewColumn";
import ViewCarouselIcon from "@mui/icons-material/ViewCarousel";
export default function Jobs() {
const {
@ -26,30 +25,32 @@ export default function Jobs() {
const quickOpenClick = () => setQuickOpen(!quickOpen);
const quickOpenClose = () => setQuickOpen(false);
const actions = [
{name: "Suite", icon: <ViewCarouselIcon/>}, {name: "Compound", icon: <ViewColumnIcon/>}, {name: "Manual", icon: <PageviewIcon/>}
]
{ name: "Suite", icon: <ViewCarouselIcon /> },
{ name: "Compound", icon: <ViewColumnIcon /> },
{ name: "Manual", icon: <PageviewIcon /> },
];
return (
<div className="jobs">
<ClickAwayListener onClickAway={quickOpenClose}>
<SpeedDial
ariaLabel="New Job"
sx={{ position: 'absolute', bottom: 16, right: 16 }}
icon={<SpeedDialIcon />}
onClick={quickOpenClick}
open={quickOpen}
>
{actions.map((action) => (
<SpeedDialAction
key={action.name}
icon={action.icon}
tooltipTitle={action.name}
/>
))}
</SpeedDial>
</ClickAwayListener>
<ClickAwayListener onClickAway={quickOpenClose}>
<SpeedDial
ariaLabel="New Job"
sx={{ position: "fixed", bottom: 16, right: 16 }}
icon={<SpeedDialIcon />}
onClick={quickOpenClick}
open={quickOpen}
>
{actions.map((action) => (
<SpeedDialAction
key={action.name}
icon={action.icon}
tooltipTitle={action.name}
/>
))}
</SpeedDial>
</ClickAwayListener>
</div>
);
}

View file

@ -1,76 +1,80 @@
import { useContext, useState, useEffect } from "react";
import React, { useContext, useState, useEffect } from "react";
import StoreContext from "../ctx/StoreContext.jsx";
import MultiOptionDialog from "./components/MultiOptionDialog.jsx";
import * as React from 'react';
import PropTypes from 'prop-types';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemText from "@mui/material/ListItemText";
import Switch from "@mui/material/Switch";
import SummarizeIcon from '@mui/icons-material/Summarize';
import SummarizeIcon from "@mui/icons-material/Summarize";
import Typography from "@mui/material/Typography";
export default function Settings(props) {
const { state: store, updateStore } = useContext(StoreContext);
const { regions } = store;
const { pages } = props;
const defaultDialog = {title: "", options: [], current: null, onSelect: null, open: false};
const [dialog, setDialog] = React.useState(defaultDialog);
const optionSettings = {region: {
title: "Region",
options: ["us", "au"],
current: store.defaultRegion,
onSelect: (r) => updateStore({defaultRegion: r})
},
defaultPage: {
title: "Default Page",
options: ["failing", "alerting"],
current: store.defaultPage,
onSelect: (p) => updateStore({defaultPage: p})
}}
const defaultDialog = {
title: "",
options: [],
current: null,
onSelect: null,
open: false,
};
const [dialog, setDialog] = useState(defaultDialog);
const optionSettings = {
region: {
title: "Region",
options: [],
current: store.defaultRegion,
onSelect: (r) => updateStore({ defaultRegion: r }),
},
defaultPage: {
title: "Default Page",
options: pages,
current: store.defaultPage,
onSelect: (p) => updateStore({ defaultPage: p }),
},
};
const handleOptionsMenu = (s) => {
setDialog({...s, open:true});
setDialog({ ...s, open: true });
};
const handleClose = (newValue, onSelect) => {
setDialog({...dialog, open:false})
setDialog({ ...dialog, open: false });
if (!newValue) return;
onSelect(newValue);
};
const handleToggle = (booleanSetting) => ()=> {
const handleToggle = (booleanSetting) => () => {
const storeUpdate = {};
storeUpdate[booleanSetting] = !store[booleanSetting];
updateStore(storeUpdate)
}
updateStore(storeUpdate);
};
function MultiOptionSubtext(props){
return( <React.Fragment>
<Typography
sx={{ display: 'inline' }}
component="span"
variant="body2"
color="primary"
>
{props.value}
</Typography>
</React.Fragment>)
function MultiOptionSubtext(props) {
return (
<React.Fragment>
<Typography
sx={{ display: "inline" }}
component="span"
variant="body2"
color="primary"
>
{props.value}
</Typography>
</React.Fragment>
);
}
return (
<Box sx={{ width: '100%', bgcolor: 'background.paper' }}>
<Box sx={{ width: "100%", bgcolor: "background.paper" }}>
<List component="div" role="group">
<ListItem
button
divider
@ -78,39 +82,48 @@ const defaultDialog = {title: "", options: [], current: null, onSelect: null, op
aria-label="default page"
onClick={() => handleOptionsMenu(optionSettings.defaultPage)}
>
<ListItemText primary="Default Page" secondary={
<MultiOptionSubtext value={optionSettings.defaultPage.current} />
}/>
<ListItemText
primary="Default Page"
secondary={
<MultiOptionSubtext value={optionSettings.defaultPage.current} />
}
/>
</ListItem>
<ListItem
<ListItem
button
divider
aria-haspopup="true"
aria-label="region"
onClick={() => handleOptionsMenu(optionSettings.region)}
disabled={optionSettings.region.options.length === 0}
>
<ListItemText primary="Region" secondary={<MultiOptionSubtext value={optionSettings.region.current} />} />
<ListItemText
primary="Region"
secondary={
<MultiOptionSubtext value={optionSettings.region.current} />
}
/>
</ListItem>
<ListItem button divider>
<ListItem button divider>
<ListItemText primary="Simplified Controls" />
<Switch
edge="end"
onChange={handleToggle("simplifiedControls")}
checked={store.simplifiedControls}
/>
<Switch
edge="end"
onChange={handleToggle("simplifiedControls")}
checked={store.simplifiedControls}
/>
</ListItem>
<ListItem button divider>
<ListItem button divider>
<ListItemText primary="Focus New Jobs" />
<Switch
edge="end"
onChange={handleToggle("focusJob")}
checked={store.focusJob}
/>
<Switch
edge="end"
onChange={handleToggle("focusJob")}
checked={store.focusJob}
/>
</ListItem>
<MultiOptionDialog
id="multi-options-menu"
keepMounted

View file

@ -0,0 +1,86 @@
import React, { useState, useContext } from "react";
import StoreContext from "../../ctx/StoreContext.jsx";
import JobContext from "../../ctx/JobContext.jsx";
import Accordion from "@mui/material/Accordion";
import AccordionDetails from "@mui/material/AccordionDetails";
import AccordionSummary from "@mui/material/AccordionSummary";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import DeleteIcon from "@mui/icons-material/Delete";
import NotificationsIcon from "@mui/icons-material/Notifications";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
export default function CatalogBox(props) {
const { catalogTest } = props;
const {
name: testName,
class: testClass,
repo: testRepo,
isCompound,
type: testType
} = catalogTest;
const { state: store, updateStore } = useContext(StoreContext);
const { state: jobState, jobBuilder} = useContext(JobContext);
const [open, setOpen] = useState(false);
const toggleOpen = () => setOpen(!open);
function Actions() {
return (
<React.Fragment>
<IconButton aria-label="play" component="span" color="primary">
<NotificationsIcon />
</IconButton>
<IconButton color="error" aria-label="delete" component="span">
<DeleteIcon />
</IconButton>
</React.Fragment>
);
}
return (
<Accordion expanded={open} disableGutters={true} onChange={toggleOpen}
square>
<AccordionSummary
style={{
backgroundColor: "rgba(0, 0, 0, .03)",
flexWrap: "wrap",
}}
>
<Typography component={"span"} style={{ wordBreak: "break-word" }}>
{`${testClass}#`}
<Box fontWeight="bold" display="inline">
{testName}
</Box>
<br />
</Typography>
<Stack
sx={{ ml: "auto", display: { md: "none", lg: "none", xl: "none" } }}
>
<Actions />
</Stack>
<Stack
direction="row"
sx={{
ml: "auto",
display: { xs: "none", sm: "none", md: "flex", lg: "flex" },
}}
>
<Actions />
</Stack>
<AccordionDetails>
<Typography>{JSON.stringify(catalogTest)}</Typography>
</AccordionDetails>
</AccordionSummary>
</Accordion>
);
}

View file

@ -1,31 +1,44 @@
import * as React from 'react';
import Paper from '@mui/material/Paper';
import InputBase from '@mui/material/InputBase';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import SearchIcon from '@mui/icons-material/Search';
import DirectionsIcon from '@mui/icons-material/Directions';
import ClearOutlinedIcon from "@mui/icons-material/ClearOutlined";
import { useEffect, useRef } from "react";
import Paper from "@mui/material/Paper";
import InputBase from "@mui/material/InputBase";
import Divider from "@mui/material/Divider";
import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu";
import SearchIcon from "@mui/icons-material/Search";
import DirectionsIcon from "@mui/icons-material/Directions";
import ClearOutlinedIcon from "@mui/icons-material/ClearOutlined";
export default function CatalogSearch(props) {
const { onChange, onClear } = props;
const searchRef = useRef(null);
const handleClear = () => {
searchRef.current.children[0].value = null;
searchRef.current.children[0].focus();
onClear();
};
export default function SearchBar(props) {
return (
<Paper
component="form"
sx={{ display: 'flex', alignItems: 'center'}}
>
<Paper component="form" sx={{ display: "flex", alignItems: "center" }}>
<InputBase
sx={{flex: 1 }}
ref={searchRef}
sx={{ ml: 1, flex: 1 }}
placeholder="Search Catalog"
inputProps={{ 'aria-label': `search catalog` }}
inputProps={{ "aria-label": `search catalog` }}
onChange={onChange}
/>
<IconButton type="submit" sx={{ p: '18px' }} aria-label="search">
<IconButton type="submit" sx={{ p: "10px" }} aria-label="search">
<SearchIcon />
</IconButton>
<Divider sx={{ height: 28, m: 0.5 }} orientation="vertical" />
<IconButton sx={{ p: '8px' }} aria-label="clear">
<Divider sx={{ height: 26, m: 0.5 }} orientation="vertical" />
<IconButton
sx={{ p: "10px", mr: 0.5 }}
aria-label="clear"
onClick={handleClear}
>
<ClearOutlinedIcon />
</IconButton>
</Paper>
);
}
}

View file

@ -0,0 +1,166 @@
import React, { useState, useContext } from "react";
import StoreContext from "../../ctx/StoreContext.jsx";
import JobContext, { jobStatus } from "../../ctx/JobContext.jsx";
import Accordion from "@mui/material/Accordion";
import AccordionDetails from "@mui/material/AccordionDetails";
import AccordionSummary from "@mui/material/AccordionSummary";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import DeleteIcon from "@mui/icons-material/Delete";
import NotificationsIcon from "@mui/icons-material/Notifications";
import ReplayIcon from "@mui/icons-material/Replay";
import PhotoCameraIcon from "@mui/icons-material/PhotoCamera";
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 Badge from "@mui/material/Badge";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
const stopPropagation = (e) => e.stopPropagation() && e.preventDefault();
export default function FailingBox(props) {
const { failingTest } = props;
const {
class: testClass,
name: testName,
timestamp,
silencedUntil,
type,
dailyFails,
screenshot: screenshotUrl,
recentResults,
failedMessage,
isCompound,
jobStatus: testJobStatus,
} = failingTest;
const { state: jobState, retryTest, retryJobStatus } = useContext(JobContext);
const { state: store, updateStore } = useContext(StoreContext);
const [open, setOpen] = useState(false);
const toggleOpen = () => setOpen(!open);
function badgeColor() {
if (dailyFails === 1) return "primary";
else if (dailyFails === 2) return "secondary";
else if (dailyFails < 6) return "warning";
return "error";
}
function jobIcon() {
switch (testJobStatus) {
case jobStatus.OK:
return <CheckIcon color="success" />;
case jobStatus.ERROR:
return <ClearIcon color="error" />;
case jobStatus.PENDING:
return <PendingIcon color="info" />;
case jobStatus.ACTIVE:
return <VisibilityIcon color="primary" />;
case jobStatus.CANCELED:
return <DoNotDisturbIcon color="warning" />;
case jobStatus.QUEUED:
return <ViewColumnIcon color="secondary" />;
default:
return <ReplayIcon />;
}
}
function Actions() {
return (
<React.Fragment>
<IconButton aria-label="photo" component="span">
<PhotoCameraIcon />
</IconButton>
<IconButton aria-label="retry" component="span">
{jobIcon()}
</IconButton>
<IconButton
aria-label="silence"
component="span"
color={silencedUntil ? "primary" : "default"}
>
<NotificationsIcon />
</IconButton>
<IconButton color="error" aria-label="delete" component="span">
<DeleteIcon />
</IconButton>
</React.Fragment>
);
}
return (
<Accordion
expanded={open}
onChange={toggleOpen}
disableGutters={true}
square
>
<AccordionSummary
style={{
backgroundColor: "rgba(0, 0, 0, .03)",
flexWrap: "wrap",
}}
>
<Badge
sx={{ mr: 2 }}
color={badgeColor()}
badgeContent={dailyFails}
anchorOrigin={{
vertical: "top",
horizontal: "left",
}}
></Badge>
<Typography component={"span"} style={{ wordBreak: "break-word" }}>
{`${testClass}#`}
<Box fontWeight="bold" display="inline">
{testName}{" "}
</Box>
<br />
<span className="recent-results">
{recentResults.map(
(v, i) =>
(v && <CheckIcon key={i} color="success" />) || (
<ClearIcon key={i} color="error" />
)
)}
</span>
{isCompound && <ViewColumnIcon />}
</Typography>
<Stack
onClick={stopPropagation}
sx={{ ml: "auto", display: { md: "none", lg: "none", xl: "none" } }}
>
<Actions />
</Stack>
<Stack
onClick={stopPropagation}
direction="row"
sx={{
ml: "auto",
display: { xs: "none", sm: "none", md: "flex", lg: "flex" },
}}
>
<Actions />
</Stack>
</AccordionSummary>
<AccordionDetails>
<Typography>{failedMessage}</Typography>
</AccordionDetails>
</Accordion>
);
}

View file

@ -1,24 +1,22 @@
import {useState, useRef, useEffect} from "react";
import Button from "@mui/material/Button"
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Dialog from '@mui/material/Dialog';
import RadioGroup from '@mui/material/RadioGroup';
import Radio from '@mui/material/Radio';
import FormControlLabel from '@mui/material/FormControlLabel';
import { useState, useRef, useEffect } from "react";
import Button from "@mui/material/Button";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions";
import Dialog from "@mui/material/Dialog";
import RadioGroup from "@mui/material/RadioGroup";
import Radio from "@mui/material/Radio";
import FormControlLabel from "@mui/material/FormControlLabel";
export default function MultiOptionDialog(props) {
const { dialog: dialogProp, onClose, open, ...other } = props;
const [value, setValue] = useState(dialogProp.current);
const [dialog, setDialog] = useState(dialogProp);
const radioGroupRef = useRef(null);
useEffect(() => {
useEffect(() => {
setDialog(dialogProp);
setValue(dialogProp.current);
}, [dialogProp, open]);
@ -30,14 +28,14 @@ export default function MultiOptionDialog(props) {
const handleCancel = () => onClose();
const handleOk = () => onClose(value, dialog.onSelect);
const handleChange = (e) =>{ setValue(e.target.value);
}
const handleChange = (e) => {
setValue(e.target.value);
};
return (
<Dialog
sx={{ '& .MuiDialog-paper': { width: '80%', maxHeight: 435 } }}
sx={{ "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }}
maxWidth="xs"
TransitionProps={{ onEntering: handleEntering }}
open={open}

View file

@ -0,0 +1,47 @@
import { useState, useContext } from "react";
import StoreContext from "../../ctx/StoreContext.jsx";
import Button from "@mui/material/Button";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions";
import Dialog from "@mui/material/Dialog";
export default function SilenceDialog(props) {
const { silence, open, onClose } = props;
const [silenceEntry, setSilenceEntry] = useState(silence);
useEffect(() => {
setSilenceEntry(silence);
}, [silence, open]);
const { state: store, updateStore } = useContext(StoreContext);
const upsertSilence = () => {
console.log("Would upsert silence", silenceEntry);
};
const handleCancel = () => onClose();
const handleOk = () => onClose(silenceEntry);
return (
<Dialog
sx={{ "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }}
maxWidth="xs"
open={open}
>
<DialogTitle>Silence Alert</DialogTitle>
<DialogContent>
<span>{JSON.stringify(silenceEntry)}</span>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleOk}>Ok</Button>
</DialogActions>
</Dialog>
);
}

View file

@ -0,0 +1,75 @@
import React, { useState, useContext } from "react";
import StoreContext from "../../ctx/StoreContext.jsx";
import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import DeleteIcon from "@mui/icons-material/Delete";
import NotificationsIcon from "@mui/icons-material/Notifications";
import Stack from "@mui/material/Stack";
export default function SilencingBox(props) {
const { silenceEntry } = props;
const {
name: testName,
method: testMethod,
class: testClass,
id: silenceId,
silencedUntil,
} = silenceEntry;
const { state: store, updateStore } = useContext(StoreContext);
function Actions() {
return (
<React.Fragment>
<IconButton aria-label="modify" component="span" color="primary">
<NotificationsIcon />
</IconButton>
<IconButton color="error" aria-label="delete" component="span">
<DeleteIcon />
</IconButton>
</React.Fragment>
);
}
return (
<Accordion expanded={false} disableGutters={true} square>
<AccordionSummary
style={{
backgroundColor: "rgba(0, 0, 0, .03)",
flexWrap: "wrap",
}}
>
<Typography component={"span"} style={{ wordBreak: "break-word" }}>
{`Test Name: ${testName}`}
<br />
{`Method: ${testMethod}`}
<br />
{`Test Class: ${testClass}`}
<br />
{`Silenced Until: ${silencedUntil} Remaining Time: 2:50`}
</Typography>
<Stack
sx={{ ml: "auto", display: { md: "none", lg: "none", xl: "none" } }}
>
<Actions />
</Stack>
<Stack
direction="row"
sx={{
ml: "auto",
display: { xs: "none", sm: "none", md: "flex", lg: "flex" },
}}
>
<Actions />
</Stack>
</AccordionSummary>
</Accordion>
);
}