mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Add intercept module
This commit is contained in:
@ -17,7 +17,12 @@
|
||||
"prettier/prettier": ["error"],
|
||||
"@next/next/no-css-tags": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
|
||||
"import/default": "off",
|
||||
|
||||
|
@ -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,15 +30,18 @@ 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,
|
||||
Sender,
|
||||
Scope,
|
||||
Settings,
|
||||
}
|
||||
|
||||
const drawerWidth = 240;
|
||||
@ -135,6 +140,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 +210,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>
|
||||
|
366
admin/src/features/intercept/components/EditRequest.tsx
Normal file
366
admin/src/features/intercept/components/EditRequest.tsx
Normal file
@ -0,0 +1,366 @@
|
||||
import CancelIcon from "@mui/icons-material/Cancel";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import SendIcon from "@mui/icons-material/Send";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import { Alert, Box, Button, CircularProgress, IconButton, Tooltip, 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 Link from "lib/components/Link";
|
||||
import RequestTabs from "lib/components/RequestTabs";
|
||||
import ResponseStatus from "lib/components/ResponseStatus";
|
||||
import ResponseTabs from "lib/components/ResponseTabs";
|
||||
import UrlBar, { HttpMethod, HttpProto, httpProtoMap } from "lib/components/UrlBar";
|
||||
import {
|
||||
HttpProtocol,
|
||||
HttpRequest,
|
||||
useCancelRequestMutation,
|
||||
useCancelResponseMutation,
|
||||
useGetInterceptedRequestQuery,
|
||||
useModifyRequestMutation,
|
||||
useModifyResponseMutation,
|
||||
} 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.
|
||||
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 [reqHeaders, setReqHeaders] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
||||
const [resHeaders, setResHeaders] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
||||
const [reqBody, setReqBody] = useState("");
|
||||
const [resBody, setResBody] = 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 handleReqHeaderChange = (key: string, value: string, idx: number) => {
|
||||
setReqHeaders((prev) => updateKeyPairItem(key, value, idx, prev));
|
||||
};
|
||||
const handleReqHeaderDelete = (idx: number) => {
|
||||
setReqHeaders((prev) => prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length)));
|
||||
};
|
||||
|
||||
const handleResHeaderChange = (key: string, value: string, idx: number) => {
|
||||
setResHeaders((prev) => updateKeyPairItem(key, value, idx, prev));
|
||||
};
|
||||
const handleResHeaderDelete = (idx: number) => {
|
||||
setResHeaders((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);
|
||||
setReqBody(interceptedRequest.body || "");
|
||||
|
||||
const newQueryParams = queryParamsFromURL(interceptedRequest.url);
|
||||
// Push empty row.
|
||||
newQueryParams.push({ key: "", value: "" });
|
||||
setQueryParams(newQueryParams);
|
||||
|
||||
const newReqHeaders = sortKeyValuePairs(interceptedRequest.headers || []);
|
||||
setReqHeaders([...newReqHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
||||
|
||||
setResBody(interceptedRequest.response?.body || "");
|
||||
const newResHeaders = sortKeyValuePairs(interceptedRequest.response?.headers || []);
|
||||
setResHeaders([...newResHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
||||
},
|
||||
});
|
||||
const interceptedReq =
|
||||
reqId && !getReqResult?.data?.interceptedRequest?.response ? getReqResult?.data?.interceptedRequest : undefined;
|
||||
const interceptedRes = reqId ? getReqResult?.data?.interceptedRequest?.response : undefined;
|
||||
|
||||
const [modifyRequest, modifyReqResult] = useModifyRequestMutation();
|
||||
const [cancelRequest, cancelReqResult] = useCancelRequestMutation();
|
||||
|
||||
const [modifyResponse, modifyResResult] = useModifyResponseMutation();
|
||||
const [cancelResponse, cancelResResult] = useCancelResponseMutation();
|
||||
|
||||
const onActionCompleted = () => {
|
||||
setURL("");
|
||||
setMethod(HttpMethod.Get);
|
||||
setReqBody("");
|
||||
setQueryParams([]);
|
||||
setReqHeaders([]);
|
||||
router.replace(`/proxy/intercept`);
|
||||
};
|
||||
|
||||
const handleFormSubmit: React.FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (interceptedReq) {
|
||||
modifyRequest({
|
||||
variables: {
|
||||
request: {
|
||||
id: interceptedReq.id,
|
||||
url,
|
||||
method,
|
||||
proto: httpProtoMap.get(proto) || HttpProtocol.Http20,
|
||||
headers: reqHeaders.filter((kv) => kv.key !== ""),
|
||||
body: reqBody || undefined,
|
||||
},
|
||||
},
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
interceptedRequests(existing: HttpRequest[], { readField }) {
|
||||
return existing.filter((ref) => interceptedReq.id !== readField("id", ref));
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
onCompleted: onActionCompleted,
|
||||
});
|
||||
}
|
||||
|
||||
if (interceptedRes) {
|
||||
modifyResponse({
|
||||
variables: {
|
||||
response: {
|
||||
requestID: interceptedRes.id,
|
||||
proto: interceptedRes.proto, // TODO: Allow modifying
|
||||
statusCode: interceptedRes.statusCode, // TODO: Allow modifying
|
||||
statusReason: interceptedRes.statusReason, // TODO: Allow modifying
|
||||
headers: resHeaders.filter((kv) => kv.key !== ""),
|
||||
body: resBody || undefined,
|
||||
},
|
||||
},
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
interceptedRequests(existing: HttpRequest[], { readField }) {
|
||||
return existing.filter((ref) => interceptedRes.id !== readField("id", ref));
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
onCompleted: onActionCompleted,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleReqCancelClick = () => {
|
||||
if (!interceptedReq) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelRequest({
|
||||
variables: {
|
||||
id: interceptedReq.id,
|
||||
},
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
interceptedRequests(existing: HttpRequest[], { readField }) {
|
||||
return existing.filter((ref) => interceptedReq.id !== readField("id", ref));
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
onCompleted: onActionCompleted,
|
||||
});
|
||||
};
|
||||
|
||||
const handleResCancelClick = () => {
|
||||
if (!interceptedRes) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelResponse({
|
||||
variables: {
|
||||
requestID: interceptedRes.id,
|
||||
},
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
interceptedRequests(existing: HttpRequest[], { readField }) {
|
||||
return existing.filter((ref) => interceptedRes.id !== readField("id", ref));
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
onCompleted: onActionCompleted,
|
||||
});
|
||||
};
|
||||
|
||||
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" }}
|
||||
/>
|
||||
{!interceptedRes && (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
disableElevation
|
||||
type="submit"
|
||||
disabled={!interceptedReq || modifyReqResult.loading || cancelReqResult.loading}
|
||||
startIcon={modifyReqResult.loading ? <CircularProgress size={22} /> : <SendIcon />}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
disableElevation
|
||||
onClick={handleReqCancelClick}
|
||||
disabled={!interceptedReq || modifyReqResult.loading || cancelReqResult.loading}
|
||||
startIcon={cancelReqResult.loading ? <CircularProgress size={22} /> : <CancelIcon />}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{interceptedRes && (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
disableElevation
|
||||
type="submit"
|
||||
disabled={modifyResResult.loading || cancelResResult.loading}
|
||||
endIcon={modifyResResult.loading ? <CircularProgress size={22} /> : <DownloadIcon />}
|
||||
>
|
||||
Receive
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
disableElevation
|
||||
onClick={handleResCancelClick}
|
||||
disabled={modifyResResult.loading || cancelResResult.loading}
|
||||
endIcon={cancelResResult.loading ? <CircularProgress size={22} /> : <CancelIcon />}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Tooltip title="Intercept settings">
|
||||
<IconButton LinkComponent={Link} href="/settings#intercept">
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
{modifyReqResult.error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{modifyReqResult.error.message}
|
||||
</Alert>
|
||||
)}
|
||||
{cancelReqResult.error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{cancelReqResult.error.message}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flex="1 auto" overflow="scroll">
|
||||
{interceptedReq && (
|
||||
<Box sx={{ height: "100%", pb: 2 }}>
|
||||
<Typography variant="overline" color="textSecondary" sx={{ position: "absolute", right: 0, mt: 1.2 }}>
|
||||
Request
|
||||
</Typography>
|
||||
<RequestTabs
|
||||
queryParams={interceptedReq ? queryParams : []}
|
||||
headers={interceptedReq ? reqHeaders : []}
|
||||
body={reqBody}
|
||||
onQueryParamChange={interceptedReq ? handleQueryParamChange : undefined}
|
||||
onQueryParamDelete={interceptedReq ? handleQueryParamDelete : undefined}
|
||||
onHeaderChange={interceptedReq ? handleReqHeaderChange : undefined}
|
||||
onHeaderDelete={interceptedReq ? handleReqHeaderDelete : undefined}
|
||||
onBodyChange={interceptedReq ? setReqBody : undefined}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{interceptedRes && (
|
||||
<Box sx={{ height: "100%", pb: 2 }}>
|
||||
<Box sx={{ position: "absolute", right: 0, mt: 1.4 }}>
|
||||
<Typography variant="overline" color="textSecondary" sx={{ float: "right", ml: 3 }}>
|
||||
Response
|
||||
</Typography>
|
||||
{interceptedRes && (
|
||||
<Box sx={{ float: "right", mt: 0.2 }}>
|
||||
<ResponseStatus
|
||||
proto={interceptedRes.proto}
|
||||
statusCode={interceptedRes.statusCode}
|
||||
statusReason={interceptedRes.statusReason}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<ResponseTabs
|
||||
headers={interceptedRes ? resHeaders : []}
|
||||
body={resBody}
|
||||
onHeaderChange={interceptedRes ? handleResHeaderChange : undefined}
|
||||
onHeaderDelete={interceptedRes ? handleResHeaderDelete : undefined}
|
||||
onBodyChange={interceptedRes ? setResBody : undefined}
|
||||
hasResponse={interceptedRes !== undefined && interceptedRes !== null}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</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,5 @@
|
||||
mutation CancelRequest($id: ID!) {
|
||||
cancelRequest(id: $id) {
|
||||
success
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
mutation CancelResponse($requestID: ID!) {
|
||||
cancelResponse(requestID: $requestID) {
|
||||
success
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
query GetInterceptedRequest($id: ID!) {
|
||||
interceptedRequest(id: $id) {
|
||||
id
|
||||
url
|
||||
method
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
body
|
||||
response {
|
||||
id
|
||||
proto
|
||||
statusCode
|
||||
statusReason
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
mutation ModifyRequest($request: ModifyRequestInput!) {
|
||||
modifyRequest(request: $request) {
|
||||
success
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
mutation ModifyResponse($response: ModifyResponseInput!) {
|
||||
modifyResponse(response: $response) {
|
||||
success
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import CloseIcon from "@mui/icons-material/Close";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import DescriptionIcon from "@mui/icons-material/Description";
|
||||
import LaunchIcon from "@mui/icons-material/Launch";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import { Alert } from "@mui/lab";
|
||||
import {
|
||||
Avatar,
|
||||
@ -29,6 +30,7 @@ import React, { useState } from "react";
|
||||
|
||||
import useOpenProjectMutation from "../hooks/useOpenProjectMutation";
|
||||
|
||||
import Link, { NextLinkComposed } from "lib/components/Link";
|
||||
import {
|
||||
ProjectsQuery,
|
||||
useCloseProjectMutation,
|
||||
@ -179,6 +181,11 @@ function ProjectList(): JSX.Element {
|
||||
{project.name} {project.isActive && <em>(Active)</em>}
|
||||
</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="Project settings">
|
||||
<IconButton LinkComponent={Link} href="/settings" disabled={!project.isActive}>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{project.isActive && (
|
||||
<Tooltip title="Close project">
|
||||
<IconButton onClick={() => closeProject()}>
|
||||
|
15
admin/src/features/projects/graphql/activeProject.graphql
Normal file
15
admin/src/features/projects/graphql/activeProject.graphql
Normal file
@ -0,0 +1,15 @@
|
||||
query ActiveProject {
|
||||
activeProject {
|
||||
id
|
||||
name
|
||||
isActive
|
||||
settings {
|
||||
intercept {
|
||||
requestsEnabled
|
||||
responsesEnabled
|
||||
requestFilter
|
||||
responseFilter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
61
admin/src/features/reqlog/components/Actions.tsx
Normal file
61
admin/src/features/reqlog/components/Actions.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
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 { useActiveProject } from "lib/ActiveProjectContext";
|
||||
import { useInterceptedRequests } from "lib/InterceptedRequestsContext";
|
||||
import { ConfirmationDialog, useConfirmationDialog } from "lib/components/ConfirmationDialog";
|
||||
import { HttpRequestLogsDocument, useClearHttpRequestLogMutation } from "lib/graphql/generated";
|
||||
|
||||
function Actions(): JSX.Element {
|
||||
const activeProject = useActiveProject();
|
||||
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>}
|
||||
|
||||
{(activeProject?.settings.intercept.requestsEnabled || activeProject?.settings.intercept.responsesEnabled) && (
|
||||
<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;
|
||||
|
294
admin/src/features/settings/components/Settings.tsx
Normal file
294
admin/src/features/settings/components/Settings.tsx
Normal file
@ -0,0 +1,294 @@
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { TabContext, TabPanel } from "@mui/lab";
|
||||
import TabList from "@mui/lab/TabList";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
FormHelperText,
|
||||
Snackbar,
|
||||
Switch,
|
||||
Tab,
|
||||
TextField,
|
||||
TextFieldProps,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { SwitchBaseProps } from "@mui/material/internal/SwitchBase";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useActiveProject } from "lib/ActiveProjectContext";
|
||||
import Link from "lib/components/Link";
|
||||
import { ActiveProjectDocument, useUpdateInterceptSettingsMutation } from "lib/graphql/generated";
|
||||
import { withoutTypename } from "lib/graphql/omitTypename";
|
||||
|
||||
enum TabValue {
|
||||
Intercept = "intercept",
|
||||
}
|
||||
|
||||
function FilterTextField(props: TextFieldProps): JSX.Element {
|
||||
return (
|
||||
<TextField
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
sx: { fontFamily: "'JetBrains Mono', monospace" },
|
||||
autoCorrect: "false",
|
||||
spellCheck: "false",
|
||||
}}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
margin="normal"
|
||||
sx={{ mr: 1 }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Settings(): JSX.Element {
|
||||
const client = useApolloClient();
|
||||
const activeProject = useActiveProject();
|
||||
const [updateInterceptSettings, updateIntercepSettingsResult] = useUpdateInterceptSettingsMutation({
|
||||
onCompleted(data) {
|
||||
client.cache.updateQuery({ query: ActiveProjectDocument }, (cachedData) => ({
|
||||
activeProject: {
|
||||
...cachedData.activeProject,
|
||||
settings: {
|
||||
...cachedData.activeProject.settings,
|
||||
intercept: data.updateInterceptSettings,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
setInterceptReqFilter(data.updateInterceptSettings.requestFilter || "");
|
||||
setInterceptResFilter(data.updateInterceptSettings.responseFilter || "");
|
||||
setSettingsUpdatedOpen(true);
|
||||
},
|
||||
});
|
||||
|
||||
const [interceptReqFilter, setInterceptReqFilter] = useState("");
|
||||
const [interceptResFilter, setInterceptResFilter] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setInterceptReqFilter(activeProject?.settings.intercept.requestFilter || "");
|
||||
}, [activeProject?.settings.intercept.requestFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setInterceptResFilter(activeProject?.settings.intercept.responseFilter || "");
|
||||
}, [activeProject?.settings.intercept.responseFilter]);
|
||||
|
||||
const handleReqInterceptEnabled: SwitchBaseProps["onChange"] = (e, checked) => {
|
||||
if (!activeProject) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
updateInterceptSettings({
|
||||
variables: {
|
||||
input: {
|
||||
...withoutTypename(activeProject.settings.intercept),
|
||||
requestsEnabled: checked,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleResInterceptEnabled: SwitchBaseProps["onChange"] = (e, checked) => {
|
||||
if (!activeProject) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
updateInterceptSettings({
|
||||
variables: {
|
||||
input: {
|
||||
...withoutTypename(activeProject.settings.intercept),
|
||||
responsesEnabled: checked,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleInterceptReqFilter = () => {
|
||||
if (!activeProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateInterceptSettings({
|
||||
variables: {
|
||||
input: {
|
||||
...withoutTypename(activeProject.settings.intercept),
|
||||
requestFilter: interceptReqFilter,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleInterceptResFilter = () => {
|
||||
if (!activeProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateInterceptSettings({
|
||||
variables: {
|
||||
input: {
|
||||
...withoutTypename(activeProject.settings.intercept),
|
||||
responseFilter: interceptResFilter,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const [tabValue, setTabValue] = useState(TabValue.Intercept);
|
||||
const [settingsUpdatedOpen, setSettingsUpdatedOpen] = useState(false);
|
||||
|
||||
const handleSettingsUpdatedClose = (_: Event | React.SyntheticEvent, reason?: string) => {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
|
||||
setSettingsUpdatedOpen(false);
|
||||
};
|
||||
|
||||
const tabSx = {
|
||||
textTransform: "none",
|
||||
};
|
||||
|
||||
return (
|
||||
<Box p={4}>
|
||||
<Snackbar open={settingsUpdatedOpen} autoHideDuration={3000} onClose={handleSettingsUpdatedClose}>
|
||||
<Alert onClose={handleSettingsUpdatedClose} severity="info">
|
||||
Intercept settings have been updated.
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
<Typography variant="h4" sx={{ mb: 2 }}>
|
||||
Settings
|
||||
</Typography>
|
||||
<Typography paragraph sx={{ mb: 4 }}>
|
||||
Settings allow you to tweak the behaviour of Hetty’s features.
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ mb: 2 }}>
|
||||
Project settings
|
||||
</Typography>
|
||||
{!activeProject && (
|
||||
<Typography paragraph>
|
||||
There is no project active. To configure project settings, first <Link href="/projects">open a project</Link>.
|
||||
</Typography>
|
||||
)}
|
||||
{activeProject && (
|
||||
<>
|
||||
<TabContext value={tabValue}>
|
||||
<TabList onChange={(_, value) => setTabValue(value)} sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||
<Tab value={TabValue.Intercept} label="Intercept" sx={tabSx} />
|
||||
</TabList>
|
||||
|
||||
<TabPanel value={TabValue.Intercept} sx={{ px: 0 }}>
|
||||
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}>
|
||||
Requests
|
||||
</Typography>
|
||||
<FormControl sx={{ mb: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
disabled={updateIntercepSettingsResult.loading}
|
||||
onChange={handleReqInterceptEnabled}
|
||||
checked={activeProject.settings.intercept.requestsEnabled}
|
||||
/>
|
||||
}
|
||||
label="Enable request interception"
|
||||
labelPlacement="start"
|
||||
sx={{ display: "inline-block", m: 0 }}
|
||||
/>
|
||||
<FormHelperText>
|
||||
When enabled, incoming HTTP requests to the proxy are stalled for{" "}
|
||||
<Link href="/proxy/intercept">manual review</Link>.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<form>
|
||||
<FormControl sx={{ width: "50%" }}>
|
||||
<FilterTextField
|
||||
label="Request filter"
|
||||
placeholder={`Example: method = "GET" OR url =~ "/foobar"`}
|
||||
value={interceptReqFilter}
|
||||
onChange={(e) => setInterceptReqFilter(e.target.value)}
|
||||
/>
|
||||
<FormHelperText>
|
||||
Filter expression to match incoming requests on. When set, only matching requests are intercepted.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="text"
|
||||
color="primary"
|
||||
size="large"
|
||||
sx={{
|
||||
mt: 2,
|
||||
py: 1.8,
|
||||
}}
|
||||
onClick={handleInterceptReqFilter}
|
||||
disabled={updateIntercepSettingsResult.loading}
|
||||
startIcon={updateIntercepSettingsResult.loading ? <CircularProgress size={22} /> : undefined}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</form>
|
||||
<Typography variant="h6" sx={{ mt: 3 }}>
|
||||
Responses
|
||||
</Typography>
|
||||
<FormControl sx={{ mb: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
disabled={updateIntercepSettingsResult.loading}
|
||||
onChange={handleResInterceptEnabled}
|
||||
checked={activeProject.settings.intercept.responsesEnabled}
|
||||
/>
|
||||
}
|
||||
label="Enable response interception"
|
||||
labelPlacement="start"
|
||||
sx={{ display: "inline-block", m: 0 }}
|
||||
/>
|
||||
<FormHelperText>
|
||||
When enabled, HTTP responses received by the proxy are stalled for{" "}
|
||||
<Link href="/proxy/intercept">manual review</Link>.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<form>
|
||||
<FormControl sx={{ width: "50%" }}>
|
||||
<FilterTextField
|
||||
label="Response filter"
|
||||
placeholder={`Example: statusCode =~ "^2" OR body =~ "foobar"`}
|
||||
value={interceptResFilter}
|
||||
onChange={(e) => setInterceptResFilter(e.target.value)}
|
||||
/>
|
||||
<FormHelperText>
|
||||
Filter expression to match received responses on. When set, only matching responses are intercepted.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="text"
|
||||
color="primary"
|
||||
size="large"
|
||||
sx={{
|
||||
mt: 2,
|
||||
py: 1.8,
|
||||
}}
|
||||
onClick={handleInterceptResFilter}
|
||||
disabled={updateIntercepSettingsResult.loading}
|
||||
startIcon={updateIntercepSettingsResult.loading ? <CircularProgress size={22} /> : undefined}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</form>
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) {
|
||||
updateInterceptSettings(input: $input) {
|
||||
requestsEnabled
|
||||
responsesEnabled
|
||||
requestFilter
|
||||
responseFilter
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import React, { createContext, useContext } from "react";
|
||||
|
||||
import { Project, useProjectsQuery } from "./graphql/generated";
|
||||
import { Project, useActiveProjectQuery } from "./graphql/generated";
|
||||
|
||||
const ActiveProjectContext = createContext<Project | null>(null);
|
||||
|
||||
@ -9,8 +9,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export function ActiveProjectProvider({ children }: Props): JSX.Element {
|
||||
const { data } = useProjectsQuery();
|
||||
const project = data?.projects.find((project) => project.isActive) || null;
|
||||
const { data } = useActiveProjectQuery();
|
||||
const project = data?.activeProject || null;
|
||||
|
||||
return <ActiveProjectContext.Provider value={project}>{children}</ActiveProjectContext.Provider>;
|
||||
}
|
||||
|
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);
|
||||
}
|
94
admin/src/lib/components/Link.tsx
Normal file
94
admin/src/lib/components/Link.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import MuiLink, { LinkProps as MuiLinkProps } from "@mui/material/Link";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import clsx from "clsx";
|
||||
import NextLink, { LinkProps as NextLinkProps } from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import * as React from "react";
|
||||
|
||||
// Add support for the sx prop for consistency with the other branches.
|
||||
const Anchor = styled("a")({});
|
||||
|
||||
interface NextLinkComposedProps
|
||||
extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href">,
|
||||
Omit<NextLinkProps, "href" | "as"> {
|
||||
to: NextLinkProps["href"];
|
||||
linkAs?: NextLinkProps["as"];
|
||||
}
|
||||
|
||||
export const NextLinkComposed = React.forwardRef<HTMLAnchorElement, NextLinkComposedProps>(function NextLinkComposed(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
const { to, linkAs, replace, scroll, shallow, prefetch, locale, ...other } = props;
|
||||
|
||||
return (
|
||||
<NextLink
|
||||
href={to}
|
||||
prefetch={prefetch}
|
||||
as={linkAs}
|
||||
replace={replace}
|
||||
scroll={scroll}
|
||||
shallow={shallow}
|
||||
passHref
|
||||
locale={locale}
|
||||
>
|
||||
<Anchor ref={ref} {...other} />
|
||||
</NextLink>
|
||||
);
|
||||
});
|
||||
|
||||
export type LinkProps = {
|
||||
activeClassName?: string;
|
||||
as?: NextLinkProps["as"];
|
||||
href: NextLinkProps["href"];
|
||||
linkAs?: NextLinkProps["as"]; // Useful when the as prop is shallow by styled().
|
||||
noLinkStyle?: boolean;
|
||||
} & Omit<NextLinkComposedProps, "to" | "linkAs" | "href"> &
|
||||
Omit<MuiLinkProps, "href">;
|
||||
|
||||
// A styled version of the Next.js Link component:
|
||||
// https://nextjs.org/docs/api-reference/next/link
|
||||
const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(function Link(props, ref) {
|
||||
const {
|
||||
activeClassName = "active",
|
||||
as,
|
||||
className: classNameProps,
|
||||
href,
|
||||
linkAs: linkAsProp,
|
||||
locale,
|
||||
noLinkStyle,
|
||||
prefetch,
|
||||
replace,
|
||||
role, // Link don't have roles.
|
||||
scroll,
|
||||
shallow,
|
||||
...other
|
||||
} = props;
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = typeof href === "string" ? href : href.pathname;
|
||||
const className = clsx(classNameProps, {
|
||||
[activeClassName]: router.pathname === pathname && activeClassName,
|
||||
});
|
||||
|
||||
const isExternal = typeof href === "string" && (href.indexOf("http") === 0 || href.indexOf("mailto:") === 0);
|
||||
|
||||
if (isExternal) {
|
||||
if (noLinkStyle) {
|
||||
return <Anchor className={className} href={href} ref={ref} {...other} />;
|
||||
}
|
||||
|
||||
return <MuiLink className={className} href={href} ref={ref} {...other} />;
|
||||
}
|
||||
|
||||
const linkAs = linkAsProp || as;
|
||||
const nextjsProps = { to: href, linkAs, replace, scroll, shallow, prefetch, locale };
|
||||
|
||||
if (noLinkStyle) {
|
||||
return <NextLinkComposed className={className} ref={ref} {...nextjsProps} {...other} />;
|
||||
}
|
||||
|
||||
return <MuiLink component={NextLinkComposed} className={className} ref={ref} {...nextjsProps} {...other} />;
|
||||
});
|
||||
|
||||
export default Link;
|
@ -2,13 +2,16 @@ import { TabContext, TabList, TabPanel } from "@mui/lab";
|
||||
import { Box, Paper, Tab, Typography } from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { KeyValuePairTable, KeyValuePair, KeyValuePairTableProps } from "./KeyValuePair";
|
||||
|
||||
import Editor from "lib/components/Editor";
|
||||
import { KeyValuePairTable } from "lib/components/KeyValuePair";
|
||||
import { HttpResponseLog } from "lib/graphql/generated";
|
||||
|
||||
interface ResponseTabsProps {
|
||||
headers: HttpResponseLog["headers"];
|
||||
body: HttpResponseLog["body"];
|
||||
headers: KeyValuePair[];
|
||||
onHeaderChange?: KeyValuePairTableProps["onChange"];
|
||||
onHeaderDelete?: KeyValuePairTableProps["onDelete"];
|
||||
body?: string | null;
|
||||
onBodyChange?: (value: string) => void;
|
||||
hasResponse: boolean;
|
||||
}
|
||||
|
||||
@ -24,7 +27,7 @@ const reqNotSent = (
|
||||
);
|
||||
|
||||
function ResponseTabs(props: ResponseTabsProps): JSX.Element {
|
||||
const { headers, body, hasResponse } = props;
|
||||
const { headers, onHeaderChange, onHeaderDelete, body, onBodyChange, hasResponse } = props;
|
||||
const [tabValue, setTabValue] = useState(TabValue.Body);
|
||||
|
||||
const contentType = headers.find((header) => header.key.toLowerCase() === "content-type")?.value;
|
||||
@ -33,6 +36,8 @@ function ResponseTabs(props: ResponseTabsProps): JSX.Element {
|
||||
textTransform: "none",
|
||||
};
|
||||
|
||||
const headersLength = onHeaderChange ? headers.length - 1 : headers.length;
|
||||
|
||||
return (
|
||||
<Box height="100%" sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<TabContext value={tabValue}>
|
||||
@ -43,20 +48,25 @@ function ResponseTabs(props: ResponseTabsProps): JSX.Element {
|
||||
label={"Body" + (body?.length ? ` (${body.length} byte` + (body.length > 1 ? "s" : "") + ")" : "")}
|
||||
sx={tabSx}
|
||||
/>
|
||||
<Tab
|
||||
value={TabValue.Headers}
|
||||
label={"Headers" + (headers.length ? ` (${headers.length})` : "")}
|
||||
sx={tabSx}
|
||||
/>
|
||||
<Tab value={TabValue.Headers} label={"Headers" + (headersLength ? ` (${headersLength})` : "")} sx={tabSx} />
|
||||
</TabList>
|
||||
</Box>
|
||||
<Box flex="1 auto" overflow="hidden">
|
||||
<TabPanel value={TabValue.Body} sx={{ p: 0, height: "100%" }}>
|
||||
{body && <Editor content={body} contentType={contentType} />}
|
||||
{hasResponse && (
|
||||
<Editor
|
||||
content={body || ""}
|
||||
onChange={(value) => {
|
||||
onBodyChange && onBodyChange(value || "");
|
||||
}}
|
||||
monacoOptions={{ readOnly: onBodyChange === undefined }}
|
||||
contentType={contentType}
|
||||
/>
|
||||
)}
|
||||
{!hasResponse && reqNotSent}
|
||||
</TabPanel>
|
||||
<TabPanel value={TabValue.Headers} sx={{ p: 0, height: "100%", overflow: "scroll" }}>
|
||||
{headers.length > 0 && <KeyValuePairTable items={headers} />}
|
||||
{hasResponse && <KeyValuePairTable items={headers} onChange={onHeaderChange} onDelete={onHeaderDelete} />}
|
||||
{!hasResponse && reqNotSent}
|
||||
</TabPanel>
|
||||
</Box>
|
||||
|
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;
|
@ -18,6 +18,16 @@ export type Scalars = {
|
||||
URL: any;
|
||||
};
|
||||
|
||||
export type CancelRequestResult = {
|
||||
__typename?: 'CancelRequestResult';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type CancelResponseResult = {
|
||||
__typename?: 'CancelResponseResult';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type ClearHttpRequestLogResult = {
|
||||
__typename?: 'ClearHTTPRequestLogResult';
|
||||
success: Scalars['Boolean'];
|
||||
@ -67,6 +77,17 @@ export enum HttpProtocol {
|
||||
Http20 = 'HTTP20'
|
||||
}
|
||||
|
||||
export type HttpRequest = {
|
||||
__typename?: 'HttpRequest';
|
||||
body?: Maybe<Scalars['String']>;
|
||||
headers: Array<HttpHeader>;
|
||||
id: Scalars['ID'];
|
||||
method: HttpMethod;
|
||||
proto: HttpProtocol;
|
||||
response?: Maybe<HttpResponse>;
|
||||
url: Scalars['URL'];
|
||||
};
|
||||
|
||||
export type HttpRequestLog = {
|
||||
__typename?: 'HttpRequestLog';
|
||||
body?: Maybe<Scalars['String']>;
|
||||
@ -90,6 +111,17 @@ export type HttpRequestLogFilterInput = {
|
||||
searchExpression?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type HttpResponse = {
|
||||
__typename?: 'HttpResponse';
|
||||
body?: Maybe<Scalars['String']>;
|
||||
headers: Array<HttpHeader>;
|
||||
/** Will be the same ID as its related request ID. */
|
||||
id: Scalars['ID'];
|
||||
proto: HttpProtocol;
|
||||
statusCode: Scalars['Int'];
|
||||
statusReason: Scalars['String'];
|
||||
};
|
||||
|
||||
export type HttpResponseLog = {
|
||||
__typename?: 'HttpResponseLog';
|
||||
body?: Maybe<Scalars['String']>;
|
||||
@ -101,8 +133,47 @@ export type HttpResponseLog = {
|
||||
statusReason: Scalars['String'];
|
||||
};
|
||||
|
||||
export type InterceptSettings = {
|
||||
__typename?: 'InterceptSettings';
|
||||
requestFilter?: Maybe<Scalars['String']>;
|
||||
requestsEnabled: Scalars['Boolean'];
|
||||
responseFilter?: Maybe<Scalars['String']>;
|
||||
responsesEnabled: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type ModifyRequestInput = {
|
||||
body?: InputMaybe<Scalars['String']>;
|
||||
headers?: InputMaybe<Array<HttpHeaderInput>>;
|
||||
id: Scalars['ID'];
|
||||
method: HttpMethod;
|
||||
modifyResponse?: InputMaybe<Scalars['Boolean']>;
|
||||
proto: HttpProtocol;
|
||||
url: Scalars['URL'];
|
||||
};
|
||||
|
||||
export type ModifyRequestResult = {
|
||||
__typename?: 'ModifyRequestResult';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type ModifyResponseInput = {
|
||||
body?: InputMaybe<Scalars['String']>;
|
||||
headers?: InputMaybe<Array<HttpHeaderInput>>;
|
||||
proto: HttpProtocol;
|
||||
requestID: Scalars['ID'];
|
||||
statusCode: Scalars['Int'];
|
||||
statusReason: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ModifyResponseResult = {
|
||||
__typename?: 'ModifyResponseResult';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
cancelRequest: CancelRequestResult;
|
||||
cancelResponse: CancelResponseResult;
|
||||
clearHTTPRequestLog: ClearHttpRequestLogResult;
|
||||
closeProject: CloseProjectResult;
|
||||
createOrUpdateSenderRequest: SenderRequest;
|
||||
@ -110,11 +181,24 @@ export type Mutation = {
|
||||
createSenderRequestFromHttpRequestLog: SenderRequest;
|
||||
deleteProject: DeleteProjectResult;
|
||||
deleteSenderRequests: DeleteSenderRequestsResult;
|
||||
modifyRequest: ModifyRequestResult;
|
||||
modifyResponse: ModifyResponseResult;
|
||||
openProject?: Maybe<Project>;
|
||||
sendRequest: SenderRequest;
|
||||
setHttpRequestLogFilter?: Maybe<HttpRequestLogFilter>;
|
||||
setScope: Array<ScopeRule>;
|
||||
setSenderRequestFilter?: Maybe<SenderRequestFilter>;
|
||||
updateInterceptSettings: InterceptSettings;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCancelRequestArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationCancelResponseArgs = {
|
||||
requestID: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
@ -138,6 +222,16 @@ export type MutationDeleteProjectArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationModifyRequestArgs = {
|
||||
request: ModifyRequestInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationModifyResponseArgs = {
|
||||
response: ModifyResponseInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationOpenProjectArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
@ -162,11 +256,22 @@ export type MutationSetSenderRequestFilterArgs = {
|
||||
filter?: InputMaybe<SenderRequestFilterInput>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateInterceptSettingsArgs = {
|
||||
input: UpdateInterceptSettingsInput;
|
||||
};
|
||||
|
||||
export type Project = {
|
||||
__typename?: 'Project';
|
||||
id: Scalars['ID'];
|
||||
isActive: Scalars['Boolean'];
|
||||
name: Scalars['String'];
|
||||
settings: ProjectSettings;
|
||||
};
|
||||
|
||||
export type ProjectSettings = {
|
||||
__typename?: 'ProjectSettings';
|
||||
intercept: InterceptSettings;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
@ -175,6 +280,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 +294,11 @@ export type QueryHttpRequestLogArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryInterceptedRequestArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type QuerySenderRequestArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
@ -248,6 +360,53 @@ export type SenderRequestInput = {
|
||||
url: Scalars['URL'];
|
||||
};
|
||||
|
||||
export type UpdateInterceptSettingsInput = {
|
||||
requestFilter?: InputMaybe<Scalars['String']>;
|
||||
requestsEnabled: Scalars['Boolean'];
|
||||
responseFilter?: InputMaybe<Scalars['String']>;
|
||||
responsesEnabled: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type CancelRequestMutationVariables = Exact<{
|
||||
id: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CancelRequestMutation = { __typename?: 'Mutation', cancelRequest: { __typename?: 'CancelRequestResult', success: boolean } };
|
||||
|
||||
export type CancelResponseMutationVariables = Exact<{
|
||||
requestID: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CancelResponseMutation = { __typename?: 'Mutation', cancelResponse: { __typename?: 'CancelResponseResult', success: boolean } };
|
||||
|
||||
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 }>, response?: { __typename?: 'HttpResponse', id: string, proto: HttpProtocol, statusCode: number, statusReason: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null } | null };
|
||||
|
||||
export type ModifyRequestMutationVariables = Exact<{
|
||||
request: ModifyRequestInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type ModifyRequestMutation = { __typename?: 'Mutation', modifyRequest: { __typename?: 'ModifyRequestResult', success: boolean } };
|
||||
|
||||
export type ModifyResponseMutationVariables = Exact<{
|
||||
response: ModifyResponseInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type ModifyResponseMutation = { __typename?: 'Mutation', modifyResponse: { __typename?: 'ModifyResponseResult', success: boolean } };
|
||||
|
||||
export type ActiveProjectQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ActiveProjectQuery = { __typename?: 'Query', activeProject?: { __typename?: 'Project', id: string, name: string, isActive: boolean, settings: { __typename?: 'ProjectSettings', intercept: { __typename?: 'InterceptSettings', requestsEnabled: boolean, responsesEnabled: boolean, requestFilter?: string | null, responseFilter?: string | null } } } | null };
|
||||
|
||||
export type CloseProjectMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
@ -353,7 +512,249 @@ 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 UpdateInterceptSettingsMutationVariables = Exact<{
|
||||
input: UpdateInterceptSettingsInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateInterceptSettingsMutation = { __typename?: 'Mutation', updateInterceptSettings: { __typename?: 'InterceptSettings', requestsEnabled: boolean, responsesEnabled: boolean, requestFilter?: string | null, responseFilter?: string | null } };
|
||||
|
||||
export type GetInterceptedRequestsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetInterceptedRequestsQuery = { __typename?: 'Query', interceptedRequests: Array<{ __typename?: 'HttpRequest', id: string, url: any, method: HttpMethod, response?: { __typename?: 'HttpResponse', statusCode: number, statusReason: string } | null }> };
|
||||
|
||||
|
||||
export const CancelRequestDocument = gql`
|
||||
mutation CancelRequest($id: ID!) {
|
||||
cancelRequest(id: $id) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type CancelRequestMutationFn = Apollo.MutationFunction<CancelRequestMutation, CancelRequestMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useCancelRequestMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useCancelRequestMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCancelRequestMutation` 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 [cancelRequestMutation, { data, loading, error }] = useCancelRequestMutation({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCancelRequestMutation(baseOptions?: Apollo.MutationHookOptions<CancelRequestMutation, CancelRequestMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CancelRequestMutation, CancelRequestMutationVariables>(CancelRequestDocument, options);
|
||||
}
|
||||
export type CancelRequestMutationHookResult = ReturnType<typeof useCancelRequestMutation>;
|
||||
export type CancelRequestMutationResult = Apollo.MutationResult<CancelRequestMutation>;
|
||||
export type CancelRequestMutationOptions = Apollo.BaseMutationOptions<CancelRequestMutation, CancelRequestMutationVariables>;
|
||||
export const CancelResponseDocument = gql`
|
||||
mutation CancelResponse($requestID: ID!) {
|
||||
cancelResponse(requestID: $requestID) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type CancelResponseMutationFn = Apollo.MutationFunction<CancelResponseMutation, CancelResponseMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useCancelResponseMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useCancelResponseMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCancelResponseMutation` 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 [cancelResponseMutation, { data, loading, error }] = useCancelResponseMutation({
|
||||
* variables: {
|
||||
* requestID: // value for 'requestID'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCancelResponseMutation(baseOptions?: Apollo.MutationHookOptions<CancelResponseMutation, CancelResponseMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CancelResponseMutation, CancelResponseMutationVariables>(CancelResponseDocument, options);
|
||||
}
|
||||
export type CancelResponseMutationHookResult = ReturnType<typeof useCancelResponseMutation>;
|
||||
export type CancelResponseMutationResult = Apollo.MutationResult<CancelResponseMutation>;
|
||||
export type CancelResponseMutationOptions = Apollo.BaseMutationOptions<CancelResponseMutation, CancelResponseMutationVariables>;
|
||||
export const GetInterceptedRequestDocument = gql`
|
||||
query GetInterceptedRequest($id: ID!) {
|
||||
interceptedRequest(id: $id) {
|
||||
id
|
||||
url
|
||||
method
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
body
|
||||
response {
|
||||
id
|
||||
proto
|
||||
statusCode
|
||||
statusReason
|
||||
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 ModifyResponseDocument = gql`
|
||||
mutation ModifyResponse($response: ModifyResponseInput!) {
|
||||
modifyResponse(response: $response) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type ModifyResponseMutationFn = Apollo.MutationFunction<ModifyResponseMutation, ModifyResponseMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useModifyResponseMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useModifyResponseMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useModifyResponseMutation` 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 [modifyResponseMutation, { data, loading, error }] = useModifyResponseMutation({
|
||||
* variables: {
|
||||
* response: // value for 'response'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useModifyResponseMutation(baseOptions?: Apollo.MutationHookOptions<ModifyResponseMutation, ModifyResponseMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<ModifyResponseMutation, ModifyResponseMutationVariables>(ModifyResponseDocument, options);
|
||||
}
|
||||
export type ModifyResponseMutationHookResult = ReturnType<typeof useModifyResponseMutation>;
|
||||
export type ModifyResponseMutationResult = Apollo.MutationResult<ModifyResponseMutation>;
|
||||
export type ModifyResponseMutationOptions = Apollo.BaseMutationOptions<ModifyResponseMutation, ModifyResponseMutationVariables>;
|
||||
export const ActiveProjectDocument = gql`
|
||||
query ActiveProject {
|
||||
activeProject {
|
||||
id
|
||||
name
|
||||
isActive
|
||||
settings {
|
||||
intercept {
|
||||
requestsEnabled
|
||||
responsesEnabled
|
||||
requestFilter
|
||||
responseFilter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useActiveProjectQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useActiveProjectQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useActiveProjectQuery` 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 } = useActiveProjectQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useActiveProjectQuery(baseOptions?: Apollo.QueryHookOptions<ActiveProjectQuery, ActiveProjectQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ActiveProjectQuery, ActiveProjectQueryVariables>(ActiveProjectDocument, options);
|
||||
}
|
||||
export function useActiveProjectLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ActiveProjectQuery, ActiveProjectQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ActiveProjectQuery, ActiveProjectQueryVariables>(ActiveProjectDocument, options);
|
||||
}
|
||||
export type ActiveProjectQueryHookResult = ReturnType<typeof useActiveProjectQuery>;
|
||||
export type ActiveProjectLazyQueryHookResult = ReturnType<typeof useActiveProjectLazyQuery>;
|
||||
export type ActiveProjectQueryResult = Apollo.QueryResult<ActiveProjectQuery, ActiveProjectQueryVariables>;
|
||||
export const CloseProjectDocument = gql`
|
||||
mutation CloseProject {
|
||||
closeProject {
|
||||
@ -982,4 +1383,80 @@ 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 UpdateInterceptSettingsDocument = gql`
|
||||
mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) {
|
||||
updateInterceptSettings(input: $input) {
|
||||
requestsEnabled
|
||||
responsesEnabled
|
||||
requestFilter
|
||||
responseFilter
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type UpdateInterceptSettingsMutationFn = Apollo.MutationFunction<UpdateInterceptSettingsMutation, UpdateInterceptSettingsMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useUpdateInterceptSettingsMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useUpdateInterceptSettingsMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useUpdateInterceptSettingsMutation` 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 [updateInterceptSettingsMutation, { data, loading, error }] = useUpdateInterceptSettingsMutation({
|
||||
* variables: {
|
||||
* input: // value for 'input'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useUpdateInterceptSettingsMutation(baseOptions?: Apollo.MutationHookOptions<UpdateInterceptSettingsMutation, UpdateInterceptSettingsMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<UpdateInterceptSettingsMutation, UpdateInterceptSettingsMutationVariables>(UpdateInterceptSettingsDocument, options);
|
||||
}
|
||||
export type UpdateInterceptSettingsMutationHookResult = ReturnType<typeof useUpdateInterceptSettingsMutation>;
|
||||
export type UpdateInterceptSettingsMutationResult = Apollo.MutationResult<UpdateInterceptSettingsMutation>;
|
||||
export type UpdateInterceptSettingsMutationOptions = Apollo.BaseMutationOptions<UpdateInterceptSettingsMutation, UpdateInterceptSettingsMutationVariables>;
|
||||
export const GetInterceptedRequestsDocument = gql`
|
||||
query GetInterceptedRequests {
|
||||
interceptedRequests {
|
||||
id
|
||||
url
|
||||
method
|
||||
response {
|
||||
statusCode
|
||||
statusReason
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __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>;
|
11
admin/src/lib/graphql/interceptedRequests.graphql
Normal file
11
admin/src/lib/graphql/interceptedRequests.graphql
Normal file
@ -0,0 +1,11 @@
|
||||
query GetInterceptedRequests {
|
||||
interceptedRequests {
|
||||
id
|
||||
url
|
||||
method
|
||||
response {
|
||||
statusCode
|
||||
statusReason
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,22 @@ function createApolloClient() {
|
||||
link: new HttpLink({
|
||||
uri: "/api/graphql/",
|
||||
}),
|
||||
cache: new InMemoryCache(),
|
||||
cache: new InMemoryCache({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
interceptedRequests: {
|
||||
merge(_, incoming) {
|
||||
return incoming;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ProjectSettings: {
|
||||
merge: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
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;
|
12
admin/src/pages/settings/index.tsx
Normal file
12
admin/src/pages/settings/index.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { Layout, Page } from "features/Layout";
|
||||
import Settings from "features/settings/components/Settings";
|
||||
|
||||
function Index(): JSX.Element {
|
||||
return (
|
||||
<Layout page={Page.Settings} title="Settings">
|
||||
<Settings />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default Index;
|
@ -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"
|
||||
|
Reference in New Issue
Block a user