mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Add initial UI/UX for intecepting requests
This commit is contained in:
@ -1,11 +1,12 @@
|
||||
import AltRouteIcon from "@mui/icons-material/AltRoute";
|
||||
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import FolderIcon from "@mui/icons-material/Folder";
|
||||
import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted";
|
||||
import HomeIcon from "@mui/icons-material/Home";
|
||||
import LocationSearchingIcon from "@mui/icons-material/LocationSearching";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import SendIcon from "@mui/icons-material/Send";
|
||||
import SettingsEthernetIcon from "@mui/icons-material/SettingsEthernet";
|
||||
import {
|
||||
Theme,
|
||||
useTheme,
|
||||
@ -19,6 +20,7 @@ import {
|
||||
CSSObject,
|
||||
Box,
|
||||
ListItemText,
|
||||
Badge,
|
||||
} from "@mui/material";
|
||||
import MuiAppBar, { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar";
|
||||
import MuiDrawer from "@mui/material/Drawer";
|
||||
@ -28,10 +30,12 @@ import Link from "next/link";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useActiveProject } from "lib/ActiveProjectContext";
|
||||
import { useInterceptedRequests } from "lib/InterceptedRequestsContext";
|
||||
|
||||
export enum Page {
|
||||
Home,
|
||||
GetStarted,
|
||||
Intercept,
|
||||
Projects,
|
||||
ProxySetup,
|
||||
ProxyLogs,
|
||||
@ -135,6 +139,7 @@ interface Props {
|
||||
|
||||
export function Layout({ title, page, children }: Props): JSX.Element {
|
||||
const activeProject = useActiveProject();
|
||||
const interceptedRequests = useInterceptedRequests();
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@ -204,12 +209,24 @@ export function Layout({ title, page, children }: Props): JSX.Element {
|
||||
</Link>
|
||||
<Link href="/proxy/logs" passHref>
|
||||
<ListItemButton key="proxyLogs" disabled={!activeProject} selected={page === Page.ProxyLogs}>
|
||||
<Tooltip title="Proxy">
|
||||
<Tooltip title="Proxy logs">
|
||||
<ListItemIcon>
|
||||
<SettingsEthernetIcon />
|
||||
<FormatListBulletedIcon />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<ListItemText primary="Proxy" />
|
||||
<ListItemText primary="Logs" />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
<Link href="/proxy/intercept" passHref>
|
||||
<ListItemButton key="proxyIntercept" disabled={!activeProject} selected={page === Page.Intercept}>
|
||||
<Tooltip title="Proxy intercept">
|
||||
<ListItemIcon>
|
||||
<Badge color="error" badgeContent={interceptedRequests?.length || 0}>
|
||||
<AltRouteIcon />
|
||||
</Badge>
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<ListItemText primary="Intercept" />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
<Link href="/sender" passHref>
|
||||
|
203
admin/src/features/intercept/components/EditRequest.tsx
Normal file
203
admin/src/features/intercept/components/EditRequest.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
import SendIcon from "@mui/icons-material/Send";
|
||||
import { Alert, Box, Button, CircularProgress, Typography } from "@mui/material";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useInterceptedRequests } from "lib/InterceptedRequestsContext";
|
||||
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 UrlBar, { HttpMethod, HttpProto, httpProtoMap } from "lib/components/UrlBar";
|
||||
import {
|
||||
HttpProtocol,
|
||||
HttpRequest,
|
||||
useGetInterceptedRequestQuery,
|
||||
useModifyRequestMutation,
|
||||
} from "lib/graphql/generated";
|
||||
import { queryParamsFromURL } from "lib/queryParamsFromURL";
|
||||
import updateKeyPairItem from "lib/updateKeyPairItem";
|
||||
import updateURLQueryParams from "lib/updateURLQueryParams";
|
||||
|
||||
function EditRequest(): JSX.Element {
|
||||
const router = useRouter();
|
||||
const interceptedRequests = useInterceptedRequests();
|
||||
|
||||
useEffect(() => {
|
||||
// If there's no request selected and there are pending reqs, navigate to
|
||||
// the first one in the list. This helps you quickly review/handle reqs
|
||||
// without having to manually select the next one in the requests table.
|
||||
console.log(router.isReady, router.query.id, interceptedRequests?.length);
|
||||
if (router.isReady && !router.query.id && interceptedRequests?.length) {
|
||||
const req = interceptedRequests[0];
|
||||
router.replace(`/proxy/intercept?id=${req.id}`);
|
||||
}
|
||||
}, [router, interceptedRequests]);
|
||||
|
||||
const reqId = router.query.id as string | undefined;
|
||||
|
||||
const [method, setMethod] = useState(HttpMethod.Get);
|
||||
const [url, setURL] = useState("");
|
||||
const [proto, setProto] = useState(HttpProto.Http20);
|
||||
const [queryParams, setQueryParams] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
||||
const [headers, setHeaders] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
||||
const [body, setBody] = useState("");
|
||||
|
||||
const handleQueryParamChange = (key: string, value: string, idx: number) => {
|
||||
setQueryParams((prev) => {
|
||||
const updated = updateKeyPairItem(key, value, idx, prev);
|
||||
setURL((prev) => updateURLQueryParams(prev, updated));
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
const handleQueryParamDelete = (idx: number) => {
|
||||
setQueryParams((prev) => {
|
||||
const updated = prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length));
|
||||
setURL((prev) => updateURLQueryParams(prev, updated));
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const handleHeaderChange = (key: string, value: string, idx: number) => {
|
||||
setHeaders((prev) => updateKeyPairItem(key, value, idx, prev));
|
||||
};
|
||||
const handleHeaderDelete = (idx: number) => {
|
||||
setHeaders((prev) => prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length)));
|
||||
};
|
||||
|
||||
const handleURLChange = (url: string) => {
|
||||
setURL(url);
|
||||
|
||||
const questionMarkIndex = url.indexOf("?");
|
||||
if (questionMarkIndex === -1) {
|
||||
setQueryParams([{ key: "", value: "" }]);
|
||||
return;
|
||||
}
|
||||
|
||||
const newQueryParams = queryParamsFromURL(url);
|
||||
// Push empty row.
|
||||
newQueryParams.push({ key: "", value: "" });
|
||||
setQueryParams(newQueryParams);
|
||||
};
|
||||
|
||||
const getReqResult = useGetInterceptedRequestQuery({
|
||||
variables: { id: reqId as string },
|
||||
skip: reqId === undefined,
|
||||
onCompleted: ({ interceptedRequest }) => {
|
||||
if (!interceptedRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
setURL(interceptedRequest.url);
|
||||
setMethod(interceptedRequest.method);
|
||||
setBody(interceptedRequest.body || "");
|
||||
|
||||
const newQueryParams = queryParamsFromURL(interceptedRequest.url);
|
||||
// Push empty row.
|
||||
newQueryParams.push({ key: "", value: "" });
|
||||
setQueryParams(newQueryParams);
|
||||
|
||||
const newHeaders = sortKeyValuePairs(interceptedRequest.headers || []);
|
||||
setHeaders([...newHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
||||
},
|
||||
});
|
||||
const interceptedReq = reqId ? getReqResult?.data?.interceptedRequest : undefined;
|
||||
|
||||
const [modifyRequest, modifyResult] = useModifyRequestMutation();
|
||||
|
||||
const handleFormSubmit: React.FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!interceptedReq) {
|
||||
return;
|
||||
}
|
||||
|
||||
modifyRequest({
|
||||
variables: {
|
||||
request: {
|
||||
id: interceptedReq.id,
|
||||
url,
|
||||
method,
|
||||
proto: httpProtoMap.get(proto) || HttpProtocol.Http20,
|
||||
headers: headers.filter((kv) => kv.key !== ""),
|
||||
body: body || undefined,
|
||||
},
|
||||
},
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
interceptedRequests(existing: HttpRequest[], { readField }) {
|
||||
return existing.filter((ref) => interceptedReq.id !== readField("id", ref));
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
onCompleted: () => {
|
||||
setURL("");
|
||||
setMethod(HttpMethod.Get);
|
||||
setBody("");
|
||||
setQueryParams([]);
|
||||
setHeaders([]);
|
||||
console.log("done!");
|
||||
router.replace(`/proxy/intercept`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" height="100%" gap={2}>
|
||||
<Box component="form" autoComplete="off" onSubmit={handleFormSubmit}>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
<UrlBar
|
||||
method={method}
|
||||
onMethodChange={interceptedReq ? setMethod : undefined}
|
||||
url={url.toString()}
|
||||
onUrlChange={interceptedReq ? handleURLChange : undefined}
|
||||
proto={proto}
|
||||
onProtoChange={interceptedReq ? setProto : undefined}
|
||||
sx={{ flex: "1 auto" }}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
disableElevation
|
||||
type="submit"
|
||||
disabled={!interceptedReq || modifyResult.loading}
|
||||
startIcon={modifyResult.loading ? <CircularProgress size={22} /> : <SendIcon />}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</Box>
|
||||
{modifyResult.error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{modifyResult.error.message}
|
||||
</Alert>
|
||||
)}
|
||||
</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={interceptedReq ? queryParams : []}
|
||||
headers={interceptedReq ? headers : []}
|
||||
body={body}
|
||||
onQueryParamChange={interceptedReq ? handleQueryParamChange : undefined}
|
||||
onQueryParamDelete={interceptedReq ? handleQueryParamDelete : undefined}
|
||||
onHeaderChange={interceptedReq ? handleHeaderChange : undefined}
|
||||
onHeaderDelete={interceptedReq ? handleHeaderDelete : undefined}
|
||||
onBodyChange={interceptedReq ? setBody : undefined}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ height: "100%", position: "relative", ml: 2, pb: 2 }}>
|
||||
<Response response={null} />
|
||||
</Box>
|
||||
</SplitPane>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditRequest;
|
21
admin/src/features/intercept/components/Intercept.tsx
Normal file
21
admin/src/features/intercept/components/Intercept.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
import EditRequest from "./EditRequest";
|
||||
import Requests from "./Requests";
|
||||
|
||||
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" }}>
|
||||
<Requests />
|
||||
</Box>
|
||||
</SplitPane>
|
||||
</Box>
|
||||
);
|
||||
}
|
33
admin/src/features/intercept/components/Requests.tsx
Normal file
33
admin/src/features/intercept/components/Requests.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Box, Paper, Typography } from "@mui/material";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useInterceptedRequests } from "lib/InterceptedRequestsContext";
|
||||
import RequestsTable from "lib/components/RequestsTable";
|
||||
|
||||
function Requests(): JSX.Element {
|
||||
const interceptedRequests = useInterceptedRequests();
|
||||
|
||||
const router = useRouter();
|
||||
const activeId = router.query.id as string | undefined;
|
||||
|
||||
const handleRowClick = (id: string) => {
|
||||
router.push(`/proxy/intercept?id=${id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{interceptedRequests && interceptedRequests.length > 0 && (
|
||||
<RequestsTable requests={interceptedRequests} onRowClick={handleRowClick} activeRowId={activeId} />
|
||||
)}
|
||||
<Box sx={{ mt: 2, height: "100%" }}>
|
||||
{interceptedRequests?.length === 0 && (
|
||||
<Paper variant="centered">
|
||||
<Typography>No pending intercepted requests.</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Requests;
|
@ -0,0 +1,13 @@
|
||||
query GetInterceptedRequest($id: ID!) {
|
||||
interceptedRequest(id: $id) {
|
||||
id
|
||||
url
|
||||
method
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
body
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
mutation ModifyRequest($request: ModifyRequestInput!) {
|
||||
modifyRequest(request: $request) {
|
||||
success
|
||||
}
|
||||
}
|
57
admin/src/features/reqlog/components/Actions.tsx
Normal file
57
admin/src/features/reqlog/components/Actions.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import AltRouteIcon from "@mui/icons-material/AltRoute";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import { Alert } from "@mui/lab";
|
||||
import { Badge, Button, IconButton, Tooltip } from "@mui/material";
|
||||
import Link from "next/link";
|
||||
|
||||
import { useInterceptedRequests } from "lib/InterceptedRequestsContext";
|
||||
import { ConfirmationDialog, useConfirmationDialog } from "lib/components/ConfirmationDialog";
|
||||
import { HttpRequestLogsDocument, useClearHttpRequestLogMutation } from "lib/graphql/generated";
|
||||
|
||||
function Actions(): JSX.Element {
|
||||
const interceptedRequests = useInterceptedRequests();
|
||||
const [clearHTTPRequestLog, clearLogsResult] = useClearHttpRequestLogMutation({
|
||||
refetchQueries: [{ query: HttpRequestLogsDocument }],
|
||||
});
|
||||
const clearHTTPConfirmationDialog = useConfirmationDialog();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ConfirmationDialog
|
||||
isOpen={clearHTTPConfirmationDialog.isOpen}
|
||||
onClose={clearHTTPConfirmationDialog.close}
|
||||
onConfirm={clearHTTPRequestLog}
|
||||
>
|
||||
All proxy logs are going to be removed. This action cannot be undone.
|
||||
</ConfirmationDialog>
|
||||
|
||||
{clearLogsResult.error && <Alert severity="error">Failed to clear HTTP logs: {clearLogsResult.error}</Alert>}
|
||||
|
||||
<Link href="/proxy/intercept/?id=" passHref>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={interceptedRequests === null || interceptedRequests.length === 0}
|
||||
color="primary"
|
||||
component="a"
|
||||
size="large"
|
||||
startIcon={
|
||||
<Badge color="error" badgeContent={interceptedRequests?.length || 0}>
|
||||
<AltRouteIcon />
|
||||
</Badge>
|
||||
}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Review Intercepted…
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Tooltip title="Clear all">
|
||||
<IconButton onClick={clearHTTPConfirmationDialog.open}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Actions;
|
@ -14,6 +14,7 @@ import {
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import Actions from "./Actions";
|
||||
import LogDetail from "./LogDetail";
|
||||
import Search from "./Search";
|
||||
|
||||
@ -94,7 +95,14 @@ export function RequestLogs(): JSX.Element {
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" height="100%">
|
||||
<Search />
|
||||
<Box display="flex">
|
||||
<Box flex="1 auto">
|
||||
<Search />
|
||||
</Box>
|
||||
<Box pt={0.5}>
|
||||
<Actions />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", flex: "1 auto", position: "relative" }}>
|
||||
<SplitPane split="horizontal" size={"40%"}>
|
||||
<Box sx={{ width: "100%", height: "100%", pb: 2 }}>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import { Alert } from "@mui/lab";
|
||||
@ -17,11 +16,8 @@ import {
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
import { ConfirmationDialog, useConfirmationDialog } from "lib/components/ConfirmationDialog";
|
||||
import {
|
||||
HttpRequestLogFilterDocument,
|
||||
HttpRequestLogsDocument,
|
||||
useClearHttpRequestLogMutation,
|
||||
useHttpRequestLogFilterQuery,
|
||||
useSetHttpRequestLogFilterMutation,
|
||||
} from "lib/graphql/generated";
|
||||
@ -49,11 +45,6 @@ function Search(): JSX.Element {
|
||||
},
|
||||
});
|
||||
|
||||
const [clearHTTPRequestLog, clearHTTPRequestLogResult] = useClearHttpRequestLogMutation({
|
||||
refetchQueries: [{ query: HttpRequestLogsDocument }],
|
||||
});
|
||||
const clearHTTPConfirmationDialog = useConfirmationDialog();
|
||||
|
||||
const filterRef = useRef<HTMLFormElement>(null);
|
||||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
|
||||
@ -81,7 +72,6 @@ function Search(): JSX.Element {
|
||||
<Box>
|
||||
<Error prefix="Error fetching filter" error={filterResult.error} />
|
||||
<Error prefix="Error setting filter" error={setFilterResult.error} />
|
||||
<Error prefix="Error clearing all HTTP logs" error={clearHTTPRequestLogResult.error} />
|
||||
<Box style={{ display: "flex", flex: 1 }}>
|
||||
<ClickAwayListener onClickAway={handleClickAway}>
|
||||
<Paper
|
||||
@ -161,21 +151,7 @@ function Search(): JSX.Element {
|
||||
</Popper>
|
||||
</Paper>
|
||||
</ClickAwayListener>
|
||||
<Box style={{ marginLeft: "auto" }}>
|
||||
<Tooltip title="Clear all">
|
||||
<IconButton onClick={clearHTTPConfirmationDialog.open}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<ConfirmationDialog
|
||||
isOpen={clearHTTPConfirmationDialog.isOpen}
|
||||
onClose={clearHTTPConfirmationDialog.close}
|
||||
onConfirm={clearHTTPRequestLog}
|
||||
>
|
||||
All proxy logs are going to be removed. This action cannot be undone.
|
||||
</ConfirmationDialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -1,15 +1,4 @@
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
BoxProps,
|
||||
Button,
|
||||
InputLabel,
|
||||
FormControl,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Alert, Box, Button, Typography } from "@mui/material";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useState } from "react";
|
||||
|
||||
@ -17,76 +6,16 @@ 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 UrlBar, { HttpMethod, HttpProto, httpProtoMap } from "lib/components/UrlBar";
|
||||
import {
|
||||
GetSenderRequestQuery,
|
||||
useCreateOrUpdateSenderRequestMutation,
|
||||
HttpProtocol,
|
||||
useGetSenderRequestQuery,
|
||||
useSendRequestMutation,
|
||||
} from "lib/graphql/generated";
|
||||
import { queryParamsFromURL } from "lib/queryParamsFromURL";
|
||||
|
||||
enum HttpMethod {
|
||||
Get = "GET",
|
||||
Post = "POST",
|
||||
Put = "PUT",
|
||||
Patch = "PATCH",
|
||||
Delete = "DELETE",
|
||||
Head = "HEAD",
|
||||
Options = "OPTIONS",
|
||||
Connect = "CONNECT",
|
||||
Trace = "TRACE",
|
||||
}
|
||||
|
||||
enum HttpProto {
|
||||
Http10 = "HTTP/1.0",
|
||||
Http11 = "HTTP/1.1",
|
||||
Http20 = "HTTP/2.0",
|
||||
}
|
||||
|
||||
const httpProtoMap = new Map([
|
||||
[HttpProto.Http10, HttpProtocol.Http10],
|
||||
[HttpProto.Http11, HttpProtocol.Http11],
|
||||
[HttpProto.Http20, HttpProtocol.Http20],
|
||||
]);
|
||||
|
||||
function updateKeyPairItem(key: string, value: string, idx: number, items: KeyValuePair[]): KeyValuePair[] {
|
||||
const updated = [...items];
|
||||
updated[idx] = { key, value };
|
||||
|
||||
// Append an empty key-value pair if the last item in the array isn't blank
|
||||
// anymore.
|
||||
if (items.length - 1 === idx && items[idx].key === "" && items[idx].value === "") {
|
||||
updated.push({ key: "", value: "" });
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
function updateURLQueryParams(url: string, queryParams: KeyValuePair[]) {
|
||||
// Note: We don't use the `URL` interface, because we're potentially dealing
|
||||
// with malformed/incorrect URLs, which would yield TypeErrors when constructed
|
||||
// via `URL`.
|
||||
let newURL = url;
|
||||
|
||||
const questionMarkIndex = url.indexOf("?");
|
||||
if (questionMarkIndex !== -1) {
|
||||
newURL = newURL.slice(0, questionMarkIndex);
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const { key, value } of queryParams.filter(({ key }) => key !== "")) {
|
||||
searchParams.append(key, value);
|
||||
}
|
||||
|
||||
const rawQueryParams = decodeURI(searchParams.toString());
|
||||
|
||||
if (rawQueryParams == "") {
|
||||
return newURL;
|
||||
}
|
||||
|
||||
return newURL + "?" + rawQueryParams;
|
||||
}
|
||||
import updateKeyPairItem from "lib/updateKeyPairItem";
|
||||
import updateURLQueryParams from "lib/updateURLQueryParams";
|
||||
|
||||
function EditRequest(): JSX.Element {
|
||||
const router = useRouter();
|
||||
@ -263,94 +192,4 @@ function EditRequest(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
interface UrlBarProps extends BoxProps {
|
||||
method: HttpMethod;
|
||||
onMethodChange: (method: HttpMethod) => void;
|
||||
url: string;
|
||||
onUrlChange: (url: string) => void;
|
||||
proto: HttpProto;
|
||||
onProtoChange: (proto: HttpProto) => void;
|
||||
}
|
||||
|
||||
function UrlBar(props: UrlBarProps) {
|
||||
const { method, onMethodChange, url, onUrlChange, proto, onProtoChange, ...other } = props;
|
||||
|
||||
return (
|
||||
<Box {...other} sx={{ ...other.sx, display: "flex" }}>
|
||||
<FormControl>
|
||||
<InputLabel id="req-method-label">Method</InputLabel>
|
||||
<Select
|
||||
labelId="req-method-label"
|
||||
id="req-method"
|
||||
value={method}
|
||||
label="Method"
|
||||
onChange={(e) => onMethodChange(e.target.value as HttpMethod)}
|
||||
sx={{
|
||||
width: "8rem",
|
||||
".MuiOutlinedInput-notchedOutline": {
|
||||
borderRightWidth: 0,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Object.values(HttpMethod).map((method) => (
|
||||
<MenuItem key={method} value={method}>
|
||||
{method}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField
|
||||
label="URL"
|
||||
placeholder="E.g. “https://example.com/foobar”"
|
||||
value={url}
|
||||
onChange={(e) => onUrlChange(e.target.value)}
|
||||
required
|
||||
variant="outlined"
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
InputProps={{
|
||||
sx: {
|
||||
".MuiOutlinedInput-notchedOutline": {
|
||||
borderRadius: 0,
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
<FormControl>
|
||||
<InputLabel id="req-proto-label">Protocol</InputLabel>
|
||||
<Select
|
||||
labelId="req-proto-label"
|
||||
id="req-proto"
|
||||
value={proto}
|
||||
label="Protocol"
|
||||
onChange={(e) => onProtoChange(e.target.value as HttpProto)}
|
||||
sx={{
|
||||
".MuiOutlinedInput-notchedOutline": {
|
||||
borderLeftWidth: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderLeftWidth: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Object.values(HttpProto).map((proto) => (
|
||||
<MenuItem key={proto} value={proto}>
|
||||
{proto}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditRequest;
|
||||
|
22
admin/src/lib/InterceptedRequestsContext.tsx
Normal file
22
admin/src/lib/InterceptedRequestsContext.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React, { createContext, useContext } from "react";
|
||||
|
||||
import { GetInterceptedRequestsQuery, useGetInterceptedRequestsQuery } from "./graphql/generated";
|
||||
|
||||
const InterceptedRequestsContext = createContext<GetInterceptedRequestsQuery["interceptedRequests"] | null>(null);
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode | undefined;
|
||||
}
|
||||
|
||||
export function InterceptedRequestsProvider({ children }: Props): JSX.Element {
|
||||
const { data } = useGetInterceptedRequestsQuery({
|
||||
pollInterval: 1000,
|
||||
});
|
||||
const reqs = data?.interceptedRequests || null;
|
||||
|
||||
return <InterceptedRequestsContext.Provider value={reqs}>{children}</InterceptedRequestsContext.Provider>;
|
||||
}
|
||||
|
||||
export function useInterceptedRequests() {
|
||||
return useContext(InterceptedRequestsContext);
|
||||
}
|
122
admin/src/lib/components/UrlBar.tsx
Normal file
122
admin/src/lib/components/UrlBar.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import { Box, BoxProps, FormControl, InputLabel, MenuItem, Select, TextField } from "@mui/material";
|
||||
|
||||
import { HttpProtocol } from "lib/graphql/generated";
|
||||
|
||||
export enum HttpMethod {
|
||||
Get = "GET",
|
||||
Post = "POST",
|
||||
Put = "PUT",
|
||||
Patch = "PATCH",
|
||||
Delete = "DELETE",
|
||||
Head = "HEAD",
|
||||
Options = "OPTIONS",
|
||||
Connect = "CONNECT",
|
||||
Trace = "TRACE",
|
||||
}
|
||||
|
||||
export enum HttpProto {
|
||||
Http10 = "HTTP/1.0",
|
||||
Http11 = "HTTP/1.1",
|
||||
Http20 = "HTTP/2.0",
|
||||
}
|
||||
|
||||
export const httpProtoMap = new Map([
|
||||
[HttpProto.Http10, HttpProtocol.Http10],
|
||||
[HttpProto.Http11, HttpProtocol.Http11],
|
||||
[HttpProto.Http20, HttpProtocol.Http20],
|
||||
]);
|
||||
|
||||
interface UrlBarProps extends BoxProps {
|
||||
method: HttpMethod;
|
||||
onMethodChange?: (method: HttpMethod) => void;
|
||||
url: string;
|
||||
onUrlChange?: (url: string) => void;
|
||||
proto: HttpProto;
|
||||
onProtoChange?: (proto: HttpProto) => void;
|
||||
}
|
||||
|
||||
function UrlBar(props: UrlBarProps) {
|
||||
const { method, onMethodChange, url, onUrlChange, proto, onProtoChange, ...other } = props;
|
||||
|
||||
return (
|
||||
<Box {...other} sx={{ ...other.sx, display: "flex" }}>
|
||||
<FormControl>
|
||||
<InputLabel id="req-method-label">Method</InputLabel>
|
||||
<Select
|
||||
labelId="req-method-label"
|
||||
id="req-method"
|
||||
value={method}
|
||||
label="Method"
|
||||
disabled={!onMethodChange}
|
||||
onChange={(e) => onMethodChange && onMethodChange(e.target.value as HttpMethod)}
|
||||
sx={{
|
||||
width: "8rem",
|
||||
".MuiOutlinedInput-notchedOutline": {
|
||||
borderRightWidth: 0,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Object.values(HttpMethod).map((method) => (
|
||||
<MenuItem key={method} value={method}>
|
||||
{method}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField
|
||||
label="URL"
|
||||
placeholder="E.g. “https://example.com/foobar”"
|
||||
value={url}
|
||||
disabled={!onUrlChange}
|
||||
onChange={(e) => onUrlChange && onUrlChange(e.target.value)}
|
||||
required
|
||||
variant="outlined"
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
InputProps={{
|
||||
sx: {
|
||||
".MuiOutlinedInput-notchedOutline": {
|
||||
borderRadius: 0,
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
<FormControl>
|
||||
<InputLabel id="req-proto-label">Protocol</InputLabel>
|
||||
<Select
|
||||
labelId="req-proto-label"
|
||||
id="req-proto"
|
||||
value={proto}
|
||||
label="Protocol"
|
||||
disabled={!onProtoChange}
|
||||
onChange={(e) => onProtoChange && onProtoChange(e.target.value as HttpProto)}
|
||||
sx={{
|
||||
".MuiOutlinedInput-notchedOutline": {
|
||||
borderLeftWidth: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderLeftWidth: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Object.values(HttpProto).map((proto) => (
|
||||
<MenuItem key={proto} value={proto}>
|
||||
{proto}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default UrlBar;
|
@ -67,6 +67,16 @@ export enum HttpProtocol {
|
||||
Http20 = 'HTTP20'
|
||||
}
|
||||
|
||||
export type HttpRequest = {
|
||||
__typename?: 'HttpRequest';
|
||||
body?: Maybe<Scalars['String']>;
|
||||
headers: Array<HttpHeader>;
|
||||
id: Scalars['ID'];
|
||||
method: HttpMethod;
|
||||
proto: HttpProtocol;
|
||||
url: Scalars['URL'];
|
||||
};
|
||||
|
||||
export type HttpRequestLog = {
|
||||
__typename?: 'HttpRequestLog';
|
||||
body?: Maybe<Scalars['String']>;
|
||||
@ -101,6 +111,20 @@ export type HttpResponseLog = {
|
||||
statusReason: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ModifyRequestInput = {
|
||||
body?: InputMaybe<Scalars['String']>;
|
||||
headers?: InputMaybe<Array<HttpHeaderInput>>;
|
||||
id: Scalars['ID'];
|
||||
method: HttpMethod;
|
||||
proto: HttpProtocol;
|
||||
url: Scalars['URL'];
|
||||
};
|
||||
|
||||
export type ModifyRequestResult = {
|
||||
__typename?: 'ModifyRequestResult';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
clearHTTPRequestLog: ClearHttpRequestLogResult;
|
||||
@ -110,6 +134,7 @@ export type Mutation = {
|
||||
createSenderRequestFromHttpRequestLog: SenderRequest;
|
||||
deleteProject: DeleteProjectResult;
|
||||
deleteSenderRequests: DeleteSenderRequestsResult;
|
||||
modifyRequest: ModifyRequestResult;
|
||||
openProject?: Maybe<Project>;
|
||||
sendRequest: SenderRequest;
|
||||
setHttpRequestLogFilter?: Maybe<HttpRequestLogFilter>;
|
||||
@ -138,6 +163,11 @@ export type MutationDeleteProjectArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationModifyRequestArgs = {
|
||||
request: ModifyRequestInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationOpenProjectArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
@ -175,6 +205,8 @@ export type Query = {
|
||||
httpRequestLog?: Maybe<HttpRequestLog>;
|
||||
httpRequestLogFilter?: Maybe<HttpRequestLogFilter>;
|
||||
httpRequestLogs: Array<HttpRequestLog>;
|
||||
interceptedRequest?: Maybe<HttpRequest>;
|
||||
interceptedRequests: Array<HttpRequest>;
|
||||
projects: Array<Project>;
|
||||
scope: Array<ScopeRule>;
|
||||
senderRequest?: Maybe<SenderRequest>;
|
||||
@ -187,6 +219,11 @@ export type QueryHttpRequestLogArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryInterceptedRequestArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type QuerySenderRequestArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
@ -248,6 +285,20 @@ export type SenderRequestInput = {
|
||||
url: Scalars['URL'];
|
||||
};
|
||||
|
||||
export type GetInterceptedRequestQueryVariables = Exact<{
|
||||
id: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetInterceptedRequestQuery = { __typename?: 'Query', interceptedRequest?: { __typename?: 'HttpRequest', id: string, url: any, method: HttpMethod, proto: HttpProtocol, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null };
|
||||
|
||||
export type ModifyRequestMutationVariables = Exact<{
|
||||
request: ModifyRequestInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type ModifyRequestMutation = { __typename?: 'Mutation', modifyRequest: { __typename?: 'ModifyRequestResult', success: boolean } };
|
||||
|
||||
export type CloseProjectMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
@ -353,7 +404,88 @@ export type GetSenderRequestsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
export type GetSenderRequestsQuery = { __typename?: 'Query', senderRequests: Array<{ __typename?: 'SenderRequest', id: string, url: any, method: HttpMethod, response?: { __typename?: 'HttpResponseLog', id: string, statusCode: number, statusReason: string } | null }> };
|
||||
|
||||
export type GetInterceptedRequestsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetInterceptedRequestsQuery = { __typename?: 'Query', interceptedRequests: Array<{ __typename?: 'HttpRequest', id: string, url: any, method: HttpMethod }> };
|
||||
|
||||
|
||||
export const GetInterceptedRequestDocument = gql`
|
||||
query GetInterceptedRequest($id: ID!) {
|
||||
interceptedRequest(id: $id) {
|
||||
id
|
||||
url
|
||||
method
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
body
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetInterceptedRequestQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetInterceptedRequestQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetInterceptedRequestQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetInterceptedRequestQuery({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetInterceptedRequestQuery(baseOptions: Apollo.QueryHookOptions<GetInterceptedRequestQuery, GetInterceptedRequestQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetInterceptedRequestQuery, GetInterceptedRequestQueryVariables>(GetInterceptedRequestDocument, options);
|
||||
}
|
||||
export function useGetInterceptedRequestLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetInterceptedRequestQuery, GetInterceptedRequestQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetInterceptedRequestQuery, GetInterceptedRequestQueryVariables>(GetInterceptedRequestDocument, options);
|
||||
}
|
||||
export type GetInterceptedRequestQueryHookResult = ReturnType<typeof useGetInterceptedRequestQuery>;
|
||||
export type GetInterceptedRequestLazyQueryHookResult = ReturnType<typeof useGetInterceptedRequestLazyQuery>;
|
||||
export type GetInterceptedRequestQueryResult = Apollo.QueryResult<GetInterceptedRequestQuery, GetInterceptedRequestQueryVariables>;
|
||||
export const ModifyRequestDocument = gql`
|
||||
mutation ModifyRequest($request: ModifyRequestInput!) {
|
||||
modifyRequest(request: $request) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type ModifyRequestMutationFn = Apollo.MutationFunction<ModifyRequestMutation, ModifyRequestMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useModifyRequestMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useModifyRequestMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useModifyRequestMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [modifyRequestMutation, { data, loading, error }] = useModifyRequestMutation({
|
||||
* variables: {
|
||||
* request: // value for 'request'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useModifyRequestMutation(baseOptions?: Apollo.MutationHookOptions<ModifyRequestMutation, ModifyRequestMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<ModifyRequestMutation, ModifyRequestMutationVariables>(ModifyRequestDocument, options);
|
||||
}
|
||||
export type ModifyRequestMutationHookResult = ReturnType<typeof useModifyRequestMutation>;
|
||||
export type ModifyRequestMutationResult = Apollo.MutationResult<ModifyRequestMutation>;
|
||||
export type ModifyRequestMutationOptions = Apollo.BaseMutationOptions<ModifyRequestMutation, ModifyRequestMutationVariables>;
|
||||
export const CloseProjectDocument = gql`
|
||||
mutation CloseProject {
|
||||
closeProject {
|
||||
@ -982,4 +1114,40 @@ export function useGetSenderRequestsLazyQuery(baseOptions?: Apollo.LazyQueryHook
|
||||
}
|
||||
export type GetSenderRequestsQueryHookResult = ReturnType<typeof useGetSenderRequestsQuery>;
|
||||
export type GetSenderRequestsLazyQueryHookResult = ReturnType<typeof useGetSenderRequestsLazyQuery>;
|
||||
export type GetSenderRequestsQueryResult = Apollo.QueryResult<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>;
|
||||
export type GetSenderRequestsQueryResult = Apollo.QueryResult<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>;
|
||||
export const GetInterceptedRequestsDocument = gql`
|
||||
query GetInterceptedRequests {
|
||||
interceptedRequests {
|
||||
id
|
||||
url
|
||||
method
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetInterceptedRequestsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetInterceptedRequestsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetInterceptedRequestsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetInterceptedRequestsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetInterceptedRequestsQuery(baseOptions?: Apollo.QueryHookOptions<GetInterceptedRequestsQuery, GetInterceptedRequestsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetInterceptedRequestsQuery, GetInterceptedRequestsQueryVariables>(GetInterceptedRequestsDocument, options);
|
||||
}
|
||||
export function useGetInterceptedRequestsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetInterceptedRequestsQuery, GetInterceptedRequestsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetInterceptedRequestsQuery, GetInterceptedRequestsQueryVariables>(GetInterceptedRequestsDocument, options);
|
||||
}
|
||||
export type GetInterceptedRequestsQueryHookResult = ReturnType<typeof useGetInterceptedRequestsQuery>;
|
||||
export type GetInterceptedRequestsLazyQueryHookResult = ReturnType<typeof useGetInterceptedRequestsLazyQuery>;
|
||||
export type GetInterceptedRequestsQueryResult = Apollo.QueryResult<GetInterceptedRequestsQuery, GetInterceptedRequestsQueryVariables>;
|
7
admin/src/lib/graphql/interceptedRequests.graphql
Normal file
7
admin/src/lib/graphql/interceptedRequests.graphql
Normal file
@ -0,0 +1,7 @@
|
||||
query GetInterceptedRequests {
|
||||
interceptedRequests {
|
||||
id
|
||||
url
|
||||
method
|
||||
}
|
||||
}
|
@ -8,7 +8,19 @@ function createApolloClient() {
|
||||
link: new HttpLink({
|
||||
uri: "/api/graphql/",
|
||||
}),
|
||||
cache: new InMemoryCache(),
|
||||
cache: new InMemoryCache({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
interceptedRequests: {
|
||||
merge(_, incoming) {
|
||||
return incoming;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
16
admin/src/lib/updateKeyPairItem.ts
Normal file
16
admin/src/lib/updateKeyPairItem.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { KeyValuePair } from "./components/KeyValuePair";
|
||||
|
||||
function updateKeyPairItem(key: string, value: string, idx: number, items: KeyValuePair[]): KeyValuePair[] {
|
||||
const updated = [...items];
|
||||
updated[idx] = { key, value };
|
||||
|
||||
// Append an empty key-value pair if the last item in the array isn't blank
|
||||
// anymore.
|
||||
if (items.length - 1 === idx && items[idx].key === "" && items[idx].value === "") {
|
||||
updated.push({ key: "", value: "" });
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
export default updateKeyPairItem;
|
28
admin/src/lib/updateURLQueryParams.ts
Normal file
28
admin/src/lib/updateURLQueryParams.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { KeyValuePair } from "./components/KeyValuePair";
|
||||
|
||||
function updateURLQueryParams(url: string, queryParams: KeyValuePair[]) {
|
||||
// Note: We don't use the `URL` interface, because we're potentially dealing
|
||||
// with malformed/incorrect URLs, which would yield TypeErrors when constructed
|
||||
// via `URL`.
|
||||
let newURL = url;
|
||||
|
||||
const questionMarkIndex = url.indexOf("?");
|
||||
if (questionMarkIndex !== -1) {
|
||||
newURL = newURL.slice(0, questionMarkIndex);
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const { key, value } of queryParams.filter(({ key }) => key !== "")) {
|
||||
searchParams.append(key, value);
|
||||
}
|
||||
|
||||
const rawQueryParams = decodeURI(searchParams.toString());
|
||||
|
||||
if (rawQueryParams == "") {
|
||||
return newURL;
|
||||
}
|
||||
|
||||
return newURL + "?" + rawQueryParams;
|
||||
}
|
||||
|
||||
export default updateURLQueryParams;
|
@ -7,6 +7,7 @@ import Head from "next/head";
|
||||
import React from "react";
|
||||
|
||||
import { ActiveProjectProvider } from "lib/ActiveProjectContext";
|
||||
import { InterceptedRequestsProvider } from "lib/InterceptedRequestsContext";
|
||||
import { useApollo } from "lib/graphql/useApollo";
|
||||
import createEmotionCache from "lib/mui/createEmotionCache";
|
||||
import theme from "lib/mui/theme";
|
||||
@ -32,10 +33,12 @@ export default function MyApp(props: MyAppProps) {
|
||||
</Head>
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<ActiveProjectProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
<InterceptedRequestsProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
</InterceptedRequestsProvider>
|
||||
</ActiveProjectProvider>
|
||||
</ApolloProvider>
|
||||
</CacheProvider>
|
||||
|
12
admin/src/pages/proxy/intercept/index.tsx
Normal file
12
admin/src/pages/proxy/intercept/index.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { Layout, Page } from "features/Layout";
|
||||
import Intercept from "features/intercept/components/Intercept";
|
||||
|
||||
function ProxyIntercept(): JSX.Element {
|
||||
return (
|
||||
<Layout page={Page.Intercept} title="Proxy intercept">
|
||||
<Intercept />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProxyIntercept;
|
@ -10,9 +10,9 @@
|
||||
"@jridgewell/trace-mapping" "^0.3.0"
|
||||
|
||||
"@apollo/client@^3.2.0":
|
||||
version "3.5.8"
|
||||
resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.5.8.tgz#7215b974c5988b6157530eb69369209210349fe0"
|
||||
integrity sha512-MAm05+I1ullr64VLpZwon/ISnkMuNLf6vDqgo9wiMhHYBGT4yOAbAIseRdjCHZwfSx/7AUuBgaTNOssZPIr6FQ==
|
||||
version "3.5.10"
|
||||
resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.5.10.tgz#43463108a6e07ae602cca0afc420805a19339a71"
|
||||
integrity sha512-tL3iSpFe9Oldq7gYikZK1dcYxp1c01nlSwtsMz75382HcI6fvQXyFXUCJTTK3wgO2/ckaBvRGw7VqjFREdVoRw==
|
||||
dependencies:
|
||||
"@graphql-typed-document-node/core" "^3.0.0"
|
||||
"@wry/context" "^0.6.0"
|
||||
|
@ -131,6 +131,7 @@ type ComplexityRoot struct {
|
||||
HTTPRequestLog func(childComplexity int, id ulid.ULID) int
|
||||
HTTPRequestLogFilter func(childComplexity int) int
|
||||
HTTPRequestLogs func(childComplexity int) int
|
||||
InterceptedRequest func(childComplexity int, id ulid.ULID) int
|
||||
InterceptedRequests func(childComplexity int) int
|
||||
Projects func(childComplexity int) int
|
||||
Scope func(childComplexity int) int
|
||||
@ -192,6 +193,7 @@ type QueryResolver interface {
|
||||
SenderRequest(ctx context.Context, id ulid.ULID) (*SenderRequest, error)
|
||||
SenderRequests(ctx context.Context) ([]SenderRequest, error)
|
||||
InterceptedRequests(ctx context.Context) ([]HTTPRequest, error)
|
||||
InterceptedRequest(ctx context.Context, id ulid.ULID) (*HTTPRequest, error)
|
||||
}
|
||||
|
||||
type executableSchema struct {
|
||||
@ -607,6 +609,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.Query.HTTPRequestLogs(childComplexity), true
|
||||
|
||||
case "Query.interceptedRequest":
|
||||
if e.complexity.Query.InterceptedRequest == nil {
|
||||
break
|
||||
}
|
||||
|
||||
args, err := ec.field_Query_interceptedRequest_args(context.TODO(), rawArgs)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return e.complexity.Query.InterceptedRequest(childComplexity, args["id"].(ulid.ULID)), true
|
||||
|
||||
case "Query.interceptedRequests":
|
||||
if e.complexity.Query.InterceptedRequests == nil {
|
||||
break
|
||||
@ -973,6 +987,7 @@ type Query {
|
||||
senderRequest(id: ID!): SenderRequest
|
||||
senderRequests: [SenderRequest!]!
|
||||
interceptedRequests: [HttpRequest!]!
|
||||
interceptedRequest(id: ID!): HttpRequest
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@ -1202,6 +1217,21 @@ func (ec *executionContext) field_Query_httpRequestLog_args(ctx context.Context,
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Query_interceptedRequest_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
args := map[string]interface{}{}
|
||||
var arg0 ulid.ULID
|
||||
if tmp, ok := rawArgs["id"]; ok {
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
|
||||
arg0, err = ec.unmarshalNID2githubᚗcomᚋoklogᚋulidᚐULID(ctx, tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
args["id"] = arg0
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Query_senderRequest_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
args := map[string]interface{}{}
|
||||
@ -3190,6 +3220,45 @@ func (ec *executionContext) _Query_interceptedRequests(ctx context.Context, fiel
|
||||
return ec.marshalNHttpRequest2ᚕgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPRequestᚄ(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Query_interceptedRequest(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "Query",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: true,
|
||||
IsResolver: true,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
rawArgs := field.ArgumentMap(ec.Variables)
|
||||
args, err := ec.field_Query_interceptedRequest_args(ctx, rawArgs)
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
fc.Args = args
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return ec.resolvers.Query().InterceptedRequest(rctx, args["id"].(ulid.ULID))
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*HTTPRequest)
|
||||
fc.Result = res
|
||||
return ec.marshalOHttpRequest2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPRequest(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -5805,6 +5874,17 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
|
||||
}
|
||||
return res
|
||||
})
|
||||
case "interceptedRequest":
|
||||
field := field
|
||||
out.Concurrently(i, func() (res graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
}
|
||||
}()
|
||||
res = ec._Query_interceptedRequest(ctx, field)
|
||||
return res
|
||||
})
|
||||
case "__type":
|
||||
out.Values[i] = ec._Query___type(ctx, field)
|
||||
case "__schema":
|
||||
@ -7117,6 +7197,13 @@ func (ec *executionContext) marshalOHttpProtocol2ᚖgithubᚗcomᚋdstotijnᚋhe
|
||||
return v
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalOHttpRequest2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPRequest(ctx context.Context, sel ast.SelectionSet, v *HTTPRequest) graphql.Marshaler {
|
||||
if v == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
return ec._HttpRequest(ctx, sel, v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalOHttpRequestLog2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPRequestLog(ctx context.Context, sel ast.SelectionSet, v *HTTPRequestLog) graphql.Marshaler {
|
||||
if v == nil {
|
||||
return graphql.Null
|
||||
|
@ -541,6 +541,22 @@ func (r *queryResolver) InterceptedRequests(ctx context.Context) ([]HTTPRequest,
|
||||
return httpReqs, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) InterceptedRequest(ctx context.Context, id ulid.ULID) (*HTTPRequest, error) {
|
||||
req, err := r.InterceptService.RequestByID(id)
|
||||
if errors.Is(err, intercept.ErrRequestNotFound) {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("could not get request by ID: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := parseHTTPRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &httpReq, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ModifyRequest(ctx context.Context, input ModifyRequestInput) (*ModifyRequestResult, error) {
|
||||
body := ""
|
||||
if input.Body != nil {
|
||||
|
@ -148,6 +148,7 @@ type Query {
|
||||
senderRequest(id: ID!): SenderRequest
|
||||
senderRequests: [SenderRequest!]!
|
||||
interceptedRequests: [HttpRequest!]!
|
||||
interceptedRequest(id: ID!): HttpRequest
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
|
@ -176,6 +176,19 @@ func (svc *Service) Requests() []*http.Request {
|
||||
return reqs
|
||||
}
|
||||
|
||||
// Request returns an intercepted request by ID. It's safe for concurrent use.
|
||||
func (svc *Service) RequestByID(id ulid.ULID) (*http.Request, error) {
|
||||
svc.mu.RLock()
|
||||
defer svc.mu.RUnlock()
|
||||
|
||||
req, ok := svc.requests[id]
|
||||
if !ok {
|
||||
return nil, ErrRequestNotFound
|
||||
}
|
||||
|
||||
return req.req, nil
|
||||
}
|
||||
|
||||
func (ids RequestIDs) Len() int {
|
||||
return len(ids)
|
||||
}
|
||||
|
Reference in New Issue
Block a user