Reuse components across Proxy and Sender modules

This commit is contained in:
David Stotijn
2022-02-25 21:08:15 +01:00
parent 11f70282d7
commit 7e43479b54
33 changed files with 859 additions and 818 deletions

View File

@ -1,51 +0,0 @@
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import React, { useState } from "react";
export function useConfirmationDialog() {
const [isOpen, setIsOpen] = useState(false);
const close = () => setIsOpen(false);
const open = () => setIsOpen(true);
return { open, close, isOpen };
}
interface ConfirmationDialog {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
children: React.ReactNode;
}
export function ConfirmationDialog(props: ConfirmationDialog) {
const { onClose, onConfirm, isOpen, children } = props;
function confirm() {
onConfirm();
onClose();
}
return (
<Dialog
open={isOpen}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">{children}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button onClick={confirm} autoFocus>
Confirm
</Button>
</DialogActions>
</Dialog>
);
}

View File

@ -1,104 +0,0 @@
import { Alert } from "@mui/lab";
import { Table, TableBody, TableCell, TableContainer, TableRow, Snackbar, SxProps, Theme } from "@mui/material";
import React, { useState } from "react";
const baseCellStyle: SxProps<Theme> = {
px: 0,
py: 0.33,
verticalAlign: "top",
border: "none",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
"&:hover": {
color: "primary.main",
whiteSpace: "inherit",
overflow: "inherit",
textOverflow: "inherit",
cursor: "copy",
},
};
const keyCellStyle = {
...baseCellStyle,
pr: 1,
width: "40%",
fontWeight: "bold",
fontSize: ".75rem",
};
const valueCellStyle = {
...baseCellStyle,
width: "60%",
border: "none",
fontSize: ".75rem",
};
interface Props {
headers: Array<{ key: string; value: string }>;
}
function HttpHeadersTable({ headers }: Props): JSX.Element {
const [open, setOpen] = useState(false);
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
const windowSel = window.getSelection();
if (!windowSel || !document) {
return;
}
const r = document.createRange();
r.selectNode(e.currentTarget);
windowSel.removeAllRanges();
windowSel.addRange(r);
document.execCommand("copy");
windowSel.removeAllRanges();
setOpen(true);
};
const handleClose = (event: Event | React.SyntheticEvent, reason?: string) => {
if (reason === "clickaway") {
return;
}
setOpen(false);
};
return (
<div>
<Snackbar open={open} autoHideDuration={3000} onClose={handleClose}>
<Alert onClose={handleClose} severity="info">
Copied to clipboard.
</Alert>
</Snackbar>
<TableContainer>
<Table
sx={{
tableLayout: "fixed",
width: "100%",
}}
size="small"
>
<TableBody>
{headers.map(({ key, value }, index) => (
<TableRow key={index}>
<TableCell component="th" scope="row" sx={keyCellStyle} onClick={handleClick}>
<code>{key}:</code>
</TableCell>
<TableCell sx={valueCellStyle} onClick={handleClick}>
<code>{value}</code>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</div>
);
}
export default HttpHeadersTable;

View File

@ -1,18 +1,20 @@
import Alert from "@mui/lab/Alert";
import { Box, Grid, Paper, CircularProgress } from "@mui/material";
import { Box, CircularProgress, Paper, Typography } from "@mui/material";
import RequestDetail from "./RequestDetail";
import ResponseDetail from "./ResponseDetail";
import Response from "lib/components/Response";
import SplitPane from "lib/components/SplitPane";
import { useHttpRequestLogQuery } from "lib/graphql/generated";
interface Props {
requestId: string;
id?: string;
}
function LogDetail({ requestId: id }: Props): JSX.Element {
function LogDetail({ id }: Props): JSX.Element {
const { loading, error, data } = useHttpRequestLogQuery({
variables: { id },
variables: { id: id as string },
skip: id === undefined,
});
if (loading) {
@ -31,28 +33,24 @@ function LogDetail({ requestId: id }: Props): JSX.Element {
}
if (!data?.httpRequestLog) {
return <div></div>;
return (
<Paper variant="centered" sx={{ mt: 2 }}>
<Typography>Select a log entry</Typography>
</Paper>
);
}
const httpRequestLog = data.httpRequestLog;
const reqLog = data.httpRequestLog;
return (
<div>
<Grid container item spacing={2}>
<Grid item xs={6}>
<Box component={Paper}>
<RequestDetail request={httpRequestLog} />
</Box>
</Grid>
<Grid item xs={6}>
{httpRequestLog.response && (
<Box component={Paper}>
<ResponseDetail response={httpRequestLog.response} />
</Box>
)}
</Grid>
</Grid>
</div>
<SplitPane split="vertical" size={"50%"}>
<RequestDetail request={reqLog} />
{reqLog.response && (
<Box sx={{ height: "100%", pt: 1, pl: 2, pb: 2 }}>
<Response response={reqLog.response} />
</Box>
)}
</SplitPane>
);
}

View File

@ -1,60 +0,0 @@
import Alert from "@mui/lab/Alert";
import { Box, CircularProgress, Link as MaterialLink, Typography } from "@mui/material";
import Link from "next/link";
import { useRouter } from "next/router";
import LogDetail from "./LogDetail";
import RequestList from "./RequestList";
import CenteredPaper from "lib/components/CenteredPaper";
import { useHttpRequestLogsQuery } from "lib/graphql/generated";
export default function LogsOverview(): JSX.Element {
const router = useRouter();
const detailReqLogId = router.query.id as string | undefined;
const { loading, error, data } = useHttpRequestLogsQuery({
pollInterval: 1000,
});
const handleLogClick = (reqId: string) => {
router.push("/proxy/logs?id=" + reqId, undefined, {
shallow: false,
});
};
if (loading) {
return <CircularProgress />;
}
if (error) {
if (error.graphQLErrors[0]?.extensions?.code === "no_active_project") {
return (
<Alert severity="info">
There is no project active.{" "}
<Link href="/projects" passHref>
<MaterialLink color="primary">Create or open</MaterialLink>
</Link>{" "}
one first.
</Alert>
);
}
return <Alert severity="error">Error fetching logs: {error.message}</Alert>;
}
const logs = data?.httpRequestLogs || [];
return (
<div>
<Box mb={2}>
<RequestList logs={logs} selectedReqLogId={detailReqLogId} onLogClick={handleLogClick} />
</Box>
<Box>
{detailReqLogId && <LogDetail requestId={detailReqLogId} />}
{logs.length !== 0 && !detailReqLogId && (
<CenteredPaper>
<Typography>Select a log entry</Typography>
</CenteredPaper>
)}
</Box>
</div>
);
}

View File

@ -1,51 +1,46 @@
import { Typography, Box, Divider } from "@mui/material";
import { Typography, Box } from "@mui/material";
import React from "react";
import HttpHeadersTable from "./HttpHeadersTable";
import Editor from "lib/components/Editor";
import RequestTabs from "lib/components/RequestTabs";
import { HttpRequestLogQuery } from "lib/graphql/generated";
import { queryParamsFromURL } from "lib/queryParamsFromURL";
interface Props {
request: NonNullable<HttpRequestLogQuery["httpRequestLog"]>;
}
function RequestDetail({ request }: Props): JSX.Element {
const { method, url, proto, headers, body } = request;
const { method, url, headers, body } = request;
const contentType = headers.find((header) => header.key === "Content-Type")?.value;
const parsedUrl = new URL(url);
return (
<div>
<Box p={2}>
<Box sx={{ height: "100%", display: "flex", flexDirection: "column", pr: 2, pb: 2 }}>
<Box sx={{ p: 2, pb: 0 }}>
<Typography variant="overline" color="textSecondary" style={{ float: "right" }}>
Request
</Typography>
<Typography
sx={{
width: "calc(100% - 80px)",
fontSize: "1rem",
wordBreak: "break-all",
whiteSpace: "pre-wrap",
}}
variant="h6"
component="h2"
sx={{
fontSize: "1rem",
fontFamily: "'JetBrains Mono', monospace",
display: "block",
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
pr: 2,
}}
>
{method} {decodeURIComponent(parsedUrl.pathname + parsedUrl.search)}{" "}
<Typography component="span" color="textSecondary" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
{proto}
</Typography>
{method} {decodeURIComponent(parsedUrl.pathname + parsedUrl.search)}
</Typography>
</Box>
<Divider />
<Box p={2}>
<HttpHeadersTable headers={headers} />
<Box flex="1 auto" overflow="scroll">
<RequestTabs headers={headers} queryParams={queryParamsFromURL(url)} body={body} />
</Box>
{body && <Editor content={body} contentType={contentType} />}
</div>
</Box>
);
}

View File

@ -1,167 +0,0 @@
import {
TableContainer,
Paper,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
Typography,
Box,
useTheme,
MenuItem,
Snackbar,
Alert,
Link,
} from "@mui/material";
import React, { useState } from "react";
import CenteredPaper from "lib/components/CenteredPaper";
import HttpStatusIcon from "lib/components/HttpStatusIcon";
import useContextMenu from "lib/components/useContextMenu";
import { HttpRequestLogsQuery, useCreateSenderRequestFromHttpRequestLogMutation } from "lib/graphql/generated";
interface Props {
logs: NonNullable<HttpRequestLogsQuery["httpRequestLogs"]>;
selectedReqLogId?: string;
onLogClick(requestId: string): void;
}
export default function RequestList({ logs, onLogClick, selectedReqLogId }: Props): JSX.Element {
return (
<div>
<RequestListTable onLogClick={onLogClick} logs={logs} selectedReqLogId={selectedReqLogId} />
{logs.length === 0 && (
<Box my={1}>
<CenteredPaper>
<Typography>No logs found.</Typography>
</CenteredPaper>
</Box>
)}
</div>
);
}
interface RequestListTableProps {
logs: HttpRequestLogsQuery["httpRequestLogs"];
selectedReqLogId?: string;
onLogClick(requestId: string): void;
}
function RequestListTable({ logs, selectedReqLogId, onLogClick }: RequestListTableProps): JSX.Element {
const theme = useTheme();
const [createSenderReqFromLog] = useCreateSenderRequestFromHttpRequestLogMutation({});
const [copyToSenderId, setCopyToSenderId] = useState("");
const [Menu, handleContextMenu, handleContextMenuClose] = useContextMenu();
const handleCopyToSenderClick = () => {
createSenderReqFromLog({
variables: {
id: copyToSenderId,
},
onCompleted({ createSenderRequestFromHttpRequestLog }) {
const { id } = createSenderRequestFromHttpRequestLog;
setNewSenderReqId(id);
setCopiedReqNotifOpen(true);
},
});
handleContextMenuClose();
};
const [newSenderReqId, setNewSenderReqId] = useState("");
const [copiedReqNotifOpen, setCopiedReqNotifOpen] = useState(false);
const handleCloseCopiedNotif = (_: Event | React.SyntheticEvent, reason?: string) => {
if (reason === "clickaway") {
return;
}
setCopiedReqNotifOpen(false);
};
return (
<div>
<Menu>
<MenuItem onClick={handleCopyToSenderClick}>Copy request to Sender</MenuItem>
</Menu>
<Snackbar
open={copiedReqNotifOpen}
autoHideDuration={3000}
onClose={handleCloseCopiedNotif}
anchorOrigin={{ horizontal: "center", vertical: "bottom" }}
>
<Alert onClose={handleCloseCopiedNotif} severity="info">
Request was copied. <Link href={`/sender?id=${newSenderReqId}`}>Edit in Sender.</Link>
</Alert>
</Snackbar>
<TableContainer
component={Paper}
style={{
minHeight: logs.length ? 200 : 0,
height: logs.length ? "24vh" : "inherit",
}}
>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Method</TableCell>
<TableCell>Origin</TableCell>
<TableCell>Path</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{logs.map(({ id, method, url, response }) => {
const { origin, pathname, search, hash } = new URL(url);
const cellStyle = {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
};
return (
<TableRow
key={id}
sx={{
"&:hover": {
cursor: "pointer",
},
...(id === selectedReqLogId && {
bgcolor: theme.palette.action.selected,
}),
}}
hover
onClick={() => onLogClick(id)}
onContextMenu={(e) => {
setCopyToSenderId(id);
handleContextMenu(e);
}}
>
<TableCell sx={{ ...cellStyle, width: "100px" }}>
<code>{method}</code>
</TableCell>
<TableCell sx={{ ...cellStyle, maxWidth: "100px" }}>{origin}</TableCell>
<TableCell sx={{ ...cellStyle, maxWidth: "200px" }}>
{decodeURIComponent(pathname + search + hash)}
</TableCell>
<TableCell style={{ maxWidth: "100px" }}>
{response && (
<div>
<HttpStatusIcon status={response.statusCode} />{" "}
<code>
{response.statusCode} {response.statusReason}
</code>
</div>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
</div>
);
}

View File

@ -0,0 +1,90 @@
import { Alert, Box, Link, MenuItem, Snackbar } from "@mui/material";
import { useRouter } from "next/router";
import { useState } from "react";
import LogDetail from "./LogDetail";
import Search from "./Search";
import RequestsTable from "lib/components/RequestsTable";
import SplitPane from "lib/components/SplitPane";
import useContextMenu from "lib/components/useContextMenu";
import { useCreateSenderRequestFromHttpRequestLogMutation, useHttpRequestLogsQuery } from "lib/graphql/generated";
export function RequestLogs(): JSX.Element {
const router = useRouter();
const id = router.query.id as string | undefined;
const { data } = useHttpRequestLogsQuery({
pollInterval: 1000,
});
const [createSenderReqFromLog] = useCreateSenderRequestFromHttpRequestLogMutation({});
const [copyToSenderId, setCopyToSenderId] = useState("");
const [Menu, handleContextMenu, handleContextMenuClose] = useContextMenu();
const handleCopyToSenderClick = () => {
createSenderReqFromLog({
variables: {
id: copyToSenderId,
},
onCompleted({ createSenderRequestFromHttpRequestLog }) {
const { id } = createSenderRequestFromHttpRequestLog;
setNewSenderReqId(id);
setCopiedReqNotifOpen(true);
},
});
handleContextMenuClose();
};
const [newSenderReqId, setNewSenderReqId] = useState("");
const [copiedReqNotifOpen, setCopiedReqNotifOpen] = useState(false);
const handleCloseCopiedNotif = (_: Event | React.SyntheticEvent, reason?: string) => {
if (reason === "clickaway") {
return;
}
setCopiedReqNotifOpen(false);
};
const handleRowClick = (id: string) => {
router.push(`/proxy/logs?id=${id}`);
};
const handleRowContextClick = (e: React.MouseEvent, id: string) => {
setCopyToSenderId(id);
handleContextMenu(e);
};
return (
<Box display="flex" flexDirection="column" height="100%">
<Search />
<Box sx={{ display: "flex", flex: "1 auto", position: "relative" }}>
<SplitPane split="horizontal" size={"40%"}>
<Box sx={{ width: "100%", height: "100%", pb: 2 }}>
<Box sx={{ width: "100%", height: "100%", overflow: "scroll" }}>
<Menu>
<MenuItem onClick={handleCopyToSenderClick}>Copy request to Sender</MenuItem>
</Menu>
<Snackbar
open={copiedReqNotifOpen}
autoHideDuration={3000}
onClose={handleCloseCopiedNotif}
anchorOrigin={{ horizontal: "center", vertical: "bottom" }}
>
<Alert onClose={handleCloseCopiedNotif} severity="info">
Request was copied. <Link href={`/sender?id=${newSenderReqId}`}>Edit in Sender.</Link>
</Alert>
</Snackbar>
<RequestsTable
requests={data?.httpRequestLogs || []}
activeRowId={id}
onRowClick={handleRowClick}
onContextMenu={handleRowContextClick}
/>
</Box>
</Box>
<LogDetail id={id} />
</SplitPane>
</Box>
</Box>
);
}

View File

@ -1,44 +0,0 @@
import { Typography, Box, Divider } from "@mui/material";
import HttpHeadersTable from "./HttpHeadersTable";
import Editor from "lib/components/Editor";
import HttpStatusIcon from "lib/components/HttpStatusIcon";
import { HttpRequestLogQuery } from "lib/graphql/generated";
interface Props {
response: NonNullable<NonNullable<HttpRequestLogQuery["httpRequestLog"]>["response"]>;
}
function ResponseDetail({ response }: Props): JSX.Element {
const contentType = response.headers.find((header) => header.key.toLowerCase() === "content-type")?.value;
return (
<div>
<Box p={2}>
<Typography variant="overline" color="textSecondary" style={{ float: "right" }}>
Response
</Typography>
<Typography variant="h6" style={{ fontSize: "1rem", whiteSpace: "nowrap" }}>
<HttpStatusIcon status={response.statusCode} />{" "}
<Typography component="span" color="textSecondary">
<Typography component="span" color="textSecondary" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
{response.proto}
</Typography>
</Typography>{" "}
{response.statusCode} {response.statusReason}
</Typography>
</Box>
<Divider />
<Box p={2}>
<HttpHeadersTable headers={response.headers} />
</Box>
{response.body && <Editor content={response.body} contentType={contentType} />}
</div>
);
}
export default ResponseDetail;

View File

@ -17,8 +17,7 @@ import {
import IconButton from "@mui/material/IconButton";
import React, { useRef, useState } from "react";
import { ConfirmationDialog, useConfirmationDialog } from "./ConfirmationDialog";
import { ConfirmationDialog, useConfirmationDialog } from "lib/components/ConfirmationDialog";
import {
HttpRequestLogFilterDocument,
HttpRequestLogsDocument,

View File

@ -10,6 +10,7 @@ query HttpRequestLog($id: ID!) {
}
body
response {
id
proto
headers {
key

View File

@ -0,0 +1,3 @@
import { RequestLogs } from "./components/RequestLogs";
export default RequestLogs;