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
No known key found for this signature in database
GPG key ID: B23243A9C47CEE2D
33 changed files with 859 additions and 818 deletions

View file

@ -19,6 +19,7 @@
"@mui/icons-material": "^5.3.1",
"@mui/lab": "^5.0.0-alpha.66",
"@mui/material": "^5.3.1",
"@mui/styles": "^5.4.2",
"allotment": "^1.9.0",
"deepmerge": "^4.2.2",
"graphql": "^16.2.0",
@ -27,7 +28,8 @@
"next": "^12.0.8",
"next-fonts": "^1.0.3",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"react-split-pane": "^0.1.92"
},
"devDependencies": {
"@babel/core": "^7.0.0",

View file

@ -27,6 +27,8 @@ import MuiListItemIcon, { ListItemIconProps } from "@mui/material/ListItemIcon";
import Link from "next/link";
import React, { useState } from "react";
import { useActiveProject } from "lib/ActiveProjectContext";
export enum Page {
Home,
GetStarted,
@ -132,6 +134,7 @@ interface Props {
}
export function Layout({ title, page, children }: Props): JSX.Element {
const activeProject = useActiveProject();
const theme = useTheme();
const [open, setOpen] = useState(false);
@ -200,7 +203,7 @@ export function Layout({ title, page, children }: Props): JSX.Element {
</ListItemButton>
</Link>
<Link href="/proxy/logs" passHref>
<ListItemButton key="proxyLogs" selected={page === Page.ProxyLogs}>
<ListItemButton key="proxyLogs" disabled={!activeProject} selected={page === Page.ProxyLogs}>
<Tooltip title="Proxy">
<ListItemIcon>
<SettingsEthernetIcon />
@ -210,7 +213,7 @@ export function Layout({ title, page, children }: Props): JSX.Element {
</ListItemButton>
</Link>
<Link href="/sender" passHref>
<ListItemButton key="sender" selected={page === Page.Sender}>
<ListItemButton key="sender" disabled={!activeProject} selected={page === Page.Sender}>
<Tooltip title="Sender">
<ListItemIcon>
<SendIcon />
@ -220,7 +223,7 @@ export function Layout({ title, page, children }: Props): JSX.Element {
</ListItemButton>
</Link>
<Link href="/scope" passHref>
<ListItemButton key="scope" selected={page === Page.Scope}>
<ListItemButton key="scope" disabled={!activeProject} selected={page === Page.Scope}>
<Tooltip title="Scope">
<ListItemIcon>
<LocationSearchingIcon />

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;

View file

@ -10,14 +10,13 @@ import {
TextField,
Typography,
} from "@mui/material";
import { AllotmentProps, PaneProps } from "allotment/dist/types/src/allotment";
import { useRouter } from "next/router";
import React, { useEffect, useRef, useState } from "react";
import EditRequestTabs from "./EditRequestTabs";
import { KeyValuePair, sortKeyValuePairs } from "./KeyValuePair";
import Response from "./Response";
import React, { useState } from "react";
import { KeyValuePair, sortKeyValuePairs } from "lib/components/KeyValuePair";
import RequestTabs from "lib/components/RequestTabs";
import Response from "lib/components/Response";
import SplitPane from "lib/components/SplitPane";
import {
GetSenderRequestQuery,
useCreateOrUpdateSenderRequestMutation,
@ -25,8 +24,7 @@ import {
useGetSenderRequestQuery,
useSendRequestMutation,
} from "lib/graphql/generated";
import "allotment/dist/style.css";
import { queryParamsFromURL } from "lib/queryParamsFromURL";
enum HttpMethod {
Get = "GET",
@ -88,22 +86,6 @@ function updateURLQueryParams(url: string, queryParams: KeyValuePair[]) {
return newURL + "?" + rawQueryParams;
}
function queryParamsFromURL(url: string): KeyValuePair[] {
const questionMarkIndex = url.indexOf("?");
if (questionMarkIndex === -1) {
return [];
}
const queryParams: KeyValuePair[] = [];
const searchParams = new URLSearchParams(url.slice(questionMarkIndex + 1));
for (const [key, value] of searchParams) {
queryParams.push({ key, value });
}
return queryParams;
}
function EditRequest(): JSX.Element {
const router = useRouter();
const reqId = router.query.id as string | undefined;
@ -219,28 +201,6 @@ function EditRequest(): JSX.Element {
createOrUpdateRequestAndSend();
};
const isMountedRef = useRef(false);
const [Allotment, setAllotment] = useState<
(React.ComponentType<AllotmentProps> & { Pane: React.ComponentType<PaneProps> }) | null
>(null);
useEffect(() => {
isMountedRef.current = true;
import("allotment")
.then((mod) => {
if (!isMountedRef.current) {
return;
}
setAllotment(mod.Allotment);
})
.catch((err) => console.error(err, `could not import allotment ${err.message}`));
return () => {
isMountedRef.current = false;
};
}, []);
if (!Allotment) {
return <div>Loading...</div>;
}
return (
<Box display="flex" flexDirection="column" height="100%" gap={2}>
<Box component="form" autoComplete="off" onSubmit={handleFormSubmit}>
@ -276,31 +236,27 @@ function EditRequest(): JSX.Element {
)}
</Box>
<Box flex="1 auto" overflow="hidden">
<Allotment>
<Box pr={2} pb={2} height="100%" overflow="hidden">
<Box height="100%" position="relative">
<Typography variant="overline" color="textSecondary" sx={{ position: "absolute", right: 0, mt: 1.2 }}>
Request
</Typography>
<EditRequestTabs
queryParams={queryParams}
headers={headers}
body={body}
onQueryParamChange={handleQueryParamChange}
onQueryParamDelete={handleQueryParamDelete}
onHeaderChange={handleHeaderChange}
onHeaderDelete={handleHeaderDelete}
onBodyChange={setBody}
/>
</Box>
<Box flex="1 auto" position="relative">
<SplitPane split="vertical" size={"50%"}>
<Box sx={{ height: "100%", mr: 2, pb: 2, position: "relative" }}>
<Typography variant="overline" color="textSecondary" sx={{ position: "absolute", right: 0, mt: 1.2 }}>
Request
</Typography>
<RequestTabs
queryParams={queryParams}
headers={headers}
body={body}
onQueryParamChange={handleQueryParamChange}
onQueryParamDelete={handleQueryParamDelete}
onHeaderChange={handleHeaderChange}
onHeaderDelete={handleHeaderDelete}
onBodyChange={setBody}
/>
</Box>
<Box pb={2} pl={2} height="100%" overflow="hidden">
<Box height="100%" position="relative">
<Response response={response} />
</Box>
<Box sx={{ height: "100%", position: "relative", ml: 2, pb: 2 }}>
<Response response={response} />
</Box>
</Allotment>
</SplitPane>
</Box>
</Box>
);

View file

@ -1,8 +1,7 @@
import { TableContainer, Table, TableHead, TableRow, TableCell, Typography, Box, TableBody } from "@mui/material";
import { Box, Paper, Typography } from "@mui/material";
import { useRouter } from "next/router";
import CenteredPaper from "lib/components/CenteredPaper";
import HttpStatusIcon from "lib/components/HttpStatusIcon";
import RequestsTable from "lib/components/RequestsTable";
import { useGetSenderRequestsQuery } from "lib/graphql/generated";
function History(): JSX.Element {
@ -20,71 +19,13 @@ function History(): JSX.Element {
return (
<Box>
{!loading && data?.senderRequests && data?.senderRequests.length > 0 && (
<TableContainer sx={{ overflowX: "initial" }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>Method</TableCell>
<TableCell>Origin</TableCell>
<TableCell>Path</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data?.senderRequests &&
data.senderRequests.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 === activeId && {
bgcolor: "action.selected",
cursor: "inherit",
}),
}}
hover
onClick={() => handleRowClick(id)}
>
<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>
<RequestsTable requests={data.senderRequests} onRowClick={handleRowClick} activeRowId={activeId} />
)}
<Box sx={{ mt: 2, height: "100%" }}>
{!loading && data?.senderRequests.length === 0 && (
<CenteredPaper>
<Paper variant="centered">
<Typography>No requests created yet.</Typography>
</CenteredPaper>
</Paper>
)}
</Box>
</Box>

View file

@ -1,130 +0,0 @@
import ClearIcon from "@mui/icons-material/Clear";
import { IconButton, InputBase, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material";
export interface KeyValuePair {
key: string;
value: string;
}
export interface KeyValuePairTableProps {
items: KeyValuePair[];
onChange?: (key: string, value: string, index: number) => void;
onDelete?: (index: number) => void;
}
export function KeyValuePairTable({ items, onChange, onDelete }: KeyValuePairTableProps): JSX.Element {
const inputSx = {
fontSize: "0.875rem",
"&.MuiInputBase-root input": {
p: 0,
},
};
return (
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Key</TableCell>
<TableCell>Value</TableCell>
{onDelete && <TableCell padding="checkbox"></TableCell>}
</TableRow>
</TableHead>
<TableBody
sx={{
"td, th, input": {
fontFamily: "'JetBrains Mono', monospace",
fontSize: "0.75rem",
py: 0.2,
},
"td span, th span": {
display: "block",
py: 0.7,
},
}}
>
{items.map(({ key, value }, idx) => (
<TableRow
key={idx}
hover
sx={{
"& .delete-button": {
visibility: "hidden",
},
"&:hover .delete-button": {
visibility: "inherit",
},
}}
>
<TableCell component="th" scope="row">
{!onChange && <span>{key}</span>}
{onChange && (
<InputBase
size="small"
fullWidth
placeholder="Key"
value={key}
onChange={(e) => {
onChange && onChange(e.target.value, value, idx);
}}
sx={inputSx}
/>
)}
</TableCell>
<TableCell sx={{ width: "60%", wordBreak: "break-all" }}>
{!onChange && value}
{onChange && (
<InputBase
size="small"
fullWidth
placeholder="Value"
value={value}
onChange={(e) => {
onChange && onChange(key, e.target.value, idx);
}}
sx={inputSx}
/>
)}
</TableCell>
{onDelete && (
<TableCell>
<div className="delete-button">
<IconButton
size="small"
onClick={() => {
onDelete && onDelete(idx);
}}
sx={{
visibility: onDelete === undefined || items.length === idx + 1 ? "hidden" : "inherit",
}}
>
<ClearIcon fontSize="inherit" />
</IconButton>
</div>
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}
export function sortKeyValuePairs(items: KeyValuePair[]): KeyValuePair[] {
const sorted = [...items];
sorted.sort((a, b) => {
if (a.key < b.key) {
return -1;
}
if (a.key > b.key) {
return 1;
}
return 0;
});
return sorted;
}
export default KeyValuePairTable;

View file

@ -0,0 +1,21 @@
import { Box } from "@mui/material";
import EditRequest from "./EditRequest";
import History from "./History";
import SplitPane from "lib/components/SplitPane";
export default function Sender(): JSX.Element {
return (
<Box sx={{ height: "100%", position: "relative" }}>
<SplitPane split="horizontal" size="70%">
<Box sx={{ width: "100%", pt: "0.75rem" }}>
<EditRequest />
</Box>
<Box sx={{ height: "100%", overflow: "scroll" }}>
<History />
</Box>
</SplitPane>
</Box>
);
}

View file

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

View file

@ -0,0 +1,20 @@
import React, { createContext, useContext } from "react";
import { Project, useProjectsQuery } from "./graphql/generated";
const ActiveProjectContext = createContext<Project | null>(null);
interface Props {
children?: React.ReactNode | undefined;
}
export function ActiveProjectProvider({ children }: Props): JSX.Element {
const { data } = useProjectsQuery();
const project = data?.projects.find((project) => project.isActive) || null;
return <ActiveProjectContext.Provider value={project}>{children}</ActiveProjectContext.Provider>;
}
export function useActiveProject() {
return useContext(ActiveProjectContext);
}

View file

@ -1,21 +0,0 @@
import { Paper } from "@mui/material";
function CenteredPaper({ children }: { children: React.ReactNode }): JSX.Element {
return (
<div>
<Paper
elevation={0}
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: 36,
}}
>
{children}
</Paper>
</div>
);
}
export default CenteredPaper;

View file

@ -0,0 +1,201 @@
import ClearIcon from "@mui/icons-material/Clear";
import {
Alert,
IconButton,
InputBase,
InputBaseProps,
Snackbar,
styled,
Table,
TableBody,
TableCell,
TableCellProps,
TableContainer,
TableHead,
TableRow,
TableRowProps,
} from "@mui/material";
import { useState } from "react";
const StyledInputBase = styled(InputBase)<InputBaseProps>(() => ({
fontSize: "0.875rem",
"&.MuiInputBase-root input": {
p: 0,
},
}));
const StyledTableRow = styled(TableRow)<TableRowProps>(() => ({
"& .delete-button": {
visibility: "hidden",
},
"&:hover .delete-button": {
visibility: "inherit",
},
}));
export interface KeyValuePair {
key: string;
value: string;
}
export interface KeyValuePairTableProps {
items: KeyValuePair[];
onChange?: (key: string, value: string, index: number) => void;
onDelete?: (index: number) => void;
}
export function KeyValuePairTable({ items, onChange, onDelete }: KeyValuePairTableProps): JSX.Element {
const [copyConfOpen, setCopyConfOpen] = useState(false);
const handleCellClick = (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();
setCopyConfOpen(true);
};
const handleCopyConfClose = (_: Event | React.SyntheticEvent, reason?: string) => {
if (reason === "clickaway") {
return;
}
setCopyConfOpen(false);
};
const baseCellStyle = {
"&:hover": {
cursor: "copy",
},
};
const KeyTableCell = styled(TableCell)<TableCellProps>(() => (!onChange ? baseCellStyle : {}));
const ValueTableCell = styled(TableCell)<TableCellProps>(() => ({
...(!onChange && baseCellStyle),
width: "60%",
wordBreak: "break-all",
}));
return (
<div>
<Snackbar open={copyConfOpen} autoHideDuration={3000} onClose={handleCopyConfClose}>
<Alert onClose={handleCopyConfClose} severity="info">
Copied to clipboard.
</Alert>
</Snackbar>
<TableContainer sx={{ overflowX: "initial" }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>Key</TableCell>
<TableCell>Value</TableCell>
{onDelete && <TableCell padding="checkbox"></TableCell>}
</TableRow>
</TableHead>
<TableBody
sx={{
"td, th, input": {
fontFamily: "'JetBrains Mono', monospace",
fontSize: "0.75rem",
py: 0.2,
},
"td span, th span": {
display: "block",
py: 0.7,
},
}}
>
{items.map(({ key, value }, idx) => (
<StyledTableRow key={idx} hover>
<KeyTableCell
component="th"
scope="row"
onClick={(e) => {
!onChange && handleCellClick(e);
}}
>
{!onChange && <span>{key}</span>}
{onChange && (
<StyledInputBase
size="small"
fullWidth
placeholder="Key"
value={key}
onChange={(e) => {
onChange && onChange(e.target.value, value, idx);
}}
/>
)}
</KeyTableCell>
<ValueTableCell
onClick={(e) => {
!onChange && handleCellClick(e);
}}
>
{!onChange && value}
{onChange && (
<StyledInputBase
size="small"
fullWidth
placeholder="Value"
value={value}
onChange={(e) => {
onChange && onChange(key, e.target.value, idx);
}}
/>
)}
</ValueTableCell>
{onDelete && (
<TableCell>
<div className="delete-button">
<IconButton
size="small"
onClick={() => {
onDelete && onDelete(idx);
}}
sx={{
visibility: onDelete === undefined || items.length === idx + 1 ? "hidden" : "inherit",
}}
>
<ClearIcon fontSize="inherit" />
</IconButton>
</div>
</TableCell>
)}
</StyledTableRow>
))}
</TableBody>
</Table>
</TableContainer>
</div>
);
}
export function sortKeyValuePairs(items: KeyValuePair[]): KeyValuePair[] {
const sorted = [...items];
sorted.sort((a, b) => {
if (a.key < b.key) {
return -1;
}
if (a.key > b.key) {
return 1;
}
return 0;
});
return sorted;
}
export default KeyValuePairTable;

View file

@ -12,18 +12,18 @@ enum TabValue {
Body = "body",
}
interface EditRequestTabsProps {
interface RequestTabsProps {
queryParams: KeyValuePair[];
headers: KeyValuePair[];
onQueryParamChange: KeyValuePairTableProps["onChange"];
onQueryParamDelete: KeyValuePairTableProps["onDelete"];
onHeaderChange: KeyValuePairTableProps["onChange"];
onHeaderDelete: KeyValuePairTableProps["onDelete"];
body: string;
onBodyChange: (value: string) => void;
onQueryParamChange?: KeyValuePairTableProps["onChange"];
onQueryParamDelete?: KeyValuePairTableProps["onDelete"];
onHeaderChange?: KeyValuePairTableProps["onChange"];
onHeaderDelete?: KeyValuePairTableProps["onDelete"];
body?: string | null;
onBodyChange?: (value: string) => void;
}
function EditRequestTabs(props: EditRequestTabsProps): JSX.Element {
function RequestTabs(props: RequestTabsProps): JSX.Element {
const {
queryParams,
onQueryParamChange,
@ -40,46 +40,45 @@ function EditRequestTabs(props: EditRequestTabsProps): JSX.Element {
textTransform: "none",
};
const queryParamsLength = onQueryParamChange ? queryParams.length - 1 : queryParams.length;
const headersLength = onHeaderChange ? headers.length - 1 : headers.length;
return (
<Box height="100%" sx={{ display: "flex", flexDirection: "column" }}>
<Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
<TabContext value={tabValue}>
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 2 }}>
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 1 }}>
<TabList onChange={(_, value) => setTabValue(value)}>
<Tab
value={TabValue.QueryParams}
label={"Query Params" + (queryParams.length - 1 ? ` (${queryParams.length - 1})` : "")}
sx={tabSx}
/>
<Tab
value={TabValue.Headers}
label={"Headers" + (headers.length - 1 ? ` (${headers.length - 1})` : "")}
label={"Query Params" + (queryParamsLength ? ` (${queryParamsLength})` : "")}
sx={tabSx}
/>
<Tab value={TabValue.Headers} label={"Headers" + (headersLength ? ` (${headersLength})` : "")} sx={tabSx} />
<Tab
value={TabValue.Body}
label={"Body" + (body.length ? ` (${body.length} byte` + (body.length > 1 ? "s" : "") + ")" : "")}
label={"Body" + (body?.length ? ` (${body.length} byte` + (body.length > 1 ? "s" : "") + ")" : "")}
sx={tabSx}
/>
</TabList>
</Box>
<Box flex="1 auto" overflow="hidden">
<TabPanel value={TabValue.QueryParams} sx={{ p: 0, height: "100%", overflow: "scroll" }}>
<Box flex="1 auto" overflow="scroll" height="100%">
<TabPanel value={TabValue.QueryParams} sx={{ p: 0, height: "100%" }}>
<Box>
<KeyValuePairTable items={queryParams} onChange={onQueryParamChange} onDelete={onQueryParamDelete} />
</Box>
</TabPanel>
<TabPanel value={TabValue.Headers} sx={{ p: 0, height: "100%", overflow: "scroll" }}>
<TabPanel value={TabValue.Headers} sx={{ p: 0, height: "100%" }}>
<Box>
<KeyValuePairTable items={headers} onChange={onHeaderChange} onDelete={onHeaderDelete} />
</Box>
</TabPanel>
<TabPanel value={TabValue.Body} sx={{ p: 0, height: "100%" }}>
<Editor
content={body}
content={body || ""}
onChange={(value) => {
onBodyChange(value || "");
onBodyChange && onBodyChange(value || "");
}}
monacoOptions={{ readOnly: false }}
monacoOptions={{ readOnly: onBodyChange === undefined }}
contentType={headers.find(({ key }) => key.toLowerCase() === "content-type")?.value}
/>
</TabPanel>
@ -89,4 +88,4 @@ function EditRequestTabs(props: EditRequestTabsProps): JSX.Element {
);
}
export default EditRequestTabs;
export default RequestTabs;

View file

@ -0,0 +1,125 @@
import {
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
styled,
TableCellProps,
TableRowProps,
} from "@mui/material";
import HttpStatusIcon from "./HttpStatusIcon";
import { HttpMethod } from "lib/graphql/generated";
const baseCellStyle = {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
} as const;
const MethodTableCell = styled(TableCell)<TableCellProps>(() => ({
...baseCellStyle,
width: "100px",
}));
const OriginTableCell = styled(TableCell)<TableCellProps>(() => ({
...baseCellStyle,
maxWidth: "100px",
}));
const PathTableCell = styled(TableCell)<TableCellProps>(() => ({
...baseCellStyle,
maxWidth: "200px",
}));
const StatusTableCell = styled(TableCell)<TableCellProps>(() => ({
...baseCellStyle,
width: "100px",
}));
const RequestTableRow = styled(TableRow)<TableRowProps>(() => ({
"&:hover": {
cursor: "pointer",
},
}));
interface HttpRequest {
id: string;
url: string;
method: HttpMethod;
response?: HttpResponse | null;
}
interface HttpResponse {
statusCode: number;
statusReason: string;
body?: string;
}
interface Props {
requests: HttpRequest[];
activeRowId?: string;
onRowClick?: (id: string) => void;
onContextMenu?: (e: React.MouseEvent, id: string) => void;
}
export default function RequestsTable(props: Props): JSX.Element {
const { requests, activeRowId, onRowClick, onContextMenu } = props;
return (
<TableContainer sx={{ overflowX: "initial" }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>Method</TableCell>
<TableCell>Origin</TableCell>
<TableCell>Path</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{requests.map(({ id, method, url, response }) => {
const { origin, pathname, search, hash } = new URL(url);
return (
<RequestTableRow
key={id}
hover
selected={id === activeRowId}
onClick={() => {
onRowClick && onRowClick(id);
}}
onContextMenu={(e) => {
onContextMenu && onContextMenu(e, id);
}}
>
<MethodTableCell>
<code>{method}</code>
</MethodTableCell>
<OriginTableCell>{origin}</OriginTableCell>
<PathTableCell>{decodeURIComponent(pathname + search + hash)}</PathTableCell>
<StatusTableCell>
{response && <Status code={response.statusCode} reason={response.statusReason} />}
</StatusTableCell>
</RequestTableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
);
}
function Status({ code, reason }: { code: number; reason: string }): JSX.Element {
return (
<div>
<HttpStatusIcon status={code} />{" "}
<code>
{code} {reason}
</code>
</div>
);
}

View file

@ -13,22 +13,20 @@ interface ResponseProps {
function Response({ response }: ResponseProps): JSX.Element {
return (
<Box height="100%">
<div>
<Box sx={{ position: "absolute", right: 0, mt: 1.4 }}>
<Typography variant="overline" color="textSecondary" sx={{ float: "right", ml: 3 }}>
Response
</Typography>
{response && (
<Box sx={{ float: "right", mt: 0.2 }}>
<ResponseStatus
proto={response.proto}
statusCode={response.statusCode}
statusReason={response.statusReason}
/>
</Box>
)}
</Box>
</div>
<Box sx={{ position: "absolute", right: 0, mt: 1.4 }}>
<Typography variant="overline" color="textSecondary" sx={{ float: "right", ml: 3 }}>
Response
</Typography>
{response && (
<Box sx={{ float: "right", mt: 0.2 }}>
<ResponseStatus
proto={response.proto}
statusCode={response.statusCode}
statusReason={response.statusReason}
/>
</Box>
)}
</Box>
<ResponseTabs
body={response?.body}
headers={sortKeyValuePairs(response?.headers || [])}

View file

@ -1,11 +1,9 @@
import { TabContext, TabList, TabPanel } from "@mui/lab";
import { Box, Tab, Typography } from "@mui/material";
import { Box, Paper, Tab, Typography } from "@mui/material";
import React, { useState } from "react";
import { KeyValuePairTable } from "./KeyValuePair";
import CenteredPaper from "lib/components/CenteredPaper";
import Editor from "lib/components/Editor";
import { KeyValuePairTable } from "lib/components/KeyValuePair";
import { HttpResponseLog } from "lib/graphql/generated";
interface ResponseTabsProps {
@ -20,9 +18,9 @@ enum TabValue {
}
const reqNotSent = (
<CenteredPaper>
<Paper variant="centered">
<Typography>Response not received yet.</Typography>
</CenteredPaper>
</Paper>
);
function ResponseTabs(props: ResponseTabsProps): JSX.Element {
@ -38,7 +36,7 @@ function ResponseTabs(props: ResponseTabsProps): JSX.Element {
return (
<Box height="100%" sx={{ display: "flex", flexDirection: "column" }}>
<TabContext value={tabValue}>
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 2 }}>
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 1 }}>
<TabList onChange={(_, value) => setTabValue(value)}>
<Tab
value={TabValue.Body}

View file

@ -0,0 +1,53 @@
import { alpha, styled } from "@mui/material/styles";
import ReactSplitPane, { SplitPaneProps } from "react-split-pane";
const BORDER_WIDTH_FACTOR = 1.75;
const SIZE_FACTOR = 4;
const MARGIN_FACTOR = -1.75;
const SplitPane = styled(ReactSplitPane)<SplitPaneProps>(({ theme }) => ({
".Resizer": {
zIndex: theme.zIndex.mobileStepper,
boxSizing: "border-box",
backgroundClip: "padding-box",
backgroundColor: alpha(theme.palette.grey[400], 0.05),
},
".Resizer:hover": {
transition: "all 0.5s ease",
backgroundColor: alpha(theme.palette.primary.main, 1),
},
".Resizer.horizontal": {
height: theme.spacing(SIZE_FACTOR),
marginTop: theme.spacing(MARGIN_FACTOR),
marginBottom: theme.spacing(MARGIN_FACTOR),
borderTop: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`,
borderBottom: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`,
borderBottomColor: "rgba(255, 255, 255, 0)",
cursor: "row-resize",
width: "100%",
},
".Resizer.vertical": {
width: theme.spacing(SIZE_FACTOR),
marginLeft: theme.spacing(MARGIN_FACTOR),
marginRight: theme.spacing(MARGIN_FACTOR),
borderLeft: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`,
borderRight: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`,
cursor: "col-resize",
},
".Resizer.disabled": {
cursor: "not-allowed",
},
".Resizer.disabled:hover": {
borderColor: "transparent",
},
".Pane": {
overflow: "hidden",
},
}));
export default SplitPane;

View file

@ -288,7 +288,7 @@ export type HttpRequestLogQueryVariables = Exact<{
}>;
export type HttpRequestLogQuery = { __typename?: 'Query', httpRequestLog?: { __typename?: 'HttpRequestLog', id: string, method: HttpMethod, url: string, proto: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }>, response?: { __typename?: 'HttpResponseLog', proto: HttpProtocol, statusCode: number, statusReason: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null } | null };
export type HttpRequestLogQuery = { __typename?: 'Query', httpRequestLog?: { __typename?: 'HttpRequestLog', id: string, method: HttpMethod, url: string, proto: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }>, response?: { __typename?: 'HttpResponseLog', id: string, proto: HttpProtocol, statusCode: number, statusReason: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null } | null };
export type HttpRequestLogFilterQueryVariables = Exact<{ [key: string]: never; }>;
@ -568,6 +568,7 @@ export const HttpRequestLogDocument = gql`
}
body
response {
id
proto
headers {
key

View file

@ -1,6 +1,12 @@
import * as colors from "@mui/material/colors";
import { createTheme } from "@mui/material/styles";
declare module "@mui/material/Paper" {
interface PaperPropsVariantOverrides {
centered: true;
}
}
const heading = {
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 600,
@ -41,13 +47,28 @@ theme = createTheme(theme, {
},
},
components: {
MuiTableCell: {
MuiTableRow: {
styleOverrides: {
stickyHeader: {
backgroundColor: theme.palette.secondary.dark,
root: {
"&.Mui-selected, &.Mui-selected:hover": {
backgroundColor: theme.palette.grey[700],
},
},
},
},
MuiPaper: {
variants: [
{
props: { variant: "centered" },
style: {
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: theme.spacing(4),
},
},
],
},
},
});

View file

@ -0,0 +1,17 @@
import { KeyValuePair } from "./components/KeyValuePair";
export function queryParamsFromURL(url: string): KeyValuePair[] {
const questionMarkIndex = url.indexOf("?");
if (questionMarkIndex === -1) {
return [];
}
const queryParams: KeyValuePair[] = [];
const searchParams = new URLSearchParams(url.slice(questionMarkIndex + 1));
for (const [key, value] of searchParams) {
queryParams.push({ key, value });
}
return queryParams;
}

View file

@ -1,11 +1,12 @@
import { ApolloProvider } from "@apollo/client";
import { CacheProvider, EmotionCache } from "@emotion/react";
import { ThemeProvider } from "@mui/material";
import CssBaseline from "@mui/material/CssBaseline";
import { ThemeProvider } from "@mui/material/styles";
import { AppProps } from "next/app";
import Head from "next/head";
import React from "react";
import { ActiveProjectProvider } from "lib/ActiveProjectContext";
import { useApollo } from "lib/graphql/useApollo";
import createEmotionCache from "lib/mui/createEmotionCache";
import theme from "lib/mui/theme";
@ -30,10 +31,12 @@ export default function MyApp(props: MyAppProps) {
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<ApolloProvider client={apolloClient}>
<ThemeProvider theme={theme}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
<ActiveProjectProvider>
<ThemeProvider theme={theme}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
</ActiveProjectProvider>
</ApolloProvider>
</CacheProvider>
);

View file

@ -1,16 +1,10 @@
import { Box } from "@mui/material";
import { Layout, Page } from "features/Layout";
import LogsOverview from "features/reqlog/components/LogsOverview";
import Search from "features/reqlog/components/Search";
import RequestLogs from "features/reqlog";
function ProxyLogs(): JSX.Element {
return (
<Layout page={Page.ProxyLogs} title="Proxy logs">
<Box mb={2}>
<Search />
</Box>
<LogsOverview />
<RequestLogs />
</Layout>
);
}

View file

@ -1,47 +1,10 @@
import { Box } from "@mui/system";
import { AllotmentProps } from "allotment";
import { PaneProps } from "allotment/dist/types/src/allotment";
import { ComponentType, useEffect, useRef, useState } from "react";
import { Layout, Page } from "features/Layout";
import EditRequest from "features/sender/components/EditRequest";
import History from "features/sender/components/History";
import Sender from "features/sender";
function Index(): JSX.Element {
const isMountedRef = useRef(false);
const [Allotment, setAllotment] = useState<
(ComponentType<AllotmentProps> & { Pane: ComponentType<PaneProps> }) | null
>(null);
useEffect(() => {
isMountedRef.current = true;
import("allotment")
.then((mod) => {
if (!isMountedRef.current) {
return;
}
setAllotment(mod.Allotment);
})
.catch((err) => console.error(err, `could not import allotment ${err.message}`));
return () => {
isMountedRef.current = false;
};
}, []);
if (!Allotment) {
return <div>Loading...</div>;
}
return (
<Layout page={Page.Sender} title="Sender">
<Allotment vertical={true} defaultSizes={[70, 30]}>
<Box sx={{ pt: 0.75, height: "100%" }}>
<EditRequest />
</Box>
<Box sx={{ height: "100%", py: 2, overflow: "hidden" }}>
<Box sx={{ height: "100%", overflow: "scroll" }}>
<History />
</Box>
</Box>
</Allotment>
<Sender />
</Layout>
);
}

View file

@ -499,7 +499,7 @@
core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.17.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.8.3":
version "7.17.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941"
integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==
@ -1229,6 +1229,15 @@
"@mui/utils" "^5.3.0"
prop-types "^15.7.2"
"@mui/private-theming@^5.4.2":
version "5.4.2"
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.4.2.tgz#f0a05f908456a2f7b87ccb6fc3b6e1faae9d89e6"
integrity sha512-mlPDYYko4wIcwXjCPEmOWbNTT4DZ6h9YHdnRtQPnWM28+TRUHEo7SbydnnmVDQLRXUfaH4Y6XtEHIfBNPE/SLg==
dependencies:
"@babel/runtime" "^7.17.0"
"@mui/utils" "^5.4.2"
prop-types "^15.7.2"
"@mui/styled-engine@^5.3.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.3.0.tgz#b260a06398fc7335a62fd65ebbb9fc3c4071027b"
@ -1238,6 +1247,29 @@
"@emotion/cache" "^11.7.1"
prop-types "^15.7.2"
"@mui/styles@^5.4.2":
version "5.4.2"
resolved "https://registry.yarnpkg.com/@mui/styles/-/styles-5.4.2.tgz#e0dadfc5de8255605f23c2f909f3669f0911bb88"
integrity sha512-BX75fNHmRF51yove9dBkH28gpSFjClOPDEnUwLTghPYN913OsqViS/iuCd61dxzygtEEmmeYuWfQjxu/F6vF5g==
dependencies:
"@babel/runtime" "^7.17.0"
"@emotion/hash" "^0.8.0"
"@mui/private-theming" "^5.4.2"
"@mui/types" "^7.1.2"
"@mui/utils" "^5.4.2"
clsx "^1.1.1"
csstype "^3.0.10"
hoist-non-react-statics "^3.3.2"
jss "^10.8.2"
jss-plugin-camel-case "^10.8.2"
jss-plugin-default-unit "^10.8.2"
jss-plugin-global "^10.8.2"
jss-plugin-nested "^10.8.2"
jss-plugin-props-sort "^10.8.2"
jss-plugin-rule-value-function "^10.8.2"
jss-plugin-vendor-prefixer "^10.8.2"
prop-types "^15.7.2"
"@mui/system@^5.3.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.3.0.tgz#cd2c5fd7631f2c90f0072c866015bb24e319b66e"
@ -1257,6 +1289,11 @@
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.0.tgz#5ed928c5a41cfbf9a4be82ea3bbdc47bcc9610d5"
integrity sha512-Hh7ALdq/GjfIwLvqH3XftuY3bcKhupktTm+S6qRIDGOtPtRuq2L21VWzOK4p7kblirK0XgGVH5BLwa6u8z/6QQ==
"@mui/types@^7.1.2":
version "7.1.2"
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.2.tgz#4f3678ae77a7a3efab73b6e040469cc6df2144ac"
integrity sha512-SD7O1nVzqG+ckQpFjDhXPZjRceB8HQFHEvdLLrPhlJy4lLbwEBbxK74Tj4t6Jgk0fTvLJisuwOutrtYe9P/xBQ==
"@mui/utils@^5.3.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.3.0.tgz#5f31915063d25c56f1d3ba9e289bf447472a868c"
@ -1268,6 +1305,17 @@
prop-types "^15.7.2"
react-is "^17.0.2"
"@mui/utils@^5.4.2":
version "5.4.2"
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.4.2.tgz#3edda8f80de235418fff0424ee66e2a49793ec01"
integrity sha512-646dBCC57MXTo/Gf3AnZSHRHznaTETQq5x7AWp5FRQ4jPeyT4WSs18cpJVwkV01cAHKh06pNQTIufIALIWCL5g==
dependencies:
"@babel/runtime" "^7.17.0"
"@types/prop-types" "^15.7.4"
"@types/react-is" "^16.7.1 || ^17.0.0"
prop-types "^15.7.2"
react-is "^17.0.2"
"@n1ru4l/graphql-live-query@^0.9.0":
version "0.9.0"
resolved "https://registry.yarnpkg.com/@n1ru4l/graphql-live-query/-/graphql-live-query-0.9.0.tgz#defaebdd31f625bee49e6745934f36312532b2bc"
@ -2489,6 +2537,14 @@ cross-undici-fetch@^0.1.19:
undici "^4.9.3"
web-streams-polyfill "^3.2.0"
css-vendor@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d"
integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==
dependencies:
"@babel/runtime" "^7.8.3"
is-in-browser "^1.0.2"
csstype@^3.0.10, csstype@^3.0.2:
version "3.0.10"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5"
@ -3490,6 +3546,11 @@ https-proxy-agent@^5.0.0:
agent-base "6"
debug "4"
hyphenate-style-name@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -3677,6 +3738,11 @@ is-glob@4.0.3, is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
dependencies:
is-extglob "^2.1.1"
is-in-browser@^1.0.2, is-in-browser@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835"
integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=
is-interactive@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
@ -3931,6 +3997,76 @@ jsonwebtoken@^8.5.1:
ms "^2.1.1"
semver "^5.6.0"
jss-plugin-camel-case@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.9.0.tgz#4921b568b38d893f39736ee8c4c5f1c64670aaf7"
integrity sha512-UH6uPpnDk413/r/2Olmw4+y54yEF2lRIV8XIZyuYpgPYTITLlPOsq6XB9qeqv+75SQSg3KLocq5jUBXW8qWWww==
dependencies:
"@babel/runtime" "^7.3.1"
hyphenate-style-name "^1.0.3"
jss "10.9.0"
jss-plugin-default-unit@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.9.0.tgz#bb23a48f075bc0ce852b4b4d3f7582bc002df991"
integrity sha512-7Ju4Q9wJ/MZPsxfu4T84mzdn7pLHWeqoGd/D8O3eDNNJ93Xc8PxnLmV8s8ZPNRYkLdxZqKtm1nPQ0BM4JRlq2w==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
jss-plugin-global@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.9.0.tgz#fc07a0086ac97aca174e37edb480b69277f3931f"
integrity sha512-4G8PHNJ0x6nwAFsEzcuVDiBlyMsj2y3VjmFAx/uHk/R/gzJV+yRHICjT4MKGGu1cJq2hfowFWCyrr/Gg37FbgQ==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
jss-plugin-nested@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.9.0.tgz#cc1c7d63ad542c3ccc6e2c66c8328c6b6b00f4b3"
integrity sha512-2UJnDrfCZpMYcpPYR16oZB7VAC6b/1QLsRiAutOt7wJaaqwCBvNsosLEu/fUyKNQNGdvg2PPJFDO5AX7dwxtoA==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
tiny-warning "^1.0.2"
jss-plugin-props-sort@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.9.0.tgz#30e9567ef9479043feb6e5e59db09b4de687c47d"
integrity sha512-7A76HI8bzwqrsMOJTWKx/uD5v+U8piLnp5bvru7g/3ZEQOu1+PjHvv7bFdNO3DwNPC9oM0a//KwIJsIcDCjDzw==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
jss-plugin-rule-value-function@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.9.0.tgz#379fd2732c0746fe45168011fe25544c1a295d67"
integrity sha512-IHJv6YrEf8pRzkY207cPmdbBstBaE+z8pazhPShfz0tZSDtRdQua5jjg6NMz3IbTasVx9FdnmptxPqSWL5tyJg==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
tiny-warning "^1.0.2"
jss-plugin-vendor-prefixer@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.9.0.tgz#aa9df98abfb3f75f7ed59a3ec50a5452461a206a"
integrity sha512-MbvsaXP7iiVdYVSEoi+blrW+AYnTDvHTW6I6zqi7JcwXdc6I9Kbm234nEblayhF38EftoenbM+5218pidmC5gA==
dependencies:
"@babel/runtime" "^7.3.1"
css-vendor "^2.0.8"
jss "10.9.0"
jss@10.9.0, jss@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss/-/jss-10.9.0.tgz#7583ee2cdc904a83c872ba695d1baab4b59c141b"
integrity sha512-YpzpreB6kUunQBbrlArlsMpXYyndt9JATbt95tajx0t4MTJJcCJdd4hdNpHmOIDiUJrF/oX5wtVFrS3uofWfGw==
dependencies:
"@babel/runtime" "^7.3.1"
csstype "^3.0.2"
is-in-browser "^1.1.3"
tiny-warning "^1.0.2"
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b"
@ -4759,7 +4895,7 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.4, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -4822,11 +4958,32 @@ react-is@^16.13.1, react-is@^16.7.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-refresh@0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
react-split-pane@^0.1.92:
version "0.1.92"
resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.92.tgz#68242f72138aed95dd5910eeb9d99822c4fc3a41"
integrity sha512-GfXP1xSzLMcLJI5BM36Vh7GgZBpy+U/X0no+VM3fxayv+p1Jly5HpMofZJraeaMl73b3hvlr+N9zJKvLB/uz9w==
dependencies:
prop-types "^15.7.2"
react-lifecycles-compat "^3.0.4"
react-style-proptype "^3.2.2"
react-style-proptype@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/react-style-proptype/-/react-style-proptype-3.2.2.tgz#d8e998e62ce79ec35b087252b90f19f1c33968a0"
integrity sha512-ywYLSjNkxKHiZOqNlso9PZByNEY+FTyh3C+7uuziK0xFXu9xzdyfHwg4S9iyiRRoPCR4k2LqaBBsWVmSBwCWYQ==
dependencies:
prop-types "^15.5.4"
react-transition-group@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
@ -5503,6 +5660,11 @@ through@^2.3.6, through@^2.3.8:
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
title-case@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/title-case/-/title-case-3.0.3.tgz#bc689b46f02e411f1d1e1d081f7c3deca0489982"