Scaffold homepage, small style tweaks

This commit is contained in:
David Stotijn
2020-09-23 23:43:20 +02:00
parent 71de41e6e6
commit 8828a586a1
22 changed files with 1041 additions and 55 deletions

View File

@ -2,6 +2,7 @@ const withCSS = require("@zeit/next-css");
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
module.exports = withCSS({
trailingSlash: true,
async rewrites() {
return [
{

View File

@ -6,7 +6,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"export": "next build && next export -o build"
"export": "next build && next export"
},
"dependencies": {
"@apollo/client": "^3.2.0",

91
admin/public/style.css Normal file
View File

@ -0,0 +1,91 @@
@font-face {
font-family: "JetBrains Mono";
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold-Italic.woff2")
format("woff2"),
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Bold-Italic.woff")
format("woff");
font-weight: 700;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "JetBrains Mono";
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold.woff2")
format("woff2"),
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Bold.woff")
format("woff");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "JetBrains Mono";
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-ExtraBold-Italic.woff2")
format("woff2"),
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-ExtraBold-Italic.woff")
format("woff");
font-weight: 800;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "JetBrains Mono";
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-ExtraBold.woff2")
format("woff2"),
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-ExtraBold.woff")
format("woff");
font-weight: 800;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "JetBrains Mono";
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Italic.woff2")
format("woff2"),
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Italic.woff")
format("woff");
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "JetBrains Mono";
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Medium-Italic.woff2")
format("woff2"),
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Medium-Italic.woff")
format("woff");
font-weight: 500;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "JetBrains Mono";
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Medium.woff2")
format("woff2"),
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Medium.woff")
format("woff");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "JetBrains Mono";
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff2")
format("woff2"),
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Regular.woff")
format("woff");
font-weight: 400;
font-style: normal;
font-display: swap;
}
code {
font-family: "JetBrains Mono", monospace;
}

View File

@ -14,12 +14,13 @@ import {
ListItem,
ListItemIcon,
ListItemText,
Box,
Tooltip,
} from "@material-ui/core";
import Link from "next/link";
import MenuIcon from "@material-ui/icons/Menu";
import HomeIcon from "@material-ui/icons/Home";
import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet";
import SendIcon from "@material-ui/icons/Send";
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
import clsx from "clsx";
@ -136,7 +137,7 @@ export function Layout(props: { children: React.ReactNode }): JSX.Element {
<MenuIcon />
</IconButton>
<Typography variant="h5" noWrap>
<span style={{ marginRight: 12 }}>🧑🔧</span>Hetty
Hetty://
</Typography>
</Toolbar>
</AppBar>
@ -164,22 +165,51 @@ export function Layout(props: { children: React.ReactNode }): JSX.Element {
</div>
<Divider />
<List>
<ListItem button key="home" className={classes.listItem}>
<Tooltip title="Home">
<ListItemIcon className={classes.listItemIcon}>
<HomeIcon />
</ListItemIcon>
</Tooltip>
<ListItemText primary="Home" />
</ListItem>
<ListItem button key="proxy" className={classes.listItem}>
<Tooltip title="Proxy">
<ListItemIcon className={classes.listItemIcon}>
<SettingsEthernetIcon />
</ListItemIcon>
</Tooltip>
<ListItemText primary="Proxy" />
</ListItem>
<Link href="/" passHref>
<ListItem
button
component="a"
key="home"
className={classes.listItem}
>
<Tooltip title="Home">
<ListItemIcon className={classes.listItemIcon}>
<HomeIcon />
</ListItemIcon>
</Tooltip>
<ListItemText primary="Home" />
</ListItem>
</Link>
<Link href="/proxy/logs" passHref>
<ListItem
button
component="a"
key="proxy"
className={classes.listItem}
>
<Tooltip title="Proxy">
<ListItemIcon className={classes.listItemIcon}>
<SettingsEthernetIcon />
</ListItemIcon>
</Tooltip>
<ListItemText primary="Proxy" />
</ListItem>
</Link>
<Link href="/sender" passHref>
<ListItem
button
component="a"
key="sender"
className={classes.listItem}
>
<Tooltip title="Sender">
<ListItemIcon className={classes.listItemIcon}>
<SendIcon />
</ListItemIcon>
</Tooltip>
<ListItemText primary="Sender" />
</ListItem>
</Link>
</List>
</Drawer>
<main className={classes.content}>

View File

@ -12,7 +12,10 @@ const monacoOptions = {
type language = "html" | "typescript" | "json";
function editorDidMount() {
return (window.MonacoEnvironment.getWorkerUrl = (moduleId, label) => {
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";
@ -26,8 +29,10 @@ function languageForContentType(contentType: string): language {
case "text/html":
return "html";
case "application/json":
case "application/json; charset=utf-8":
return "json";
case "application/javascript":
case "application/javascript; charset=utf-8":
return "typescript";
default:
return;

View File

@ -9,25 +9,36 @@ import {
TableRow,
} from "@material-ui/core";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
const useStyles = makeStyles((theme: Theme) => {
const paddingX = 0;
const paddingY = theme.spacing(1) / 3;
const tableCell = {
paddingLeft: paddingX,
paddingRight: paddingX,
paddingTop: paddingY,
paddingBottom: paddingY,
verticalAlign: "top",
border: "none",
};
return createStyles({
table: {
tableLayout: "fixed",
width: "100%",
},
keyCell: {
verticalAlign: "top",
width: "30%",
...tableCell,
width: "40%",
fontWeight: "bold",
},
valueCell: {
width: "70%",
verticalAlign: "top",
...tableCell,
width: "60%",
border: "none",
wordBreak: "break-all",
whiteSpace: "pre-wrap",
},
})
);
});
});
interface Props {
headers: Array<{ key: string; value: string }>;
@ -42,7 +53,7 @@ function HttpHeadersTable({ headers }: Props): JSX.Element {
{headers.map(({ key, value }, index) => (
<TableRow key={index}>
<TableCell component="th" scope="row" className={classes.keyCell}>
<code>{key}</code>
<code>{key}:</code>
</TableCell>
<TableCell className={classes.valueCell}>
<code>{value}</code>

View File

@ -6,7 +6,7 @@ function HttpStatusIcon({ status }: { status: number }): JSX.Element {
switch (Math.floor(status / 100)) {
case 2:
case 3:
return <FiberManualRecordIcon style={{ ...style, color: green[600] }} />;
return <FiberManualRecordIcon style={{ ...style, color: green[400] }} />;
case 4:
return <FiberManualRecordIcon style={{ ...style, color: orange[400] }} />;
case 5:

View File

@ -51,6 +51,14 @@ function LogDetail({ requestId: id }: Props): JSX.Element {
);
}
if (!data.httpRequestLog) {
return (
<Alert severity="warning">
Request <strong>{id}</strong> was not found.
</Alert>
);
}
const { method, url, proto, headers, body, response } = data.httpRequestLog;
return (

View File

@ -1,3 +1,4 @@
import { useRouter } from "next/router";
import { gql, useQuery } from "@apollo/client";
import { useState } from "react";
import { Box, Typography, CircularProgress } from "@material-ui/core";
@ -23,10 +24,17 @@ const HTTP_REQUEST_LOGS = gql`
`;
function LogsOverview(): JSX.Element {
const router = useRouter();
const detailReqLogId = router.query.id as string;
console.log(detailReqLogId);
const { loading, error, data } = useQuery(HTTP_REQUEST_LOGS);
const [detailReqLogId, setDetailReqLogId] = useState<string | null>(null);
const handleLogClick = (reqId: string) => setDetailReqLogId(reqId);
const handleLogClick = (reqId: string) => {
router.push("/proxy/logs?id=" + reqId, undefined, {
shallow: false,
});
};
if (loading) {
return <CircularProgress />;
@ -40,7 +48,11 @@ function LogsOverview(): JSX.Element {
return (
<div>
<Box mb={2}>
<RequestList logs={logs} onLogClick={handleLogClick} />
<RequestList
logs={logs}
selectedReqLogId={detailReqLogId}
onLogClick={handleLogClick}
/>
</Box>
<Box>
{detailReqLogId && <LogDetail requestId={detailReqLogId} />}

View File

@ -67,7 +67,11 @@ function RequestDetail({ request }: Props): JSX.Element {
</Typography>
<Typography className={classes.requestTitle} variant="h6">
{method} {decodeURIComponent(parsedUrl.pathname + parsedUrl.search)}{" "}
<Typography component="span" color="textSecondary">
<Typography
component="span"
color="textSecondary"
style={{ fontFamily: "'JetBrains Mono', monospace" }}
>
{proto}
</Typography>
</Typography>
@ -75,7 +79,9 @@ function RequestDetail({ request }: Props): JSX.Element {
<Divider />
<HttpHeadersTable headers={headers} />
<Box m={2}>
<HttpHeadersTable headers={headers} />
</Box>
{body && <Editor content={body} contentType={contentType} />}
</div>

View File

@ -6,23 +6,64 @@ import {
TableRow,
TableCell,
TableBody,
CircularProgress,
Typography,
Box,
createStyles,
makeStyles,
Theme,
withTheme,
} from "@material-ui/core";
import HttpStatusIcon from "./HttpStatusCode";
import CenteredPaper from "../CenteredPaper";
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 {
logs: Array<any>;
selectedReqLogId?: string;
onLogClick(requestId: string): void;
theme: Theme;
}
function RequestList({ logs, onLogClick }: Props): JSX.Element {
function RequestList({
logs,
onLogClick,
selectedReqLogId,
theme,
}: Props): JSX.Element {
return (
<div>
<RequestListTable onLogClick={onLogClick} logs={logs} />
<RequestListTable
onLogClick={onLogClick}
logs={logs}
selectedReqLogId={selectedReqLogId}
theme={theme}
/>
{logs.length === 0 && (
<Box my={1}>
<CenteredPaper>
@ -36,12 +77,16 @@ function RequestList({ logs, onLogClick }: Props): JSX.Element {
interface RequestListTableProps {
logs?: any;
selectedReqLogId?: string;
onLogClick(requestId: string): void;
theme: Theme;
}
function RequestListTable({
logs,
selectedReqLogId,
onLogClick,
theme,
}: RequestListTableProps): JSX.Element {
return (
<TableContainer
@ -70,10 +115,21 @@ function RequestListTable({
textOverflow: "ellipsis",
} as any;
const rowStyle = {
backgroundColor:
id === selectedReqLogId
? theme.palette.action.selected
: "inherit",
};
return (
<TableRow key={id} onClick={() => onLogClick(id)}>
<TableRow
key={id}
style={rowStyle}
onClick={() => onLogClick(id)}
>
<TableCell style={{ ...cellStyle, width: "100px" }}>
{method}
<code>{method}</code>
</TableCell>
<TableCell style={{ ...cellStyle, maxWidth: "100px" }}>
{origin}
@ -85,7 +141,7 @@ function RequestListTable({
{response && (
<div>
<HttpStatusIcon status={response.statusCode} />{" "}
{response.status}
<code>{response.status}</code>
</div>
)}
</TableCell>
@ -98,4 +154,4 @@ function RequestListTable({
);
}
export default RequestList;
export default withTheme(RequestList);

View File

@ -34,7 +34,13 @@ function ResponseDetail({ response }: Props): JSX.Element {
>
<HttpStatusIcon status={response.statusCode} />{" "}
<Typography component="span" color="textSecondary">
{response.proto}
<Typography
component="span"
color="textSecondary"
style={{ fontFamily: "'JetBrains Mono', monospace" }}
>
{response.proto}
</Typography>
</Typography>{" "}
{response.status}
</Typography>
@ -42,7 +48,9 @@ function ResponseDetail({ response }: Props): JSX.Element {
<Divider />
<HttpHeadersTable headers={response.headers} />
<Box m={2}>
<HttpHeadersTable headers={response.headers} />
</Box>
{response.body && (
<Editor content={response.body} contentType={contentType} />

View File

@ -1,6 +1,6 @@
import { createMuiTheme } from "@material-ui/core/styles";
import grey from "@material-ui/core/colors/grey";
import green from "@material-ui/core/colors/green";
import teal from "@material-ui/core/colors/teal";
const theme = createMuiTheme({
palette: {
@ -9,7 +9,36 @@ const theme = createMuiTheme({
main: grey[900],
},
secondary: {
main: green[500],
main: teal["A400"],
},
},
typography: {
h2: {
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 600,
},
h3: {
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 600,
},
h4: {
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 600,
},
h5: {
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 600,
},
h6: {
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 600,
},
},
overrides: {
MuiTableCell: {
stickyHeader: {
backgroundColor: grey[900],
},
},
},
});

View File

@ -22,7 +22,7 @@ function App({ Component, pageProps }: AppProps): JSX.Element {
return (
<React.Fragment>
<Head>
<title>Hetty</title>
<title>Hetty://</title>
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"

View File

@ -10,10 +10,15 @@ export default class MyDocument extends Document {
<Html lang="en">
<Head>
<meta name="theme-color" content={theme.palette.primary.main} />
<link rel="stylesheet" href="/style.css" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap"
/>
</Head>
<body>
<Main />

View File

@ -1,8 +1,81 @@
import {
Box,
Button,
createStyles,
IconButton,
makeStyles,
Theme,
Typography,
} from "@material-ui/core";
import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet";
import SendIcon from "@material-ui/icons/Send";
import Link from "next/link";
import Layout 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 {
const classes = useStyles();
return (
<div>
<h1>Hetty123</h1>
</div>
<Layout>
<Box p={4}>
<Box mb={4} width="60%">
<Typography variant="h2">
<span className={classes.titleHighlight}>Hetty://</span>
<br />
The simple HTTP toolkit for security research.
</Typography>
</Box>
<Typography className={classes.subtitle} paragraph>
What if security testing was intuitive, powerful, and good looking?
What if it was <strong>free</strong>, instead of $400 per year?{" "}
<span className={classes.titleHighlight}>Hetty</span> is listening on{" "}
<code>:8080</code>
</Typography>
<Box>
<Link href="/proxy" passHref>
<Button
className={classes.button}
variant="contained"
color="secondary"
component="a"
size="large"
startIcon={<SettingsEthernetIcon />}
>
Setup proxy
</Button>
</Link>
<Link href="/proxy" passHref>
<Button
className={classes.button}
variant="contained"
color="primary"
component="a"
size="large"
startIcon={<SendIcon />}
>
Send HTTP requests
</Button>
</Link>
</Box>
</Box>
</Layout>
);
}

View File

@ -1,8 +1,29 @@
import React from "react";
import { Box, Button, Typography } from "@material-ui/core";
import ListIcon from "@material-ui/icons/List";
import Link from "next/link";
import Layout from "../../components/Layout";
function Index(): JSX.Element {
return (
<div>
<h1>Proxy123</h1>
</div>
<Layout>
<Box mb={2}>
<Typography variant="h5">Proxy setup</Typography>
</Box>
<Typography paragraph>Coming soon</Typography>
<Link href="/proxy/logs" passHref>
<Button
variant="contained"
color="secondary"
component="a"
size="large"
startIcon={<ListIcon />}
>
View logs
</Button>
</Link>
</Layout>
);
}

View File

@ -0,0 +1,16 @@
import { Box, Typography } from "@material-ui/core";
import Layout from "../../components/Layout";
function Index(): JSX.Element {
return (
<Layout>
<Box mb={2}>
<Typography variant="h5">Sender</Typography>
</Box>
<Typography paragraph>Coming soon</Typography>
</Layout>
);
}
export default Index;