Add initial UI/UX for intecepting requests

This commit is contained in:
David Stotijn
2022-03-10 20:30:40 +01:00
parent 71e550f0cd
commit 9dd8464af7
24 changed files with 882 additions and 203 deletions

View File

@ -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>

View 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;

View 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>
);
}

View 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;

View File

@ -0,0 +1,13 @@
query GetInterceptedRequest($id: ID!) {
interceptedRequest(id: $id) {
id
url
method
proto
headers {
key
value
}
body
}
}

View File

@ -0,0 +1,5 @@
mutation ModifyRequest($request: ModifyRequestInput!) {
modifyRequest(request: $request) {
success
}
}

View 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;

View File

@ -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 }}>

View File

@ -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>
);
}

View File

@ -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;

View 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);
}

View 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;

View File

@ -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>;

View File

@ -0,0 +1,7 @@
query GetInterceptedRequests {
interceptedRequests {
id
url
method
}
}

View File

@ -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;
},
},
},
},
},
}),
});
}

View 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;

View 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;

View File

@ -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>

View 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;

View File

@ -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"