Update Next.js, Material UI

This commit is contained in:
David Stotijn
2022-01-28 20:20:15 +01:00
parent 73ebb89863
commit aa8ddf4122
42 changed files with 2777 additions and 6026 deletions

View File

@ -1,7 +1,7 @@
before: before:
hooks: hooks:
- make clean - make clean
- make build-admin - sh -c "NEXT_PUBLIC_VERSION={{ .Version}} make build-admin"
- go mod tidy - go mod tidy
builds: builds:

6
admin/.eslintrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "next/core-web-vitals",
"rules": {
"@next/next/no-css-tags": "off"
}
}

4
admin/.prettierignore Normal file
View File

@ -0,0 +1,4 @@
/.next/
/out/
/build
/coverage

3
admin/.prettierrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"printWidth": 120
}

5
admin/next-env.d.ts vendored
View File

@ -1,2 +1,5 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/types/global" /> /// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -1,7 +1,10 @@
const withCSS = require("@zeit/next-css"); // @ts-check
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
module.exports = withCSS({ /**
* @type {import('next').NextConfig}
**/
const nextConfig = {
reactStrictMode: true,
trailingSlash: true, trailingSlash: true,
async rewrites() { async rewrites() {
return [ return [
@ -11,24 +14,6 @@ module.exports = withCSS({
}, },
]; ];
}, },
webpack: (config) => { };
config.module.rules.push({
test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/,
use: {
loader: "url-loader",
options: {
limit: 100000,
},
},
});
config.plugins.push( module.exports = nextConfig;
new MonacoWebpackPlugin({
languages: ["html", "json", "javascript"],
filename: "static/[name].worker.js",
})
);
return config;
},
});

View File

@ -6,31 +6,38 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint",
"export": "next build && next export -o dist" "export": "next build && next export -o dist"
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.2.0", "@apollo/client": "^3.2.0",
"@material-ui/core": "^4.11.0", "@emotion/react": "^11.7.1",
"@material-ui/icons": "^4.9.1", "@emotion/server": "^11.4.0",
"@material-ui/lab": "^4.0.0-alpha.56", "@emotion/styled": "^11.6.0",
"@zeit/next-css": "^1.0.1", "@monaco-editor/react": "^4.3.1",
"graphql": "^15.3.0", "@mui/icons-material": "^5.3.1",
"monaco-editor": "^0.20.0", "@mui/lab": "^5.0.0-alpha.66",
"monaco-editor-webpack-plugin": "^1.9.0", "@mui/material": "^5.3.1",
"next": "^9.5.4", "deepmerge": "^4.2.2",
"graphql": "^16.2.0",
"lodash": "^4.17.21",
"monaco-editor": "^0.31.1",
"next": "^12.0.8",
"next-fonts": "^1.0.3", "next-fonts": "^1.0.3",
"react": "^16.13.1", "react": "^17.0.2",
"react-dom": "^16.13.1", "react-dom": "^17.0.2",
"react-monaco-editor": "^0.34.0",
"react-syntax-highlighter": "^13.5.3",
"typescript": "^4.0.3" "typescript": "^4.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^14.11.1", "@babel/core": "^7.0.0",
"@types/react": "^16.9.49", "@types/lodash": "^4.14.178",
"eslint": "^7.9.0", "@types/node": "^17.0.12",
"eslint-config-prettier": "^6.11.0", "@types/react": "^17.0.38",
"eslint-plugin-prettier": "^3.1.4", "eslint": "^8.7.0",
"prettier": "^2.1.2" "eslint-config-next": "12.0.8",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.1.2",
"webpack": "^5.67.0"
} }
} }

View File

@ -1,10 +1,6 @@
import { Paper } from "@material-ui/core"; import { Paper } from "@mui/material";
function CenteredPaper({ function CenteredPaper({ children }: { children: React.ReactNode }): JSX.Element {
children,
}: {
children: React.ReactNode;
}): JSX.Element {
return ( return (
<div> <div>
<Paper <Paper

View File

@ -1,31 +1,31 @@
import React from "react"; import React from "react";
import { import {
makeStyles,
Theme, Theme,
createStyles,
useTheme, useTheme,
AppBar,
Toolbar, Toolbar,
IconButton, IconButton,
Typography, Typography,
Drawer,
Divider, Divider,
List, List,
ListItem,
ListItemIcon,
ListItemText,
Tooltip, Tooltip,
} from "@material-ui/core"; styled,
CSSObject,
Box,
ListItemText,
} from "@mui/material";
import MuiAppBar, { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar";
import MuiDrawer from "@mui/material/Drawer";
import MuiListItemButton, { ListItemButtonProps } from "@mui/material/ListItemButton";
import MuiListItemIcon, { ListItemIconProps } from "@mui/material/ListItemIcon";
import Link from "next/link"; import Link from "next/link";
import MenuIcon from "@material-ui/icons/Menu"; import MenuIcon from "@mui/icons-material/Menu";
import HomeIcon from "@material-ui/icons/Home"; import HomeIcon from "@mui/icons-material/Home";
import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet"; import SettingsEthernetIcon from "@mui/icons-material/SettingsEthernet";
import SendIcon from "@material-ui/icons/Send"; import SendIcon from "@mui/icons-material/Send";
import FolderIcon from "@material-ui/icons/Folder"; import FolderIcon from "@mui/icons-material/Folder";
import LocationSearchingIcon from "@material-ui/icons/LocationSearching"; import LocationSearchingIcon from "@mui/icons-material/LocationSearching";
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@material-ui/icons/ChevronRight"; import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import clsx from "clsx";
export enum Page { export enum Page {
Home, Home,
@ -39,85 +39,91 @@ export enum Page {
const drawerWidth = 240; const drawerWidth = 240;
const useStyles = makeStyles((theme: Theme) => const openedMixin = (theme: Theme): CSSObject => ({
createStyles({ width: drawerWidth,
root: { transition: theme.transitions.create("width", {
display: "flex", easing: theme.transitions.easing.sharp,
width: "100%", duration: theme.transitions.duration.enteringScreen,
}, }),
appBar: { overflowX: "hidden",
zIndex: theme.zIndex.drawer + 1, });
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp, const closedMixin = (theme: Theme): CSSObject => ({
duration: theme.transitions.duration.leavingScreen, transition: theme.transitions.create("width", {
}), easing: theme.transitions.easing.sharp,
}, duration: theme.transitions.duration.leavingScreen,
appBarShift: { }),
marginLeft: drawerWidth, overflowX: "hidden",
width: `calc(100% - ${drawerWidth}px)`, width: 56,
transition: theme.transitions.create(["width", "margin"], { });
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen, const DrawerHeader = styled("div")(({ theme }) => ({
}), display: "flex",
}, alignItems: "center",
menuButton: { justifyContent: "flex-start",
marginRight: 28, padding: theme.spacing(0, 1),
}, // necessary for content to be below app bar
hide: { ...theme.mixins.toolbar,
display: "none", }));
},
drawer: { interface AppBarProps extends MuiAppBarProps {
width: drawerWidth, open?: boolean;
flexShrink: 0, }
whiteSpace: "nowrap",
}, const AppBar = styled(MuiAppBar, {
drawerOpen: { shouldForwardProp: (prop) => prop !== "open",
width: drawerWidth, })<AppBarProps>(({ theme, open }) => ({
transition: theme.transitions.create("width", { backgroundColor: theme.palette.secondary.dark,
easing: theme.transitions.easing.sharp, zIndex: theme.zIndex.drawer + 1,
duration: theme.transitions.duration.enteringScreen, transition: theme.transitions.create(["width", "margin"], {
}), easing: theme.transitions.easing.sharp,
}, duration: theme.transitions.duration.leavingScreen,
drawerClose: { }),
transition: theme.transitions.create("width", { ...(open && {
easing: theme.transitions.easing.sharp, marginLeft: drawerWidth,
duration: theme.transitions.duration.leavingScreen, width: `calc(100% - ${drawerWidth}px)`,
}), transition: theme.transitions.create(["width", "margin"], {
overflowX: "hidden", easing: theme.transitions.easing.sharp,
width: theme.spacing(7) + 1, duration: theme.transitions.duration.enteringScreen,
[theme.breakpoints.up("sm")]: { }),
width: theme.spacing(7) + 8, }),
}));
const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== "open" })(({ theme, open }) => ({
width: drawerWidth,
flexShrink: 0,
whiteSpace: "nowrap",
boxSizing: "border-box",
...(open && {
...openedMixin(theme),
"& .MuiDrawer-paper": openedMixin(theme),
}),
...(!open && {
...closedMixin(theme),
"& .MuiDrawer-paper": closedMixin(theme),
}),
}));
const ListItemButton = styled(MuiListItemButton)<ListItemButtonProps>(({ theme }) => ({
[theme.breakpoints.up("sm")]: {
px: 1,
},
"&.MuiListItemButton-root": {
"&.Mui-selected": {
backgroundColor: theme.palette.primary.main,
"& .MuiListItemIcon-root": {
color: theme.palette.secondary.dark,
},
"& .MuiListItemText-root": {
color: theme.palette.secondary.dark,
}, },
}, },
toolbar: { },
display: "flex", }));
alignItems: "center",
justifyContent: "flex-end", const ListItemIcon = styled(MuiListItemIcon)<ListItemIconProps>(() => ({
padding: theme.spacing(0, 1), minWidth: 42,
// necessary for content to be below app bar }));
...theme.mixins.toolbar,
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
listItem: {
paddingLeft: 16,
paddingRight: 16,
[theme.breakpoints.up("sm")]: {
paddingLeft: 20,
paddingRight: 20,
},
},
listItemIcon: {
minWidth: 42,
},
titleHighlight: {
color: theme.palette.secondary.main,
marginRight: 4,
},
})
);
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
@ -126,7 +132,6 @@ interface Props {
} }
export function Layout({ title, page, children }: Props): JSX.Element { export function Layout({ title, page, children }: Props): JSX.Element {
const classes = useStyles();
const theme = useTheme(); const theme = useTheme();
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
@ -138,145 +143,109 @@ export function Layout({ title, page, children }: Props): JSX.Element {
setOpen(false); setOpen(false);
}; };
const SiteTitle = styled("span")({
...(title !== "" && {
color: theme.palette.primary.main,
marginRight: 4,
}),
});
return ( return (
<div className={classes.root}> <Box sx={{ display: "flex" }}>
<AppBar <AppBar position="fixed" open={open}>
position="fixed"
className={clsx(classes.appBar, {
[classes.appBarShift]: open,
})}
>
<Toolbar> <Toolbar>
<IconButton <IconButton
color="inherit" color="inherit"
aria-label="open drawer" aria-label="Open drawer"
onClick={handleDrawerOpen} onClick={handleDrawerOpen}
edge="start" edge="start"
className={clsx(classes.menuButton, { sx={{
[classes.hide]: open, mr: 5,
})} ...(open && { display: "none" }),
}}
> >
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<Typography variant="h5" noWrap> <Box
<span className={title !== "" ? classes.titleHighlight : ""}> sx={{
Hetty:// display: "flex",
</span> justifyContent: "space-around",
{title} width: "100%",
</Typography> }}
>
<Typography variant="h5" noWrap sx={{ width: "100%" }}>
<SiteTitle>Hetty://</SiteTitle>
{title}
</Typography>
<Box sx={{ flexShrink: 0, pt: 0.75 }}>v{process.env.NEXT_PUBLIC_VERSION || "0.0"}</Box>
</Box>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<Drawer <Drawer variant="permanent" open={open}>
variant="permanent" <DrawerHeader>
className={clsx(classes.drawer, {
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
}),
}}
>
<div className={classes.toolbar}>
<IconButton onClick={handleDrawerClose}> <IconButton onClick={handleDrawerClose}>
{theme.direction === "rtl" ? ( {theme.direction === "rtl" ? <ChevronRightIcon /> : <ChevronLeftIcon />}
<ChevronRightIcon />
) : (
<ChevronLeftIcon />
)}
</IconButton> </IconButton>
</div> </DrawerHeader>
<Divider /> <Divider />
<List> <List sx={{ p: 0 }}>
<Link href="/" passHref> <Link href="/" passHref>
<ListItem <ListItemButton key="home" selected={page === Page.Home}>
button
component="a"
key="home"
selected={page === Page.Home}
className={classes.listItem}
>
<Tooltip title="Home"> <Tooltip title="Home">
<ListItemIcon className={classes.listItemIcon}> <ListItemIcon>
<HomeIcon /> <HomeIcon />
</ListItemIcon> </ListItemIcon>
</Tooltip> </Tooltip>
<ListItemText primary="Home" /> <ListItemText primary="Home" />
</ListItem> </ListItemButton>
</Link> </Link>
<Link href="/proxy/logs" passHref> <Link href="/proxy/logs" passHref>
<ListItem <ListItemButton key="proxyLogs" selected={page === Page.ProxyLogs}>
button
component="a"
key="proxyLogs"
selected={page === Page.ProxyLogs}
className={classes.listItem}
>
<Tooltip title="Proxy"> <Tooltip title="Proxy">
<ListItemIcon className={classes.listItemIcon}> <ListItemIcon>
<SettingsEthernetIcon /> <SettingsEthernetIcon />
</ListItemIcon> </ListItemIcon>
</Tooltip> </Tooltip>
<ListItemText primary="Proxy" /> <ListItemText primary="Proxy" />
</ListItem> </ListItemButton>
</Link> </Link>
<Link href="/sender" passHref> <Link href="/sender" passHref>
<ListItem <ListItemButton key="sender" selected={page === Page.Sender}>
button
component="a"
key="sender"
selected={page === Page.Sender}
className={classes.listItem}
>
<Tooltip title="Sender"> <Tooltip title="Sender">
<ListItemIcon className={classes.listItemIcon}> <ListItemIcon>
<SendIcon /> <SendIcon />
</ListItemIcon> </ListItemIcon>
</Tooltip> </Tooltip>
<ListItemText primary="Sender" /> <ListItemText primary="Sender" />
</ListItem> </ListItemButton>
</Link> </Link>
<Link href="/scope" passHref> <Link href="/scope" passHref>
<ListItem <ListItemButton key="scope" selected={page === Page.Scope}>
button
component="a"
key="scope"
selected={page === Page.Scope}
className={classes.listItem}
>
<Tooltip title="Scope"> <Tooltip title="Scope">
<ListItemIcon className={classes.listItemIcon}> <ListItemIcon>
<LocationSearchingIcon /> <LocationSearchingIcon />
</ListItemIcon> </ListItemIcon>
</Tooltip> </Tooltip>
<ListItemText primary="Scope" /> <ListItemText primary="Scope" />
</ListItem> </ListItemButton>
</Link> </Link>
<Link href="/projects" passHref> <Link href="/projects" passHref>
<ListItem <ListItemButton key="projects" selected={page === Page.Projects}>
button
component="a"
key="projects"
selected={page === Page.Projects}
className={classes.listItem}
>
<Tooltip title="Projects"> <Tooltip title="Projects">
<ListItemIcon className={classes.listItemIcon}> <ListItemIcon>
<FolderIcon /> <FolderIcon />
</ListItemIcon> </ListItemIcon>
</Tooltip> </Tooltip>
<ListItemText primary="Projects" /> <ListItemText primary="Projects" />
</ListItem> </ListItemButton>
</Link> </Link>
</List> </List>
</Drawer> </Drawer>
<main className={classes.content}> <Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<div className={classes.toolbar} /> <DrawerHeader />
{children} {children}
</main> </Box>
</div> </Box>
); );
} }

View File

@ -1,29 +1,8 @@
import { gql, useMutation } from "@apollo/client"; import { gql, useMutation } from "@apollo/client";
import { import { Box, Button, CircularProgress, TextField, Typography } from "@mui/material";
Box, import AddIcon from "@mui/icons-material/Add";
Button,
CircularProgress,
createStyles,
makeStyles,
TextField,
Theme,
Typography,
} from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import React, { useState } from "react"; import React, { useState } from "react";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
projectName: {
marginTop: -6,
marginRight: theme.spacing(2),
},
button: {
marginRight: theme.spacing(2),
},
})
);
const CREATE_PROJECT = gql` const CREATE_PROJECT = gql`
mutation CreateProject($name: String!) { mutation CreateProject($name: String!) {
createProject(name: $name) { createProject(name: $name) {
@ -44,21 +23,17 @@ const OPEN_PROJECT = gql`
`; `;
function NewProject(): JSX.Element { function NewProject(): JSX.Element {
const classes = useStyles(); const [name, setName] = useState("");
const [input, setInput] = useState(null);
const [createProject, { error: createProjErr, loading: createProjLoading }] = useMutation(CREATE_PROJECT, { const [createProject, { error: createProjErr, loading: createProjLoading }] = useMutation(CREATE_PROJECT, {
onError: () => { }, onError: () => {},
onCompleted(data) { onCompleted(data) {
input.value = ""; setName("");
openProject({ variables: { id: data.createProject.id } }); openProject({ variables: { id: data.createProject.id } });
}, },
}); });
const [openProject, { error: openProjErr, loading: openProjLoading }] = useMutation(OPEN_PROJECT, { const [openProject, { error: openProjErr, loading: openProjLoading }] = useMutation(OPEN_PROJECT, {
onError: () => { }, onError: () => {},
onCompleted() {
input.value = "";
},
update(cache, { data: { openProject } }) { update(cache, { data: { openProject } }) {
cache.modify({ cache.modify({
fields: { fields: {
@ -99,7 +74,7 @@ function NewProject(): JSX.Element {
const handleCreateAndOpenProjectForm = (e: React.SyntheticEvent) => { const handleCreateAndOpenProjectForm = (e: React.SyntheticEvent) => {
e.preventDefault(); e.preventDefault();
createProject({ variables: { name: input.value } }); createProject({ variables: { name } });
}; };
return ( return (
@ -109,25 +84,26 @@ function NewProject(): JSX.Element {
</Box> </Box>
<form onSubmit={handleCreateAndOpenProjectForm} autoComplete="off"> <form onSubmit={handleCreateAndOpenProjectForm} autoComplete="off">
<TextField <TextField
className={classes.projectName} sx={{
color="secondary" mr: 2,
inputProps={{
id: "projectName",
ref: (node) => {
setInput(node);
},
}} }}
color="primary"
size="small"
label="Project name" label="Project name"
placeholder="Project name…" placeholder="Project name…"
onChange={(e) => setName(e.target.value)}
error={Boolean(createProjErr || openProjErr)} error={Boolean(createProjErr || openProjErr)}
helperText={createProjErr && createProjErr.message || openProjErr && openProjErr.message} helperText={(createProjErr && createProjErr.message) || (openProjErr && openProjErr.message)}
/> />
<Button <Button
className={classes.button}
type="submit" type="submit"
variant="contained" variant="contained"
color="secondary" color="primary"
size="large" size="large"
sx={{
pt: 0.9,
pb: 0.7,
}}
disabled={createProjLoading || openProjLoading} disabled={createProjLoading || openProjLoading}
startIcon={createProjLoading || openProjLoading ? <CircularProgress size={22} /> : <AddIcon />} startIcon={createProjLoading || openProjLoading ? <CircularProgress size={22} /> : <AddIcon />}
> >

View File

@ -4,7 +4,6 @@ import {
Box, Box,
Button, Button,
CircularProgress, CircularProgress,
createStyles,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
@ -16,34 +15,20 @@ import {
ListItemAvatar, ListItemAvatar,
ListItemSecondaryAction, ListItemSecondaryAction,
ListItemText, ListItemText,
makeStyles, Paper,
Snackbar, Snackbar,
Theme,
Tooltip, Tooltip,
Typography, Typography,
} from "@material-ui/core"; useTheme,
import CloseIcon from "@material-ui/icons/Close"; } from "@mui/material";
import DescriptionIcon from "@material-ui/icons/Description"; import CloseIcon from "@mui/icons-material/Close";
import DeleteIcon from "@material-ui/icons/Delete"; import DescriptionIcon from "@mui/icons-material/Description";
import LaunchIcon from "@material-ui/icons/Launch"; import DeleteIcon from "@mui/icons-material/Delete";
import { Alert } from "@material-ui/lab"; import LaunchIcon from "@mui/icons-material/Launch";
import { Alert } from "@mui/lab";
import React, { useState } from "react"; import React, { useState } from "react";
const useStyles = makeStyles((theme: Theme) => import { Project } from "../../lib/Project";
createStyles({
projectsList: {
backgroundColor: theme.palette.background.paper,
},
activeProject: {
color: theme.palette.getContrastText(theme.palette.secondary.main),
backgroundColor: theme.palette.secondary.main,
},
deleteProjectButton: {
color: theme.palette.error.main,
},
})
);
const PROJECTS = gql` const PROJECTS = gql`
query Projects { query Projects {
@ -82,58 +67,56 @@ const DELETE_PROJECT = gql`
`; `;
function ProjectList(): JSX.Element { function ProjectList(): JSX.Element {
const classes = useStyles(); const theme = useTheme();
const { loading: projLoading, error: projErr, data: projData } = useQuery( const { loading: projLoading, error: projErr, data: projData } = useQuery<{ projects: Project[] }>(PROJECTS);
PROJECTS const [openProject, { error: openProjErr, loading: openProjLoading }] = useMutation<{ openProject: Project }>(
OPEN_PROJECT,
{
errorPolicy: "all",
onError: () => {},
update(cache, { data }) {
cache.modify({
fields: {
activeProject() {
const activeProjRef = cache.writeFragment({
data: data?.openProject,
fragment: gql`
fragment ActiveProject on Project {
id
name
isActive
type
}
`,
});
return activeProjRef;
},
projects(_, { DELETE }) {
cache.writeFragment({
id: data?.openProject.id,
data: openProject,
fragment: gql`
fragment OpenProject on Project {
id
name
isActive
type
}
`,
});
return DELETE;
},
httpRequestLogFilter(_, { DELETE }) {
return DELETE;
},
},
});
},
}
); );
const [
openProject,
{ error: openProjErr, loading: openProjLoading },
] = useMutation(OPEN_PROJECT, {
errorPolicy: "all",
onError: () => { },
update(cache, { data: { openProject } }) {
cache.modify({
fields: {
activeProject() {
const activeProjRef = cache.writeFragment({
data: openProject,
fragment: gql`
fragment ActiveProject on Project {
id
name
isActive
type
}
`,
});
return activeProjRef;
},
projects(_, { DELETE }) {
cache.writeFragment({
id: openProject.id,
data: openProject,
fragment: gql`
fragment OpenProject on Project {
id
name
isActive
type
}
`,
});
return DELETE;
},
httpRequestLogFilter(_, { DELETE }) {
return DELETE;
},
},
});
},
});
const [closeProject, { error: closeProjErr }] = useMutation(CLOSE_PROJECT, { const [closeProject, { error: closeProjErr }] = useMutation(CLOSE_PROJECT, {
errorPolicy: "all", errorPolicy: "all",
onError: () => { }, onError: () => {},
update(cache) { update(cache) {
cache.modify({ cache.modify({
fields: { fields: {
@ -150,12 +133,9 @@ function ProjectList(): JSX.Element {
}); });
}, },
}); });
const [ const [deleteProject, { loading: deleteProjLoading, error: deleteProjErr }] = useMutation(DELETE_PROJECT, {
deleteProject,
{ loading: deleteProjLoading, error: deleteProjErr },
] = useMutation(DELETE_PROJECT, {
errorPolicy: "all", errorPolicy: "all",
onError: () => { }, onError: () => {},
update(cache) { update(cache) {
cache.modify({ cache.modify({
fields: { fields: {
@ -169,21 +149,21 @@ function ProjectList(): JSX.Element {
}, },
}); });
const [deleteProj, setDeleteProj] = useState(null); const [deleteProj, setDeleteProj] = useState<Project>();
const [deleteDiagOpen, setDeleteDiagOpen] = useState(false); const [deleteDiagOpen, setDeleteDiagOpen] = useState(false);
const handleDeleteButtonClick = (project: any) => { const handleDeleteButtonClick = (project: any) => {
setDeleteProj(project); setDeleteProj(project);
setDeleteDiagOpen(true); setDeleteDiagOpen(true);
}; };
const handleDeleteConfirm = () => { const handleDeleteConfirm = () => {
deleteProject({ variables: { id: deleteProj.id } }); deleteProject({ variables: { id: deleteProj?.id } });
}; };
const handleDeleteCancel = () => { const handleDeleteCancel = () => {
setDeleteDiagOpen(false); setDeleteDiagOpen(false);
}; };
const [deleteNotifOpen, setDeleteNotifOpen] = useState(false); const [deleteNotifOpen, setDeleteNotifOpen] = useState(false);
const handleCloseDeleteNotif = (_, reason?: string) => { const handleCloseDeleteNotif = (_: Event | React.SyntheticEvent, reason?: string) => {
if (reason === "clickaway") { if (reason === "clickaway") {
return; return;
} }
@ -198,23 +178,25 @@ function ProjectList(): JSX.Element {
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>
Deleting a project permanently removes its database file from disk. Deleting a project permanently removes all its data from the database. This action is irreversible.
This action is irreversible.
</DialogContentText> </DialogContentText>
{deleteProjErr && ( {deleteProjErr && <Alert severity="error">Error closing project: {deleteProjErr.message}</Alert>}
<Alert severity="error">
Error closing project: {deleteProjErr.message}
</Alert>
)}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleDeleteCancel} autoFocus> <Button onClick={handleDeleteCancel} autoFocus color="secondary" variant="contained">
Cancel Cancel
</Button> </Button>
<Button <Button
className={classes.deleteProjectButton} sx={{
color: "white",
backgroundColor: "error.main",
"&:hover": {
backgroundColor: "error.dark",
},
}}
onClick={handleDeleteConfirm} onClick={handleDeleteConfirm}
disabled={deleteProjLoading} disabled={deleteProjLoading}
variant="contained"
> >
Delete Delete
</Button> </Button>
@ -225,6 +207,7 @@ function ProjectList(): JSX.Element {
open={deleteNotifOpen} open={deleteNotifOpen}
autoHideDuration={3000} autoHideDuration={3000}
onClose={handleCloseDeleteNotif} onClose={handleCloseDeleteNotif}
anchorOrigin={{ horizontal: "center", vertical: "bottom" }}
> >
<Alert onClose={handleCloseDeleteNotif} severity="info"> <Alert onClose={handleCloseDeleteNotif} severity="info">
Project <strong>{deleteProj?.name}</strong> was deleted. Project <strong>{deleteProj?.name}</strong> was deleted.
@ -237,82 +220,70 @@ function ProjectList(): JSX.Element {
<Box mb={4}> <Box mb={4}>
{projLoading && <CircularProgress />} {projLoading && <CircularProgress />}
{projErr && ( {projErr && <Alert severity="error">Error fetching projects: {projErr.message}</Alert>}
<Alert severity="error"> {openProjErr && <Alert severity="error">Error opening project: {openProjErr.message}</Alert>}
Error fetching projects: {projErr.message} {closeProjErr && <Alert severity="error">Error closing project: {closeProjErr.message}</Alert>}
</Alert>
)}
{openProjErr && (
<Alert severity="error">
Error opening project: {openProjErr.message}
</Alert>
)}
{closeProjErr && (
<Alert severity="error">
Error closing project: {closeProjErr.message}
</Alert>
)}
</Box> </Box>
{projData?.projects.length > 0 && ( {projData && projData.projects.length > 0 && (
<List className={classes.projectsList}> <Paper>
{projData.projects.map((project) => ( <List>
<ListItem key={project.id}> {projData.projects.map((project) => (
<ListItemAvatar> <ListItem key={project.id}>
<Avatar <ListItemAvatar>
className={ <Avatar
project.isActive ? classes.activeProject : undefined sx={{
} ...(project.isActive && {
> color: theme.palette.secondary.dark,
<DescriptionIcon /> backgroundColor: theme.palette.primary.main,
</Avatar> }),
</ListItemAvatar> }}
<ListItemText> >
{project.name} {project.isActive && <em>(Active)</em>} <DescriptionIcon />
</ListItemText> </Avatar>
<ListItemSecondaryAction> </ListItemAvatar>
{project.isActive && ( <ListItemText>
<Tooltip title="Close project"> {project.name} {project.isActive && <em>(Active)</em>}
<IconButton onClick={() => closeProject()}> </ListItemText>
<CloseIcon /> <ListItemSecondaryAction>
</IconButton> {project.isActive && (
</Tooltip> <Tooltip title="Close project">
)} <IconButton onClick={() => closeProject()}>
{!project.isActive && ( <CloseIcon />
<Tooltip title="Open project"> </IconButton>
</Tooltip>
)}
{!project.isActive && (
<Tooltip title="Open project">
<span>
<IconButton
disabled={openProjLoading || projLoading}
onClick={() =>
openProject({
variables: { id: project.id },
})
}
>
<LaunchIcon />
</IconButton>
</span>
</Tooltip>
)}
<Tooltip title="Delete project">
<span> <span>
<IconButton <IconButton onClick={() => handleDeleteButtonClick(project)} disabled={project.isActive}>
disabled={openProjLoading || projLoading} <DeleteIcon />
onClick={() =>
openProject({
variables: { id: project.id },
})
}
>
<LaunchIcon />
</IconButton> </IconButton>
</span> </span>
</Tooltip> </Tooltip>
)} </ListItemSecondaryAction>
<Tooltip title="Delete project"> </ListItem>
<span> ))}
<IconButton </List>
onClick={() => handleDeleteButtonClick(project)} </Paper>
disabled={project.isActive}
>
<DeleteIcon />
</IconButton>
</span>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
)} )}
{projData?.projects.length === 0 && ( {projData?.projects.length === 0 && (
<Alert severity="info"> <Alert severity="info">There are no projects. Create one to get started.</Alert>
There are no projects. Create one to get started.
</Alert>
)} )}
</div> </div>
); );

View File

@ -1,10 +1,10 @@
import React, { useState } from "react"; import React, { useState } from "react";
import Button from "@material-ui/core/Button"; import Button from "@mui/material/Button";
import Dialog from "@material-ui/core/Dialog"; import Dialog from "@mui/material/Dialog";
import DialogActions from "@material-ui/core/DialogActions"; import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@material-ui/core/DialogContent"; import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText"; import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle"; import DialogTitle from "@mui/material/DialogTitle";
export function useConfirmationDialog() { export function useConfirmationDialog() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -38,12 +38,10 @@ export function ConfirmationDialog(props: ConfirmationDialog) {
> >
<DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle> <DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText id="alert-dialog-description"> <DialogContentText id="alert-dialog-description">{children}</DialogContentText>
{children}
</DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose}>Abort</Button> <Button onClick={onClose}>Cancel</Button>
<Button onClick={confirm} autoFocus> <Button onClick={confirm} autoFocus>
Confirm Confirm
</Button> </Button>

View File

@ -1,7 +1,7 @@
import dynamic from "next/dynamic"; import MonacoEditor from "@monaco-editor/react";
const MonacoEditor = dynamic(import("react-monaco-editor"), { ssr: false }); import monaco from "monaco-editor/esm/vs/editor/editor.api";
const monacoOptions = { const monacoOptions: monaco.editor.IEditorOptions = {
readOnly: true, readOnly: true,
wordWrap: "on", wordWrap: "on",
minimap: { minimap: {
@ -11,20 +11,7 @@ const monacoOptions = {
type language = "html" | "typescript" | "json"; type language = "html" | "typescript" | "json";
function editorDidMount() { function languageForContentType(contentType?: string): language | undefined {
return ((window as any).MonacoEnvironment.getWorkerUrl = (
moduleId,
label
) => {
if (label === "json") return "/_next/static/json.worker.js";
if (label === "html") return "/_next/static/html.worker.js";
if (label === "javascript") return "/_next/static/ts.worker.js";
return "/_next/static/editor.worker.js";
});
}
function languageForContentType(contentType: string): language {
switch (contentType) { switch (contentType) {
case "text/html": case "text/html":
return "html"; return "html";
@ -41,7 +28,7 @@ function languageForContentType(contentType: string): language {
interface Props { interface Props {
content: string; content: string;
contentType: string; contentType?: string;
} }
function Editor({ content, contentType }: Props): JSX.Element { function Editor({ content, contentType }: Props): JSX.Element {
@ -50,8 +37,7 @@ function Editor({ content, contentType }: Props): JSX.Element {
height={"600px"} height={"600px"}
language={languageForContentType(contentType)} language={languageForContentType(contentType)}
theme="vs-dark" theme="vs-dark"
editorDidMount={editorDidMount} options={monacoOptions}
options={monacoOptions as any}
value={content} value={content}
/> />
); );

View File

@ -1,83 +1,66 @@
import { import { Table, TableBody, TableCell, TableContainer, TableRow, Snackbar } from "@mui/material";
makeStyles, import { Alert } from "@mui/lab";
Theme,
createStyles,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
Snackbar,
} from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import React, { useState } from "react"; import React, { useState } from "react";
const useStyles = makeStyles((theme: Theme) => { const baseCellStyle = {
const paddingX = 0; px: 0,
const paddingY = theme.spacing(1) / 3; py: 0.33,
const tableCell = { verticalAlign: "top",
paddingLeft: paddingX, border: "none",
paddingRight: paddingX, whiteSpace: "nowrap" as any,
paddingTop: paddingY, overflow: "hidden",
paddingBottom: paddingY, textOverflow: "ellipsis",
verticalAlign: "top", "&:hover": {
border: "none", color: "primary.main",
whiteSpace: "nowrap" as any, whiteSpace: "inherit" as any,
overflow: "hidden", overflow: "inherit",
textOverflow: "ellipsis", textOverflow: "inherit",
"&:hover": { cursor: "copy",
color: theme.palette.secondary.main, },
whiteSpace: "inherit" as any, };
overflow: "inherit",
textOverflow: "inherit", const keyCellStyle = {
cursor: "copy", ...baseCellStyle,
}, pr: 1,
}; width: "40%",
return createStyles({ fontWeight: "bold",
root: {}, fontSize: ".75rem",
table: { };
tableLayout: "fixed",
width: "100%", const valueCellStyle = {
}, ...baseCellStyle,
keyCell: { width: "60%",
...tableCell, border: "none",
paddingRight: theme.spacing(1), fontSize: ".75rem",
width: "40%", };
fontWeight: "bold",
fontSize: ".75rem",
},
valueCell: {
...tableCell,
width: "60%",
border: "none",
fontSize: ".75rem",
},
});
});
interface Props { interface Props {
headers: Array<{ key: string; value: string }>; headers: Array<{ key: string; value: string }>;
} }
function HttpHeadersTable({ headers }: Props): JSX.Element { function HttpHeadersTable({ headers }: Props): JSX.Element {
const classes = useStyles();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleClick = (e: React.MouseEvent) => { const handleClick = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
const windowSel = window.getSelection();
if (!windowSel || !document) {
return;
}
const r = document.createRange(); const r = document.createRange();
r.selectNode(e.currentTarget); r.selectNode(e.currentTarget);
window.getSelection().removeAllRanges(); windowSel.removeAllRanges();
window.getSelection().addRange(r); windowSel.addRange(r);
document.execCommand("copy"); document.execCommand("copy");
window.getSelection().removeAllRanges(); windowSel.removeAllRanges();
setOpen(true); setOpen(true);
}; };
const handleClose = (event?: React.SyntheticEvent, reason?: string) => { const handleClose = (event: Event | React.SyntheticEvent, reason?: string) => {
if (reason === "clickaway") { if (reason === "clickaway") {
return; return;
} }
@ -92,20 +75,21 @@ function HttpHeadersTable({ headers }: Props): JSX.Element {
Copied to clipboard. Copied to clipboard.
</Alert> </Alert>
</Snackbar> </Snackbar>
<TableContainer className={classes.root}> <TableContainer>
<Table className={classes.table} size="small"> <Table
sx={{
tableLayout: "fixed",
width: "100%",
}}
size="small"
>
<TableBody> <TableBody>
{headers.map(({ key, value }, index) => ( {headers.map(({ key, value }, index) => (
<TableRow key={index}> <TableRow key={index}>
<TableCell <TableCell component="th" scope="row" sx={keyCellStyle} onClick={handleClick}>
component="th"
scope="row"
className={classes.keyCell}
onClick={handleClick}
>
<code>{key}:</code> <code>{key}:</code>
</TableCell> </TableCell>
<TableCell className={classes.valueCell} onClick={handleClick}> <TableCell sx={valueCellStyle} onClick={handleClick}>
<code>{value}</code> <code>{value}</code>
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@ -1,31 +1,25 @@
import { Theme, withTheme } from "@material-ui/core"; import { SvgIconTypeMap } from "@mui/material";
import { orange, red } from "@material-ui/core/colors"; import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
import FiberManualRecordIcon from "@material-ui/icons/FiberManualRecord";
interface Props { interface Props {
status: number; status: number;
theme: Theme;
} }
function HttpStatusIcon({ status, theme }: Props): JSX.Element { export default function HttpStatusIcon({ status }: Props): JSX.Element {
const style = { marginTop: "-.25rem", verticalAlign: "middle" }; let color: SvgIconTypeMap["props"]["color"] = "inherit";
switch (Math.floor(status / 100)) { switch (Math.floor(status / 100)) {
case 2: case 2:
case 3: case 3:
return ( color = "primary";
<FiberManualRecordIcon break;
style={{ ...style, color: theme.palette.secondary.main }}
/>
);
case 4: case 4:
return ( color = "warning";
<FiberManualRecordIcon style={{ ...style, color: orange["A400"] }} /> break;
);
case 5: case 5:
return <FiberManualRecordIcon style={{ ...style, color: red["A400"] }} />; color = "error";
default: break;
return <FiberManualRecordIcon style={style} />;
} }
}
export default withTheme(HttpStatusIcon); return <FiberManualRecordIcon sx={{ marginTop: "-.25rem", verticalAlign: "middle" }} color={color} />;
}

View File

@ -1,9 +1,9 @@
import { gql, useQuery } from "@apollo/client"; import { gql, useQuery } from "@apollo/client";
import { Box, Grid, Paper, CircularProgress } from "@material-ui/core"; import { Box, Grid, Paper, CircularProgress } from "@mui/material";
import ResponseDetail from "./ResponseDetail"; import ResponseDetail from "./ResponseDetail";
import RequestDetail from "./RequestDetail"; import RequestDetail from "./RequestDetail";
import Alert from "@material-ui/lab/Alert"; import Alert from "@mui/lab/Alert";
const HTTP_REQUEST_LOG = gql` const HTTP_REQUEST_LOG = gql`
query HttpRequestLog($id: ID!) { query HttpRequestLog($id: ID!) {
@ -44,11 +44,7 @@ function LogDetail({ requestId: id }: Props): JSX.Element {
return <CircularProgress />; return <CircularProgress />;
} }
if (error) { if (error) {
return ( return <Alert severity="error">Error fetching logs details: {error.message}</Alert>;
<Alert severity="error">
Error fetching logs details: {error.message}
</Alert>
);
} }
if (!data.httpRequestLog) { if (!data.httpRequestLog) {

View File

@ -1,12 +1,7 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
import { import { Box, CircularProgress, Link as MaterialLink, Typography } from "@mui/material";
Box, import Alert from "@mui/lab/Alert";
CircularProgress,
Link as MaterialLink,
Typography,
} from "@material-ui/core";
import Alert from "@material-ui/lab/Alert";
import RequestList from "./RequestList"; import RequestList from "./RequestList";
import LogDetail from "./LogDetail"; import LogDetail from "./LogDetail";
@ -33,7 +28,7 @@ function LogsOverview(): JSX.Element {
<Alert severity="info"> <Alert severity="info">
There is no project active.{" "} There is no project active.{" "}
<Link href="/projects" passHref> <Link href="/projects" passHref>
<MaterialLink color="secondary">Create or open</MaterialLink> <MaterialLink color="primary">Create or open</MaterialLink>
</Link>{" "} </Link>{" "}
one first. one first.
</Alert> </Alert>
@ -47,11 +42,7 @@ function LogsOverview(): JSX.Element {
return ( return (
<div> <div>
<Box mb={2}> <Box mb={2}>
<RequestList <RequestList logs={logs || []} selectedReqLogId={detailReqLogId} onLogClick={handleLogClick} />
logs={logs}
selectedReqLogId={detailReqLogId}
onLogClick={handleLogClick}
/>
</Box> </Box>
<Box> <Box>
{detailReqLogId && <LogDetail requestId={detailReqLogId} />} {detailReqLogId && <LogDetail requestId={detailReqLogId} />}

View File

@ -1,42 +1,9 @@
import React from "react"; import React from "react";
import { import { Typography, Box, Divider } from "@mui/material";
Typography,
Box,
createStyles,
makeStyles,
Theme,
Divider,
} from "@material-ui/core";
import HttpHeadersTable from "./HttpHeadersTable"; import HttpHeadersTable from "./HttpHeadersTable";
import Editor from "./Editor"; import Editor from "./Editor";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
requestTitle: {
width: "calc(100% - 80px)",
fontSize: "1rem",
wordBreak: "break-all",
whiteSpace: "pre-wrap",
},
headersTable: {
tableLayout: "fixed",
width: "100%",
},
headerKeyCell: {
verticalAlign: "top",
width: "30%",
fontWeight: "bold",
},
headerValueCell: {
width: "70%",
verticalAlign: "top",
wordBreak: "break-all",
whiteSpace: "pre-wrap",
},
})
);
interface Props { interface Props {
request: { request: {
method: string; method: string;
@ -49,29 +16,27 @@ interface Props {
function RequestDetail({ request }: Props): JSX.Element { function RequestDetail({ request }: Props): JSX.Element {
const { method, url, proto, headers, body } = request; const { method, url, proto, headers, body } = request;
const classes = useStyles();
const contentType = headers.find((header) => header.key === "Content-Type") const contentType = headers.find((header) => header.key === "Content-Type")?.value;
?.value;
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
return ( return (
<div> <div>
<Box p={2}> <Box p={2}>
<Typography <Typography variant="overline" color="textSecondary" style={{ float: "right" }}>
variant="overline"
color="textSecondary"
style={{ float: "right" }}
>
Request Request
</Typography> </Typography>
<Typography className={classes.requestTitle} variant="h6"> <Typography
sx={{
width: "calc(100% - 80px)",
fontSize: "1rem",
wordBreak: "break-all",
whiteSpace: "pre-wrap",
}}
variant="h6"
>
{method} {decodeURIComponent(parsedUrl.pathname + parsedUrl.search)}{" "} {method} {decodeURIComponent(parsedUrl.pathname + parsedUrl.search)}{" "}
<Typography <Typography component="span" color="textSecondary" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
component="span"
color="textSecondary"
style={{ fontFamily: "'JetBrains Mono', monospace" }}
>
{proto} {proto}
</Typography> </Typography>
</Typography> </Typography>

View File

@ -8,48 +8,23 @@ import {
TableBody, TableBody,
Typography, Typography,
Box, Box,
createStyles, useTheme,
makeStyles, } from "@mui/material";
Theme,
withTheme,
} from "@material-ui/core";
import HttpStatusIcon from "./HttpStatusCode"; import HttpStatusIcon from "./HttpStatusCode";
import CenteredPaper from "../CenteredPaper"; import CenteredPaper from "../CenteredPaper";
import { RequestLog } from "../../lib/requestLogs";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
row: {
"&:hover": {
cursor: "pointer",
},
},
/* Pseudo-class applied to the root element if `hover={true}`. */
hover: {},
})
);
interface Props { interface Props {
logs: Array<any>; logs: RequestLog[];
selectedReqLogId?: string; selectedReqLogId?: string;
onLogClick(requestId: string): void; onLogClick(requestId: string): void;
theme: Theme;
} }
function RequestList({ export default function RequestList({ logs, onLogClick, selectedReqLogId }: Props): JSX.Element {
logs,
onLogClick,
selectedReqLogId,
theme,
}: Props): JSX.Element {
return ( return (
<div> <div>
<RequestListTable <RequestListTable onLogClick={onLogClick} logs={logs} selectedReqLogId={selectedReqLogId} />
onLogClick={onLogClick}
logs={logs}
selectedReqLogId={selectedReqLogId}
theme={theme}
/>
{logs.length === 0 && ( {logs.length === 0 && (
<Box my={1}> <Box my={1}>
<CenteredPaper> <CenteredPaper>
@ -62,19 +37,14 @@ function RequestList({
} }
interface RequestListTableProps { interface RequestListTableProps {
logs?: any; logs: RequestLog[];
selectedReqLogId?: string; selectedReqLogId?: string;
onLogClick(requestId: string): void; onLogClick(requestId: string): void;
theme: Theme;
} }
function RequestListTable({ function RequestListTable({ logs, selectedReqLogId, onLogClick }: RequestListTableProps): JSX.Element {
logs, const theme = useTheme();
selectedReqLogId,
onLogClick,
theme,
}: RequestListTableProps): JSX.Element {
const classes = useStyles();
return ( return (
<TableContainer <TableContainer
component={Paper} component={Paper}
@ -102,26 +72,25 @@ function RequestListTable({
textOverflow: "ellipsis", textOverflow: "ellipsis",
} as any; } as any;
const rowStyle = {
backgroundColor:
id === selectedReqLogId && theme.palette.action.selected,
};
return ( return (
<TableRow <TableRow
key={id} key={id}
className={classes.row} sx={{
style={rowStyle} "&:hover": {
cursor: "pointer",
},
...(id === selectedReqLogId && {
bgcolor: theme.palette.action.selected,
}),
}}
hover hover
onClick={() => onLogClick(id)} onClick={() => onLogClick(id)}
> >
<TableCell style={{ ...cellStyle, width: "100px" }}> <TableCell style={{ ...cellStyle, width: "100px" }}>
<code>{method}</code> <code>{method}</code>
</TableCell> </TableCell>
<TableCell style={{ ...cellStyle, maxWidth: "100px" }}> <TableCell sx={{ ...cellStyle, maxWidth: "100px" }}>{origin}</TableCell>
{origin} <TableCell sx={{ ...cellStyle, maxWidth: "200px" }}>
</TableCell>
<TableCell style={{ ...cellStyle, maxWidth: "200px" }}>
{decodeURIComponent(pathname + search + hash)} {decodeURIComponent(pathname + search + hash)}
</TableCell> </TableCell>
<TableCell style={{ maxWidth: "100px" }}> <TableCell style={{ maxWidth: "100px" }}>
@ -142,5 +111,3 @@ function RequestListTable({
</TableContainer> </TableContainer>
); );
} }
export default withTheme(RequestList);

View File

@ -1,4 +1,4 @@
import { Typography, Box, Divider } from "@material-ui/core"; import { Typography, Box, Divider } from "@mui/material";
import HttpStatusIcon from "./HttpStatusCode"; import HttpStatusIcon from "./HttpStatusCode";
import Editor from "./Editor"; import Editor from "./Editor";
@ -15,30 +15,17 @@ interface Props {
} }
function ResponseDetail({ response }: Props): JSX.Element { function ResponseDetail({ response }: Props): JSX.Element {
const contentType = response.headers.find( const contentType = response.headers.find((header) => header.key === "Content-Type")?.value;
(header) => header.key === "Content-Type"
)?.value;
return ( return (
<div> <div>
<Box p={2}> <Box p={2}>
<Typography <Typography variant="overline" color="textSecondary" style={{ float: "right" }}>
variant="overline"
color="textSecondary"
style={{ float: "right" }}
>
Response Response
</Typography> </Typography>
<Typography <Typography variant="h6" style={{ fontSize: "1rem", whiteSpace: "nowrap" }}>
variant="h6"
style={{ fontSize: "1rem", whiteSpace: "nowrap" }}
>
<HttpStatusIcon status={response.statusCode} />{" "} <HttpStatusIcon status={response.statusCode} />{" "}
<Typography component="span" color="textSecondary"> <Typography component="span" color="textSecondary">
<Typography <Typography component="span" color="textSecondary" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
component="span"
color="textSecondary"
style={{ fontFamily: "'JetBrains Mono', monospace" }}
>
{response.proto} {response.proto}
</Typography> </Typography>
</Typography>{" "} </Typography>{" "}
@ -52,9 +39,7 @@ function ResponseDetail({ response }: Props): JSX.Element {
<HttpHeadersTable headers={response.headers} /> <HttpHeadersTable headers={response.headers} />
</Box> </Box>
{response.body && ( {response.body && <Editor content={response.body} contentType={contentType} />}
<Editor content={response.body} contentType={contentType} />
)}
</div> </div>
); );
} }

View File

@ -3,29 +3,23 @@ import {
Checkbox, Checkbox,
CircularProgress, CircularProgress,
ClickAwayListener, ClickAwayListener,
createStyles,
FormControlLabel, FormControlLabel,
InputBase, InputBase,
makeStyles,
Paper, Paper,
Popper, Popper,
Theme,
Tooltip, Tooltip,
useTheme, useTheme,
} from "@material-ui/core"; } from "@mui/material";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@mui/material/IconButton";
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@mui/icons-material/Search";
import FilterListIcon from "@material-ui/icons/FilterList"; import FilterListIcon from "@mui/icons-material/FilterList";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { gql, useMutation, useQuery } from "@apollo/client"; import { gql, useMutation, useQuery } from "@apollo/client";
import { withoutTypename } from "../../lib/omitTypename"; import { withoutTypename } from "../../lib/omitTypename";
import { Alert } from "@material-ui/lab"; import { Alert } from "@mui/lab";
import { useClearHTTPRequestLog } from "./hooks/useClearHTTPRequestLog"; import { useClearHTTPRequestLog } from "./hooks/useClearHTTPRequestLog";
import { import { ConfirmationDialog, useConfirmationDialog } from "./ConfirmationDialog";
ConfirmationDialog,
useConfirmationDialog,
} from "./ConfirmationDialog";
const FILTER = gql` const FILTER = gql`
query HttpRequestLogFilter { query HttpRequestLogFilter {
@ -45,79 +39,43 @@ const SET_FILTER = gql`
} }
`; `;
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
padding: "2px 4px",
display: "flex",
alignItems: "center",
width: 400,
},
input: {
marginLeft: theme.spacing(1),
flex: 1,
},
iconButton: {
padding: 10,
},
filterPopper: {
width: 400,
marginTop: 6,
zIndex: 99,
},
filterOptions: {
padding: theme.spacing(2),
},
filterLoading: {
marginRight: 1,
color: theme.palette.text.primary,
},
})
);
export interface SearchFilter { export interface SearchFilter {
onlyInScope: boolean; onlyInScope: boolean;
searchExpression: string; searchExpression: string;
} }
function Search(): JSX.Element { function Search(): JSX.Element {
const classes = useStyles();
const theme = useTheme(); const theme = useTheme();
const [searchExpr, setSearchExpr] = useState(""); const [searchExpr, setSearchExpr] = useState("");
const { loading: filterLoading, error: filterErr, data: filter } = useQuery( const {
FILTER, loading: filterLoading,
{ error: filterErr,
onCompleted: (data) => { data: filter,
setSearchExpr(data.httpRequestLogFilter?.searchExpression || ""); } = useQuery(FILTER, {
}, onCompleted: (data) => {
} setSearchExpr(data.httpRequestLogFilter?.searchExpression || "");
); },
});
const [ const [setFilterMutate, { error: setFilterErr, loading: setFilterLoading }] = useMutation<{
setFilterMutate,
{ error: setFilterErr, loading: setFilterLoading },
] = useMutation<{
setHttpRequestLogFilter: SearchFilter | null; setHttpRequestLogFilter: SearchFilter | null;
}>(SET_FILTER, { }>(SET_FILTER, {
update(cache, { data: { setHttpRequestLogFilter } }) { update(cache, { data }) {
cache.writeQuery({ cache.writeQuery({
query: FILTER, query: FILTER,
data: { data: {
httpRequestLogFilter: setHttpRequestLogFilter, httpRequestLogFilter: data?.setHttpRequestLogFilter,
}, },
}); });
}, },
onError: () => {}, onError: () => {},
}); });
const [ const [clearHTTPRequestLog, clearHTTPRequestLogResult] = useClearHTTPRequestLog();
clearHTTPRequestLog,
clearHTTPRequestLogResult,
] = useClearHTTPRequestLog();
const clearHTTPConfirmationDialog = useConfirmationDialog(); const clearHTTPConfirmationDialog = useConfirmationDialog();
const filterRef = useRef<HTMLElement | null>(); const filterRef = useRef<HTMLFormElement>(null);
const [filterOpen, setFilterOpen] = useState(false); const [filterOpen, setFilterOpen] = useState(false);
const handleSubmit = (e: React.SyntheticEvent) => { const handleSubmit = (e: React.SyntheticEvent) => {
@ -133,8 +91,8 @@ function Search(): JSX.Element {
e.preventDefault(); e.preventDefault();
}; };
const handleClickAway = (event: React.MouseEvent<EventTarget>) => { const handleClickAway = (event: MouseEvent | TouchEvent) => {
if (filterRef.current.contains(event.target as HTMLElement)) { if (filterRef?.current && filterRef.current.contains(event.target as HTMLElement)) {
return; return;
} }
setFilterOpen(false); setFilterOpen(false);
@ -144,63 +102,67 @@ function Search(): JSX.Element {
<Box> <Box>
<Error prefix="Error fetching filter" error={filterErr} /> <Error prefix="Error fetching filter" error={filterErr} />
<Error prefix="Error setting filter" error={setFilterErr} /> <Error prefix="Error setting filter" error={setFilterErr} />
<Error <Error prefix="Error clearing all HTTP logs" error={clearHTTPRequestLogResult.error} />
prefix="Error clearing all HTTP logs"
error={clearHTTPRequestLogResult.error}
/>
<Box style={{ display: "flex", flex: 1 }}> <Box style={{ display: "flex", flex: 1 }}>
<ClickAwayListener onClickAway={handleClickAway}> <ClickAwayListener onClickAway={handleClickAway}>
<Paper <Paper
component="form" component="form"
onSubmit={handleSubmit} onSubmit={handleSubmit}
ref={filterRef} ref={filterRef}
className={classes.root} sx={{
padding: "2px 4px",
display: "flex",
alignItems: "center",
width: 400,
}}
> >
<Tooltip title="Toggle filter options"> <Tooltip title="Toggle filter options">
<IconButton <IconButton
className={classes.iconButton}
onClick={() => setFilterOpen(!filterOpen)} onClick={() => setFilterOpen(!filterOpen)}
style={{ sx={{
color: filter?.httpRequestLogFilter?.onlyInScope p: 1,
? theme.palette.secondary.main color: filter?.httpRequestLogFilter?.onlyInScope ? "primary.main" : "inherit",
: "inherit",
}} }}
> >
{filterLoading || setFilterLoading ? ( {filterLoading || setFilterLoading ? (
<CircularProgress <CircularProgress sx={{ color: theme.palette.text.primary }} size={23} />
className={classes.filterLoading}
size={23}
/>
) : ( ) : (
<FilterListIcon /> <FilterListIcon />
)} )}
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<InputBase <InputBase
className={classes.input} sx={{
ml: 1,
flex: 1,
}}
placeholder="Search proxy logs…" placeholder="Search proxy logs…"
value={searchExpr} value={searchExpr}
onChange={(e) => setSearchExpr(e.target.value)} onChange={(e) => setSearchExpr(e.target.value)}
onFocus={() => setFilterOpen(true)} onFocus={() => setFilterOpen(true)}
/> />
<Tooltip title="Search"> <Tooltip title="Search">
<IconButton type="submit" className={classes.iconButton}> <IconButton type="submit" sx={{ padding: 1.25 }}>
<SearchIcon /> <SearchIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Popper <Popper
className={classes.filterPopper}
open={filterOpen} open={filterOpen}
anchorEl={filterRef.current} anchorEl={filterRef.current}
placement="bottom-start" placement="bottom"
style={{ zIndex: theme.zIndex.appBar }}
> >
<Paper className={classes.filterOptions}> <Paper
sx={{
width: 400,
marginTop: 0.5,
p: 1.5,
}}
>
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <Checkbox
checked={ checked={filter?.httpRequestLogFilter?.onlyInScope ? true : false}
filter?.httpRequestLogFilter?.onlyInScope ? true : false
}
disabled={filterLoading || setFilterLoading} disabled={filterLoading || setFilterLoading}
onChange={(e) => onChange={(e) =>
setFilterMutate({ setFilterMutate({

View File

@ -3,20 +3,18 @@ import {
Box, Box,
Button, Button,
CircularProgress, CircularProgress,
createStyles,
FormControl, FormControl,
FormControlLabel, FormControlLabel,
FormLabel, FormLabel,
makeStyles,
Radio, Radio,
RadioGroup, RadioGroup,
TextField, TextField,
Theme, } from "@mui/material";
} from "@material-ui/core"; import AddIcon from "@mui/icons-material/Add";
import AddIcon from "@material-ui/icons/Add"; import { Alert } from "@mui/lab";
import { Alert } from "@material-ui/lab";
import React from "react"; import React from "react";
import { SCOPE } from "./Rules"; import { SCOPE } from "./Rules";
import { ScopeRule } from "../../lib/scope";
const SET_SCOPE = gql` const SET_SCOPE = gql`
mutation SetScope($scope: [ScopeRuleInput!]!) { mutation SetScope($scope: [ScopeRuleInput!]!) {
@ -26,25 +24,15 @@ const SET_SCOPE = gql`
} }
`; `;
const useStyles = makeStyles((theme: Theme) =>
createStyles({
ruleExpression: {
fontFamily: "'JetBrains Mono', monospace",
},
})
);
function AddRule(): JSX.Element { function AddRule(): JSX.Element {
const classes = useStyles();
const [ruleType, setRuleType] = React.useState("url"); const [ruleType, setRuleType] = React.useState("url");
const [expression, setExpression] = React.useState(null); const [expression, setExpression] = React.useState("");
const client = useApolloClient(); const client = useApolloClient();
const [setScope, { error, loading }] = useMutation(SET_SCOPE, { const [setScope, { error, loading }] = useMutation(SET_SCOPE, {
onError() {}, onError() {},
onCompleted() { onCompleted() {
expression.value = ""; setExpression("");
}, },
update(_, { data: { setScope } }) { update(_, { data: { setScope } }) {
client.writeQuery({ client.writeQuery({
@ -59,21 +47,20 @@ function AddRule(): JSX.Element {
}; };
const handleSubmit = (e: React.SyntheticEvent) => { const handleSubmit = (e: React.SyntheticEvent) => {
e.preventDefault(); e.preventDefault();
let scope = []; let scope: ScopeRule[] = [];
try { try {
const data = client.readQuery({ const data = client.readQuery<{ scope: ScopeRule[] }>({
query: SCOPE, query: SCOPE,
}); });
scope = data.scope; if (data) {
scope = data.scope;
}
} catch (e) {} } catch (e) {}
setScope({ setScope({
variables: { variables: {
scope: [ scope: [...scope.map(({ url }) => ({ url })), { url: expression }],
...scope.map(({ url }) => ({ url })),
{ url: expression.value },
],
}, },
}); });
}; };
@ -87,15 +74,10 @@ function AddRule(): JSX.Element {
)} )}
<form onSubmit={handleSubmit} autoComplete="off"> <form onSubmit={handleSubmit} autoComplete="off">
<FormControl fullWidth> <FormControl fullWidth>
<FormLabel color="secondary" component="legend"> <FormLabel color="primary" component="legend">
Rule Type Rule Type
</FormLabel> </FormLabel>
<RadioGroup <RadioGroup row name="ruleType" value={ruleType} onChange={handleTypeChange}>
row
name="ruleType"
value={ruleType}
onChange={handleTypeChange}
>
<FormControlLabel value="url" control={<Radio />} label="URL" /> <FormControlLabel value="url" control={<Radio />} label="URL" />
</RadioGroup> </RadioGroup>
</FormControl> </FormControl>
@ -104,20 +86,17 @@ function AddRule(): JSX.Element {
label="Expression" label="Expression"
placeholder="^https:\/\/(.*)example.com(.*)" placeholder="^https:\/\/(.*)example.com(.*)"
helperText="Regular expression to match on." helperText="Regular expression to match on."
color="secondary" color="primary"
variant="outlined" variant="outlined"
required required
value={expression}
onChange={(e) => setExpression(e.target.value)}
InputProps={{ InputProps={{
className: classes.ruleExpression, sx: { fontFamily: "'JetBrains Mono', monospace" },
}} }}
InputLabelProps={{ InputLabelProps={{
shrink: true, shrink: true,
}} }}
inputProps={{
ref: (node) => {
setExpression(node);
},
}}
margin="normal" margin="normal"
/> />
</FormControl> </FormControl>
@ -125,7 +104,7 @@ function AddRule(): JSX.Element {
<Button <Button
type="submit" type="submit"
variant="contained" variant="contained"
color="secondary" color="primary"
disabled={loading} disabled={loading}
startIcon={loading ? <CircularProgress size={22} /> : <AddIcon />} startIcon={loading ? <CircularProgress size={22} /> : <AddIcon />}
> >

View File

@ -8,11 +8,12 @@ import {
ListItemSecondaryAction, ListItemSecondaryAction,
ListItemText, ListItemText,
Tooltip, Tooltip,
} from "@material-ui/core"; } from "@mui/material";
import CodeIcon from "@material-ui/icons/Code"; import CodeIcon from "@mui/icons-material/Code";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import React from "react"; import React from "react";
import { SCOPE } from "./Rules"; import { SCOPE } from "./Rules";
import { ScopeRule } from "../../lib/scope";
const SET_SCOPE = gql` const SET_SCOPE = gql`
mutation SetScope($scope: [ScopeRuleInput!]!) { mutation SetScope($scope: [ScopeRuleInput!]!) {
@ -22,7 +23,13 @@ const SET_SCOPE = gql`
} }
`; `;
function RuleListItem({ scope, rule, index }): JSX.Element { type RuleListItemProps = {
scope: ScopeRule[];
rule: ScopeRule;
index: number;
};
function RuleListItem({ scope, rule, index }: RuleListItemProps): JSX.Element {
const client = useApolloClient(); const client = useApolloClient();
const [setScope, { loading }] = useMutation(SET_SCOPE, { const [setScope, { loading }] = useMutation(SET_SCOPE, {
update(_, { data: { setScope } }) { update(_, { data: { setScope } }) {
@ -65,8 +72,8 @@ function RuleListItem({ scope, rule, index }): JSX.Element {
); );
} }
function RuleListItemText({ rule }): JSX.Element { function RuleListItemText({ rule }: { rule: ScopeRule }): JSX.Element {
let text: JSX.Element; let text: JSX.Element = <div></div>;
if (rule.url) { if (rule.url) {
text = <code>{rule.url}</code>; text = <code>{rule.url}</code>;
@ -77,10 +84,14 @@ function RuleListItemText({ rule }): JSX.Element {
return <ListItemText>{text}</ListItemText>; return <ListItemText>{text}</ListItemText>;
} }
function RuleTypeChip({ rule }): JSX.Element { function RuleTypeChip({ rule }: { rule: ScopeRule }): JSX.Element {
let label = "Unknown";
if (rule.url) { if (rule.url) {
return <Chip label="URL" variant="outlined" />; label = "URL";
} }
return <Chip label={label} variant="outlined" />;
} }
export default RuleListItem; export default RuleListItem;

View File

@ -1,22 +1,9 @@
import { gql, useQuery } from "@apollo/client"; import { gql, useQuery } from "@apollo/client";
import { import { CircularProgress, List } from "@mui/material";
CircularProgress, import { Alert } from "@mui/lab";
createStyles,
List,
makeStyles,
Theme,
} from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import React from "react"; import React from "react";
import RuleListItem from "./RuleListItem"; import RuleListItem from "./RuleListItem";
import { ScopeRule } from "../../lib/scope";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
rulesList: {
backgroundColor: theme.palette.background.paper,
},
})
);
export const SCOPE = gql` export const SCOPE = gql`
query Scope { query Scope {
@ -27,24 +14,20 @@ export const SCOPE = gql`
`; `;
function Rules(): JSX.Element { function Rules(): JSX.Element {
const classes = useStyles(); const { loading, error, data } = useQuery<{ scope: ScopeRule[] }>(SCOPE);
const { loading, error, data } = useQuery(SCOPE);
return ( return (
<div> <div>
{loading && <CircularProgress />} {loading && <CircularProgress />}
{error && ( {error && <Alert severity="error">Error fetching scope: {error.message}</Alert>}
<Alert severity="error">Error fetching scope: {error.message}</Alert> {data && data.scope.length > 0 && (
)} <List
{data?.scope.length > 0 && ( sx={{
<List className={classes.rulesList}> bgcolor: "background.paper",
}}
>
{data.scope.map((rule, index) => ( {data.scope.map((rule, index) => (
<RuleListItem <RuleListItem key={index} rule={rule} scope={data.scope} index={index} />
key={index}
rule={rule}
scope={data.scope}
index={index}
/>
))} ))}
</List> </List>
)} )}

5
admin/src/lib/Project.ts Normal file
View File

@ -0,0 +1,5 @@
export type Project = {
id: string
name: string
isActive: boolean
}

View File

@ -0,0 +1,7 @@
import createCache from "@emotion/cache";
// prepend: true moves MUI styles to the top of the <head> so they're loaded first.
// It allows developers to easily override MUI styles with other styling solutions, like CSS modules.
export default function createEmotionCache() {
return createCache({ key: "css", prepend: true });
}

View File

@ -1,7 +1,11 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; import { ApolloClient, HttpLink, InMemoryCache, NormalizedCacheObject } from "@apollo/client";
import merge from "deepmerge";
import isEqual from "lodash/isEqual";
let apolloClient; export const APOLLO_STATE_PROP_NAME = "__APOLLO_STATE__";
let apolloClient: ApolloClient<NormalizedCacheObject>;
function createApolloClient() { function createApolloClient() {
return new ApolloClient({ return new ApolloClient({
@ -21,9 +25,18 @@ export function initializeApollo(initialState = null) {
if (initialState) { if (initialState) {
// Get existing cache, loaded during client side data fetching // Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract(); const existingCache = _apolloClient.extract();
// Restore the cache using the data passed from getStaticProps/getServerSideProps
// combined with the existing cached data // Merge the existing cache into data passed from getStaticProps/getServerSideProps
_apolloClient.cache.restore({ ...existingCache, ...initialState }); const data = merge(initialState, existingCache, {
// combine arrays using object equality (like in sets)
arrayMerge: (destinationArray, sourceArray) => [
...sourceArray,
...destinationArray.filter((d) => sourceArray.every((s) => !isEqual(d, s))),
],
});
// Restore the cache with the merged data
_apolloClient.cache.restore(data);
} }
// For SSG and SSR always create a new Apollo Client // For SSG and SSR always create a new Apollo Client
if (typeof window === "undefined") return _apolloClient; if (typeof window === "undefined") return _apolloClient;
@ -33,7 +46,16 @@ export function initializeApollo(initialState = null) {
return _apolloClient; return _apolloClient;
} }
export function useApollo(initialState) { export function addApolloState(client: ApolloClient<NormalizedCacheObject>, pageProps: any) {
const store = useMemo(() => initializeApollo(initialState), [initialState]); if (pageProps?.props) {
pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract();
}
return pageProps;
}
export function useApollo(pageProps: any) {
const state = pageProps[APOLLO_STATE_PROP_NAME];
const store = useMemo(() => initializeApollo(state), [state]);
return store; return store;
} }

View File

@ -1,4 +1,4 @@
const omitTypename = (key, value) => (key === "__typename" ? undefined : value); const omitTypename = (key: string, value: any) => (key === "__typename" ? undefined : value);
export function withoutTypename(input: any): any { export function withoutTypename(input: any): any {
return JSON.parse(JSON.stringify(input), omitTypename); return JSON.parse(JSON.stringify(input), omitTypename);

View File

@ -0,0 +1,24 @@
export type RequestLog = {
id: string
url: string
method: string
proto: string
headers: HTTPHeader[]
body?: string
timestamp: string
response?: ResponseLog
}
export type ResponseLog = {
proto: string
statusCode: number
statusReason: string
body?: string
headers: HTTPHeader[]
}
export type HTTPHeader = {
key: string
value: string
}

3
admin/src/lib/scope.ts Normal file
View File

@ -0,0 +1,3 @@
export type ScopeRule = {
url?: string
}

View File

@ -1,49 +1,51 @@
import { createMuiTheme } from "@material-ui/core/styles"; import { createTheme } from "@mui/material/styles";
import grey from "@material-ui/core/colors/grey"; import * as colors from "@mui/material/colors";
import teal from "@material-ui/core/colors/teal";
const theme = createMuiTheme({ const heading = {
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 600,
};
let theme = createTheme({
palette: { palette: {
type: "dark", mode: "dark",
primary: { primary: {
main: grey[900], main: colors.teal["A400"],
}, },
secondary: { secondary: {
main: teal["A400"], main: colors.grey[900],
}, light: "#333",
info: { dark: colors.common.black,
main: teal["A400"],
},
success: {
main: teal["A400"],
}, },
}, },
typography: { typography: {
h2: { h2: heading,
fontFamily: "'JetBrains Mono', monospace", h3: heading,
fontWeight: 600, h4: heading,
h5: heading,
h6: heading,
},
});
theme = createTheme(theme, {
palette: {
background: {
default: theme.palette.secondary.main,
paper: theme.palette.secondary.light,
}, },
h3: { info: {
fontFamily: "'JetBrains Mono', monospace", main: theme.palette.primary.main,
fontWeight: 600,
}, },
h4: { success: {
fontFamily: "'JetBrains Mono', monospace", main: theme.palette.primary.main,
fontWeight: 600,
},
h5: {
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 600,
},
h6: {
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 600,
}, },
}, },
overrides: { components: {
MuiTableCell: { MuiTableCell: {
stickyHeader: { styleOverrides: {
backgroundColor: grey[900], stickyHeader: {
backgroundColor: theme.palette.secondary.dark,
},
}, },
}, },
}, },

View File

@ -1,32 +1,31 @@
import React from "react"; import * as React from "react";
import Head from "next/head";
import { AppProps } from "next/app"; import { AppProps } from "next/app";
import { ApolloProvider } from "@apollo/client"; import { ApolloProvider } from "@apollo/client";
import Head from "next/head"; import { ThemeProvider } from "@mui/material/styles";
import { ThemeProvider } from "@material-ui/core/styles"; import CssBaseline from "@mui/material/CssBaseline";
import CssBaseline from "@material-ui/core/CssBaseline"; import { CacheProvider, EmotionCache } from "@emotion/react";
import createEmotionCache from "../lib/createEmotionCache";
import theme from "../lib/theme"; import theme from "../lib/theme";
import { useApollo } from "../lib/graphql"; import { useApollo } from "../lib/graphql";
function App({ Component, pageProps }: AppProps): JSX.Element { // Client-side cache, shared for the whole session of the user in the browser.
const apolloClient = useApollo(pageProps.initialApolloState); const clientSideEmotionCache = createEmotionCache();
React.useEffect(() => { interface MyAppProps extends AppProps {
// Remove the server-side injected CSS. emotionCache?: EmotionCache;
const jssStyles = document.querySelector("#jss-server-side"); }
if (jssStyles) {
jssStyles.parentElement.removeChild(jssStyles); export default function MyApp(props: MyAppProps) {
} const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
}, []); const apolloClient = useApollo(pageProps);
return ( return (
<React.Fragment> <CacheProvider value={emotionCache}>
<Head> <Head>
<title>Hetty://</title> <title>Hetty://</title>
<meta <meta name="viewport" content="initial-scale=1, width=device-width" />
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
</Head> </Head>
<ApolloProvider client={apolloClient}> <ApolloProvider client={apolloClient}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
@ -34,8 +33,6 @@ function App({ Component, pageProps }: AppProps): JSX.Element {
<Component {...pageProps} /> <Component {...pageProps} />
</ThemeProvider> </ThemeProvider>
</ApolloProvider> </ApolloProvider>
</React.Fragment> </CacheProvider>
); );
} }
export default App;

View File

@ -1,7 +1,8 @@
import React from "react"; import * as React from "react";
import Document, { Html, Head, Main, NextScript } from "next/document"; import Document, { Html, Head, Main, NextScript } from "next/document";
import { ServerStyleSheets } from "@material-ui/core/styles"; import createEmotionServer from "@emotion/server/create-instance";
import createEmotionCache from "../lib/createEmotionCache";
import theme from "../lib/theme"; import theme from "../lib/theme";
export default class MyDocument extends Document { export default class MyDocument extends Document {
@ -11,14 +12,9 @@ export default class MyDocument extends Document {
<Head> <Head>
<meta name="theme-color" content={theme.palette.primary.main} /> <meta name="theme-color" content={theme.palette.primary.main} />
<link rel="stylesheet" href="/style.css" /> <link rel="stylesheet" href="/style.css" />
<link <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
rel="stylesheet" <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" />
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" {(this.props as any).emotionStyleTags}
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap"
/>
</Head> </Head>
<body> <body>
<Main /> <Main />
@ -30,25 +26,60 @@ export default class MyDocument extends Document {
} }
// `getInitialProps` belongs to `_document` (instead of `_app`), // `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG). // it's compatible with static-site generation (SSG).
MyDocument.getInitialProps = async (ctx) => { MyDocument.getInitialProps = async (ctx) => {
// Render app and page and get the context of the page with collected side effects. // Resolution order
const sheets = new ServerStyleSheets(); //
// On the server:
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. document.getInitialProps
// 4. app.render
// 5. page.render
// 6. document.render
//
// On the server with error:
// 1. document.getInitialProps
// 2. app.render
// 3. page.render
// 4. document.render
//
// On the client
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. app.render
// 4. page.render
const originalRenderPage = ctx.renderPage; const originalRenderPage = ctx.renderPage;
// You can consider sharing the same emotion cache between all the SSR requests to speed up performance.
// However, be aware that it can have global side effects.
const cache = createEmotionCache();
const { extractCriticalToChunks } = createEmotionServer(cache);
ctx.renderPage = () => ctx.renderPage = () =>
originalRenderPage({ originalRenderPage({
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />), enhanceApp: (App: any) =>
function EnhanceApp(props) {
return <App emotionCache={cache} {...props} />;
},
}); });
const initialProps = await Document.getInitialProps(ctx); const initialProps = await Document.getInitialProps(ctx);
// This is important. It prevents emotion to render invalid HTML.
// See https://github.com/mui-org/material-ui/issues/26561#issuecomment-855286153
const emotionStyles = extractCriticalToChunks(initialProps.html);
const emotionStyleTags = emotionStyles.styles.map((style) => (
<style
data-emotion={`${style.key} ${style.ids.join(" ")}`}
key={style.key}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: style.css }}
/>
));
return { return {
...initialProps, ...initialProps,
// Styles fragment is rendered after the app and page rendering finish. emotionStyleTags,
styles: [
...React.Children.toArray(initialProps.styles),
sheets.getStyleElement(),
],
}; };
}; };

View File

@ -1,4 +1,4 @@
import { Box, Link as MaterialLink, Typography } from "@material-ui/core"; import { Box, Link as MaterialLink, Typography } from "@mui/material";
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React from "react";
@ -13,17 +13,13 @@ function Index(): JSX.Element {
<Typography variant="h4">Get started</Typography> <Typography variant="h4">Get started</Typography>
</Box> </Box>
<Typography paragraph> <Typography paragraph>
Youve loaded a (new) project. Whats next? You can now use the MITM Youve loaded a (new) project. Whats next? You can now use the MITM proxy and review HTTP requests and
proxy and review HTTP requests and responses via the{" "} responses via the{" "}
<Link href="/proxy/logs" passHref> <Link href="/proxy/logs" passHref>
<MaterialLink color="secondary">Proxy logs</MaterialLink> <MaterialLink color="primary">Proxy logs</MaterialLink>
</Link> </Link>
. Stuck? Ask for help on the{" "} . Stuck? Ask for help on the{" "}
<MaterialLink <MaterialLink href="https://github.com/dstotijn/hetty/discussions" color="primary" target="_blank">
href="https://github.com/dstotijn/hetty/discussions"
color="secondary"
target="_blank"
>
Discussions forum Discussions forum
</MaterialLink> </MaterialLink>
. .

View File

@ -1,61 +1,46 @@
import { import { Box, Button, Typography } from "@mui/material";
Box, import FolderIcon from "@mui/icons-material/Folder";
Button,
createStyles,
makeStyles,
Theme,
Typography,
} from "@material-ui/core";
import FolderIcon from "@material-ui/icons/Folder";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router";
import Layout, { Page } from "../components/Layout"; import Layout, { Page } from "../components/Layout";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
titleHighlight: {
color: theme.palette.secondary.main,
},
subtitle: {
fontSize: "1.6rem",
width: "60%",
lineHeight: 2,
marginBottom: theme.spacing(5),
},
button: {
marginRight: theme.spacing(2),
},
})
);
function Index(): JSX.Element { function Index(): JSX.Element {
const classes = useStyles(); const highlightSx = { color: "primary.main" };
return ( return (
<Layout page={Page.Home} title=""> <Layout page={Page.Home} title="">
<Box p={4}> <Box p={4}>
<Box mb={4} width="60%"> <Box mb={4} width="60%">
<Typography variant="h2"> <Typography variant="h2">
<span className={classes.titleHighlight}>Hetty://</span> <Box component="span" sx={highlightSx}>
Hetty://
</Box>
<br /> <br />
The simple HTTP toolkit for security research. The simple HTTP toolkit for security research.
</Typography> </Typography>
</Box> </Box>
<Typography className={classes.subtitle} paragraph> <Typography
What if security testing was intuitive, powerful, and good looking? paragraph
What if it was <strong>free</strong>, instead of $400 per year?{" "} sx={{
<span className={classes.titleHighlight}>Hetty</span> is listening on{" "} fontSize: "1.6rem",
<code>:8080</code> width: "60%",
lineHeight: 2,
mb: 5,
}}
>
Welcome to{" "}
<Box component="span" sx={highlightSx}>
Hetty
</Box>
. Get started by creating a project.
</Typography> </Typography>
<Link href="/projects" passHref> <Link href="/projects" passHref>
<Button <Button
className={classes.button} sx={{ mr: 2 }}
variant="contained" variant="contained"
color="secondary" color="primary"
component="a" component="a"
size="large" size="large"
startIcon={<FolderIcon />} startIcon={<FolderIcon />}

View File

@ -1,4 +1,4 @@
import { Box, Divider, Grid, Typography } from "@material-ui/core"; import { Box, Divider, Grid, Typography } from "@mui/material";
import Layout, { Page } from "../../components/Layout"; import Layout, { Page } from "../../components/Layout";
import NewProject from "../../components/projects/NewProject"; import NewProject from "../../components/projects/NewProject";
import ProjectList from "../../components/projects/ProjectList"; import ProjectList from "../../components/projects/ProjectList";
@ -11,8 +11,7 @@ function Index(): JSX.Element {
<Typography variant="h4">Projects</Typography> <Typography variant="h4">Projects</Typography>
</Box> </Box>
<Typography paragraph> <Typography paragraph>
Projects contain settings and data generated/processed by Hetty. They Projects contain settings and data generated/processed by Hetty. They are stored in a single database on disk.
are stored in a single database on disk.
</Typography> </Typography>
<Box my={4}> <Box my={4}>
<Divider /> <Divider />

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { Button, Typography } from "@material-ui/core"; import { Button, Typography } from "@mui/material";
import ListIcon from "@material-ui/icons/List"; import ListIcon from "@mui/icons-material/List";
import Link from "next/link"; import Link from "next/link";
import Layout, { Page } from "../../components/Layout"; import Layout, { Page } from "../../components/Layout";
@ -10,13 +10,7 @@ function Index(): JSX.Element {
<Layout page={Page.ProxySetup} title="Proxy setup"> <Layout page={Page.ProxySetup} title="Proxy setup">
<Typography paragraph>Coming soon</Typography> <Typography paragraph>Coming soon</Typography>
<Link href="/proxy/logs" passHref> <Link href="/proxy/logs" passHref>
<Button <Button variant="contained" color="primary" component="a" size="large" startIcon={<ListIcon />}>
variant="contained"
color="secondary"
component="a"
size="large"
startIcon={<ListIcon />}
>
View logs View logs
</Button> </Button>
</Link> </Link>

View File

@ -1,4 +1,4 @@
import { Box } from "@material-ui/core"; import { Box } from "@mui/material";
import LogsOverview from "../../../components/reqlog/LogsOverview"; import LogsOverview from "../../../components/reqlog/LogsOverview";
import Layout, { Page } from "../../../components/Layout"; import Layout, { Page } from "../../../components/Layout";

View File

@ -1,4 +1,4 @@
import { Box, Divider, Grid, Typography } from "@material-ui/core"; import { Box, Divider, Grid, Typography } from "@mui/material";
import React from "react"; import React from "react";
import Layout, { Page } from "../../components/Layout"; import Layout, { Page } from "../../components/Layout";
@ -13,11 +13,9 @@ function Index(): JSX.Element {
<Typography variant="h4">Scope</Typography> <Typography variant="h4">Scope</Typography>
</Box> </Box>
<Typography paragraph> <Typography paragraph>
Scope rules are used by various modules in Hetty and can influence Scope rules are used by various modules in Hetty and can influence their behavior. For example: the Proxy logs
their behavior. For example: the Proxy logs module can match incoming module can match incoming requests against scope rules and decide its behavior (e.g. log or bypass) based on
requests against scope rules and decide its behavior (e.g. log or the outcome of the match. All scope configuration is stored per project.
bypass) based on the outcome of the match. All scope configuration is
stored per project.
</Typography> </Typography>
<Box my={4}> <Box my={4}>
<Divider /> <Divider />

View File

@ -1,4 +1,4 @@
import { Box, Typography } from "@material-ui/core"; import { Box, Typography } from "@mui/material";
import Layout, { Page } from "../../components/Layout"; import Layout, { Page } from "../../components/Layout";

View File

@ -8,7 +8,7 @@
], ],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": false, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
@ -16,7 +16,8 @@
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve" "jsx": "preserve",
"incremental": true
}, },
"include": [ "include": [
"next-env.d.ts", "next-env.d.ts",

File diff suppressed because it is too large Load Diff