mirror of
https://github.com/dstotijn/hetty
synced 2024-11-10 06:04:19 +00:00
Reuse components across Proxy and Sender modules
This commit is contained in:
parent
11f70282d7
commit
7e43479b54
33 changed files with 859 additions and 818 deletions
|
@ -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",
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
90
admin/src/features/reqlog/components/RequestLogs.tsx
Normal file
90
admin/src/features/reqlog/components/RequestLogs.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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,
|
||||
|
|
|
@ -10,6 +10,7 @@ query HttpRequestLog($id: ID!) {
|
|||
}
|
||||
body
|
||||
response {
|
||||
id
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
|
|
3
admin/src/features/reqlog/index.ts
Normal file
3
admin/src/features/reqlog/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { RequestLogs } from "./components/RequestLogs";
|
||||
|
||||
export default RequestLogs;
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
21
admin/src/features/sender/components/Sender.tsx
Normal file
21
admin/src/features/sender/components/Sender.tsx
Normal 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>
|
||||
);
|
||||
}
|
3
admin/src/features/sender/index.ts
Normal file
3
admin/src/features/sender/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Sender from "./components/Sender";
|
||||
|
||||
export default Sender;
|
20
admin/src/lib/ActiveProjectContext.tsx
Normal file
20
admin/src/lib/ActiveProjectContext.tsx
Normal 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);
|
||||
}
|
|
@ -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;
|
201
admin/src/lib/components/KeyValuePair.tsx
Normal file
201
admin/src/lib/components/KeyValuePair.tsx
Normal 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;
|
|
@ -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;
|
125
admin/src/lib/components/RequestsTable.tsx
Normal file
125
admin/src/lib/components/RequestsTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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 || [])}
|
|
@ -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}
|
53
admin/src/lib/components/SplitPane.tsx
Normal file
53
admin/src/lib/components/SplitPane.tsx
Normal 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;
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
17
admin/src/lib/queryParamsFromURL.tsx
Normal file
17
admin/src/lib/queryParamsFromURL.tsx
Normal 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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
166
admin/yarn.lock
166
admin/yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue