Savepoint

This commit is contained in:
Dunemask 2022-05-17 12:32:04 +00:00
parent 7db1a3456b
commit 02c483950c
45 changed files with 5136 additions and 256 deletions

View file

@ -1,10 +1,7 @@
import { useContext } from "react";
// Import Contexts
import { JobProvider } from "./ctx/JobContext.jsx";
import { ViewProvider } from "./ctx/ViewContext.jsx";
import { StoreProvider } from "./ctx/StoreContext.jsx";
import { BrowserRouter } from "react-router-dom";
// Import Views
import Views from "./Views.jsx";
@ -13,9 +10,9 @@ export default function Dashboard() {
<div className="qualiteer">
<JobProvider>
<StoreProvider>
<ViewProvider>
<BrowserRouter>
<Views />
</ViewProvider>
</BrowserRouter>
</StoreProvider>
</JobProvider>
</div>

View file

@ -1,21 +1,23 @@
import { useContext, useState } from "react";
import ViewContext from "./ctx/ViewContext.jsx";
import * as React from "react";
import {
Routes,
Route,
Link,
BrowserRouter,
Navigate,
useLocation,
} from "react-router-dom";
import AppBar from "@mui/material/AppBar";
import Badge, { BadgeProps } from "@mui/material/Badge";
import { styled } from "@mui/material/styles";
import Box from "@mui/material/Box";
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 MenuIcon from "@mui/icons-material/Menu";
import Container from "@mui/material/Container";
import Avatar from "@mui/material/Avatar";
import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
import MenuItem from "@mui/material/MenuItem";
import Drawer from "@mui/material/Drawer";
import ListItem from "@mui/material/ListItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import List from "@mui/material/List";
@ -24,79 +26,124 @@ import NotificationsIcon from "@mui/icons-material/Notifications";
import WorkIcon from "@mui/icons-material/Work";
import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted";
import SettingsIcon from "@mui/icons-material/Settings";
import ErrorIcon from "@mui/icons-material/Error";
import WarningIcon from "@mui/icons-material/Warning";
import InfoIcon from "@mui/icons-material/Info";
// Import Pages
import Failing from "./views/Failing.jsx";
import Alerting from "./views/Alerting.jsx";
import Jobs from "./views/Jobs.jsx";
import Catalog from "./views/Catalog.jsx";
import Settings from "./views/Settings.jsx";
import About from "./views/About.jsx";
const pages = ["failing", "alerting", "jobs", "tests", "settings"];
const pages = ["failing", "alerting", "jobs", "catalog", "settings", "about"];
const icons = [
ErrorIcon,
NotificationsIcon,
WorkIcon,
FormatListBulletedIcon,
SettingsIcon,
<Badge badgeContent={4} color="error">
<WarningIcon />
</Badge>,
<NotificationsIcon />,
<Badge badgeContent={4} color="primary">
<WorkIcon />
</Badge>,
<FormatListBulletedIcon />,
<SettingsIcon />,
<InfoIcon />,
];
const drawerWidth = 240;
export default function Views() {
const [view, setView] = useState(pages[0]);
const location = useLocation();
const [drawerOpen, setDrawer] = React.useState(false);
const toggleDrawer = () => setDrawer(!drawerOpen);
const closeDrawer = () => setDrawer(false);
const openPage = (e) => setView(e.target.outerText.toLowerCase());
const reloadPage = () => window.location.reload(false);
const SideBadge = styled(Badge)(({ theme }) => ({
"& .MuiBadge-badge": {
right: -6,
top: 10,
padding: "0 4px",
},
}));
const navHeader = () => {
const pathStr =
location.pathname.charAt(1).toUpperCase() + location.pathname.slice(2);
if (location.pathname !== "/failing") return pathStr;
return (
<SideBadge badgeContent={4} color="error" overlap="circular">
{pathStr}
</SideBadge>
);
};
return (
<AppBar position="static" color="secondary">
<Container maxWidth="xl">
<Toolbar disableGutters>
<Drawer open={drawerOpen} onClose={closeDrawer}>
{" "}
<Box sx={{ width: 250 }} role="presentation">
<List>
{pages.map((text, index) => (
<ListItemButton
key={text}
onClick={openPage}
selected={view === text}
>
<ListItemIcon>{/*icons[index]*/}</ListItemIcon>
<ListItemText
primary={text.charAt(0).toUpperCase() + text.slice(1)}
/>
</ListItemButton>
))}
</List>
</Box>
</Drawer>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }}
onClick={toggleDrawer}
>
<MenuIcon />
</IconButton>
<Typography
variant="h6"
noWrap
component="div"
sx={{ mr: 2, display: { xs: "none", md: "flex" } }}
>
{view.charAt(0).toUpperCase() + view.slice(1)}
</Typography>
<Typography
variant="h6"
noWrap
component="div"
sx={{ flexGrow: 1, display: { xs: "flex", md: "none" } }}
>
{view.charAt(0).toUpperCase() + view.slice(1)}
</Typography>
<Box sx={{ flexGrow: 0 }}>
<Avatar alt="Remy Sharp" src="/assets/QA.jpg" />
</Box>
</Toolbar>
</Container>
</AppBar>
<div className="view">
<AppBar
position="fixed"
sx={{ bgcolor: "black", zIndex: (theme) => theme.zIndex.drawer + 1 }}
>
<Box sx={{ flexGrow: 1, margin: "0 20px" }}>
<Toolbar disableGutters>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }}
onClick={toggleDrawer}
>
<MenuIcon />
</IconButton>
<Drawer open={drawerOpen} onClose={closeDrawer}>
<Toolbar />
<Box sx={{ width: 250, overflow: "auto" }} role="presentation">
<List>
{pages.map((text, index) => (
<ListItemButton
key={text}
component={Link}
to={"/" + text}
selected={location.pathname === "/" + text}
onClick={closeDrawer}
>
<ListItemIcon>{icons[index]}</ListItemIcon>
<ListItemText
primary={text.charAt(0).toUpperCase() + text.slice(1)}
/>
</ListItemButton>
))}
</List>
</Box>
</Drawer>
<Typography
variant="h6"
noWrap
component="div"
sx={{ flexGrow: 1 }}
>
{navHeader()}
</Typography>
<Avatar alt="Remy Sharp" src="/assets/QA.png" onClick={reloadPage}/>
</Toolbar>
</Box>
</AppBar>
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<Toolbar />
<Routes>
<Route exact path="/" element={<Navigate to="/failing" replace />} />
<Route path="/failing" element={<Failing />} />
<Route path="/alerting" element={<Alerting />} />
<Route path="/jobs" element={<Jobs />} />
<Route path="/catalog" element={<Catalog />} />
<Route path="/settings" element={<Settings pages={pages} />} />
<Route path="/about" element={<About />} />
</Routes>
</Box>
</div>
);
}

View file

@ -36,16 +36,32 @@ 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 jobCreate = (job) =>
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 jobBuilder(){
}
const context = {
state,
dispatch,
jobUpdate: (job, jobId) => dispatch({ type: ACTIONS.UPDATE, jobId, job }),
jobCreate: (job) =>
dispatch({ type: ACTIONS.CREATE, job: { ...job, log: [] } }),
jobDelete: (jobId) => dispatch({ type: ACTIONS.DELETE, jobId }),
jobUpdate,
jobCreate,
jobDelete,
retryAll
};
const contextValue = useMemo(() => context, [state, dispatch]);

View file

@ -5,9 +5,18 @@ const ACTIONS = {
UPDATE: "u",
};
const initialState = {};
const initialState = {
intervals: [],
failing: [],
regions: [],
focusJob: false,
simplifiedControls: false,
defaultRegion: "us", // Local Store
defaultPage: "failing", // Local Store
};
const reducer = (state, action) => {
const { store } = action;
// Actions
switch (action.type) {
case ACTIONS.UPDATE:
@ -23,7 +32,7 @@ export const StoreProvider = ({ children }) => {
const context = {
state,
dispatch,
updateStore: (store) => dispatch(state, { type: ACTIONS.UPDATE, store }),
updateStore: (store) => dispatch({ type: ACTIONS.UPDATE, store }),
};
const contextValue = useMemo(() => context, [state, dispatch]);

View file

@ -1,32 +0,0 @@
import React, { useReducer, createContext, useMemo } from "react";
const ViewContext = createContext();
const ACTIONS = {
UPDATE: "u",
};
const initialState = {
activePage: "Home",
};
const reducer = (state, action) => {
// Actions
switch (action.type) {
case ACTIONS.UPDATE:
return { ...state };
default:
return state;
}
};
export const ViewProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const contextValue = useMemo(() => ({ state, dispatch }), [state, dispatch]);
return (
<ViewContext.Provider value={contextValue}>{children}</ViewContext.Provider>
);
};
export default ViewContext;

32
src/views/About.jsx Normal file
View file

@ -0,0 +1,32 @@
import Typography from "@mui/material/Typography";
import Link from "@mui/material/Link";
import Container from "@mui/material/Container";
import Box from "@mui/material/Box";
const memeUrl = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
const repoUrl = "https://gitlab.com/dunemask/Qualiteer";
export default function About() {
return (
<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>
);
}

61
src/views/Alerting.jsx Normal file
View file

@ -0,0 +1,61 @@
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 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);
const [alertDialogOpen, setAlertDialogOpen] = useState(false);
const quickAlertClick = () => setAlertDialogOpen(!alertDialogOpen);
function silenceAlert(){
}
const handleClose = (confirmed) => () => {
quickAlertClick();
if(!confirmed) return;
silenceAlert();
}
return (
<div className="alerting">
<Dialog
open={alertDialogOpen}
onClose={handleClose()}
sx={{ '& .MuiDialog-paper': { width: '80%', maxHeight: 435 } }}
maxWidth="xs"
>
<DialogTitle>
Silence Alert
</DialogTitle>
<DialogContent>
</DialogContent>
<DialogActions>
<Button onClick={handleClose()}>Cancel</Button>
<Button onClick={handleClose(true)} autoFocus>
Silence
</Button>
</DialogActions>
</Dialog>
<SpeedDial
ariaLabel="Silence Alert"
sx={{ position: 'absolute', bottom: 16, right: 16 }}
icon={<SpeedDialIcon />}
onClick={quickAlertClick}
open={false}
/>
</div>
);
}

29
src/views/Catalog.jsx Normal file
View file

@ -0,0 +1,29 @@
import { useContext } from "react";
import StoreContext from "../ctx/StoreContext.jsx";
import JobContext from "../ctx/JobContext.jsx";
import TextField from "@mui/material/TextField";
import CatalogSearch from "./components/CatalogSearch.jsx";
export default function Catalog() {
const {
state: jobState,
dispatch: jobDispatch,
jobUpdate,
jobCreate,
} = useContext(JobContext);
const { state: store, updateStore } = useContext(StoreContext);
return (
<div className="catalog">
<CatalogSearch />
<TextField
label="Search Catalog"
type="search"
variant="filled"
/>
</div>
);
}

70
src/views/Failing.jsx Normal file
View file

@ -0,0 +1,70 @@
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 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';
export default function Failing() {
const {
state: jobState,
retryAll
} = useContext(JobContext);
const { state: store, updateStore } = useContext(StoreContext);
const [retryAllOpen, setRetryAllOpen] = useState(false);
const retryAllClick = () => setRetryAllOpen(!retryAllOpen);
const handleClose = (confirmed) => ()=> {
retryAllClick();
if(!confirmed) return;
retryAll(store.failing);
}
return (
<div className="failing">
<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>
<SpeedDial
ariaLabel="Retry All"
sx={{ position: 'absolute', bottom: 16, right: 16 }}
icon={<ReplayIcon />}
onClick={retryAllClick}
open={false}
/>
</div>
);
}

55
src/views/Jobs.jsx Normal file
View file

@ -0,0 +1,55 @@
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 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 {
state: jobState,
dispatch: jobDispatch,
jobUpdate,
jobCreate,
} = useContext(JobContext);
const { state: store, updateStore } = useContext(StoreContext);
const [quickOpen, setQuickOpen] = useState(false);
const quickOpenClick = () => setQuickOpen(!quickOpen);
const quickOpenClose = () => setQuickOpen(false);
const actions = [
{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>
</div>
);
}

124
src/views/Settings.jsx Normal file
View file

@ -0,0 +1,124 @@
import { 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 Switch from "@mui/material/Switch";
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 handleOptionsMenu = (s) => {
setDialog({...s, open:true});
};
const handleClose = (newValue, onSelect) => {
setDialog({...dialog, open:false})
if (!newValue) return;
onSelect(newValue);
};
const handleToggle = (booleanSetting) => ()=> {
const storeUpdate = {};
storeUpdate[booleanSetting] = !store[booleanSetting];
updateStore(storeUpdate)
}
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' }}>
<List component="div" role="group">
<ListItem
button
divider
aria-haspopup="true"
aria-label="default page"
onClick={() => handleOptionsMenu(optionSettings.defaultPage)}
>
<ListItemText primary="Default Page" secondary={
<MultiOptionSubtext value={optionSettings.defaultPage.current} />
}/>
</ListItem>
<ListItem
button
divider
aria-haspopup="true"
aria-label="region"
onClick={() => handleOptionsMenu(optionSettings.region)}
>
<ListItemText primary="Region" secondary={<MultiOptionSubtext value={optionSettings.region.current} />} />
</ListItem>
<ListItem button divider>
<ListItemText primary="Simplified Controls" />
<Switch
edge="end"
onChange={handleToggle("simplifiedControls")}
checked={store.simplifiedControls}
/>
</ListItem>
<ListItem button divider>
<ListItemText primary="Focus New Jobs" />
<Switch
edge="end"
onChange={handleToggle("focusJob")}
checked={store.focusJob}
/>
</ListItem>
<MultiOptionDialog
id="multi-options-menu"
keepMounted
open={dialog.open}
onClose={handleClose}
dialog={dialog}
/>
</List>
</Box>
);
}

View file

@ -0,0 +1,31 @@
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";
export default function SearchBar(props) {
return (
<Paper
component="form"
sx={{ display: 'flex', alignItems: 'center'}}
>
<InputBase
sx={{flex: 1 }}
placeholder="Search Catalog"
inputProps={{ 'aria-label': `search catalog` }}
/>
<IconButton type="submit" sx={{ p: '18px' }} aria-label="search">
<SearchIcon />
</IconButton>
<Divider sx={{ height: 28, m: 0.5 }} orientation="vertical" />
<IconButton sx={{ p: '8px' }} aria-label="clear">
<ClearOutlinedIcon />
</IconButton>
</Paper>
);
}

View file

@ -0,0 +1,73 @@
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(() => {
setDialog(dialogProp);
setValue(dialogProp.current);
}, [dialogProp, open]);
const handleEntering = () => {
if (radioGroupRef.current != null) radioGroupRef.current.focus();
};
const handleCancel = () => onClose();
const handleOk = () => onClose(value, dialog.onSelect);
const handleChange = (e) =>{ setValue(e.target.value);
}
return (
<Dialog
sx={{ '& .MuiDialog-paper': { width: '80%', maxHeight: 435 } }}
maxWidth="xs"
TransitionProps={{ onEntering: handleEntering }}
open={open}
{...other}
>
<DialogTitle>{dialog.title}</DialogTitle>
<DialogContent dividers>
<RadioGroup
ref={radioGroupRef}
aria-label={dialogProp.title}
name={dialog.title}
value={value}
onChange={handleChange}
>
{dialog.options.map((option) => (
<FormControlLabel
value={option}
key={option}
control={<Radio />}
label={option}
/>
))}
</RadioGroup>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleOk}>Ok</Button>
</DialogActions>
</Dialog>
);
}