mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Tidy up admin
structure
This commit is contained in:
399
admin/src/features/sender/components/EditRequest.tsx
Normal file
399
admin/src/features/sender/components/EditRequest.tsx
Normal file
@ -0,0 +1,399 @@
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
BoxProps,
|
||||
Button,
|
||||
InputLabel,
|
||||
FormControl,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { AllotmentProps, PaneProps } from "allotment/dist/types/src/allotment";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
import EditRequestTabs from "./EditRequestTabs";
|
||||
import { KeyValuePair, sortKeyValuePairs } from "./KeyValuePair";
|
||||
import Response from "./Response";
|
||||
|
||||
import {
|
||||
GetSenderRequestQuery,
|
||||
useCreateOrUpdateSenderRequestMutation,
|
||||
HttpProtocol,
|
||||
useGetSenderRequestQuery,
|
||||
useSendRequestMutation,
|
||||
} from "lib/graphql/generated";
|
||||
|
||||
import "allotment/dist/style.css";
|
||||
|
||||
enum HttpMethod {
|
||||
Get = "GET",
|
||||
Post = "POST",
|
||||
Put = "PUT",
|
||||
Patch = "PATCH",
|
||||
Delete = "DELETE",
|
||||
Head = "HEAD",
|
||||
Options = "OPTIONS",
|
||||
Connect = "CONNECT",
|
||||
Trace = "TRACE",
|
||||
}
|
||||
|
||||
enum HttpProto {
|
||||
Http1 = "HTTP/1.1",
|
||||
Http2 = "HTTP/2.0",
|
||||
}
|
||||
|
||||
const httpProtoMap = new Map([
|
||||
[HttpProto.Http1, HttpProtocol.Http1],
|
||||
[HttpProto.Http2, HttpProtocol.Http2],
|
||||
]);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function queryParamsFromURL(url: string): KeyValuePair[] {
|
||||
const questionMarkIndex = url.indexOf("?");
|
||||
if (questionMarkIndex === -1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const queryParams: KeyValuePair[] = [];
|
||||
|
||||
const searchParams = new URLSearchParams(url.slice(questionMarkIndex + 1));
|
||||
for (const [key, value] of searchParams) {
|
||||
queryParams.push({ key, value });
|
||||
}
|
||||
|
||||
return queryParams;
|
||||
}
|
||||
|
||||
function EditRequest(): JSX.Element {
|
||||
const router = useRouter();
|
||||
const reqId = router.query.id as string | undefined;
|
||||
|
||||
const [method, setMethod] = useState(HttpMethod.Get);
|
||||
const [url, setURL] = useState("");
|
||||
const [proto, setProto] = useState(HttpProto.Http2);
|
||||
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 [response, setResponse] = useState<NonNullable<GetSenderRequestQuery["senderRequest"]>["response"]>(null);
|
||||
const getReqResult = useGetSenderRequestQuery({
|
||||
variables: { id: reqId as string },
|
||||
skip: reqId === undefined,
|
||||
onCompleted: ({ senderRequest }) => {
|
||||
if (!senderRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
setURL(senderRequest.url);
|
||||
setMethod(senderRequest.method);
|
||||
setBody(senderRequest.body || "");
|
||||
|
||||
const newQueryParams = queryParamsFromURL(senderRequest.url);
|
||||
// Push empty row.
|
||||
newQueryParams.push({ key: "", value: "" });
|
||||
setQueryParams(newQueryParams);
|
||||
|
||||
const newHeaders = sortKeyValuePairs(senderRequest.headers || []);
|
||||
setHeaders([...newHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
||||
console.log(senderRequest.response);
|
||||
setResponse(senderRequest.response);
|
||||
},
|
||||
});
|
||||
|
||||
const [createOrUpdateRequest, createResult] = useCreateOrUpdateSenderRequestMutation();
|
||||
const [sendRequest, sendResult] = useSendRequestMutation();
|
||||
|
||||
const createOrUpdateRequestAndSend = () => {
|
||||
const senderReq = getReqResult?.data?.senderRequest;
|
||||
createOrUpdateRequest({
|
||||
variables: {
|
||||
request: {
|
||||
// Update existing sender request if it was cloned from a request log
|
||||
// and it doesn't have a response body yet (e.g. not sent yet).
|
||||
...(senderReq && senderReq.sourceRequestLogID && !senderReq.response && { id: senderReq.id }),
|
||||
url,
|
||||
method,
|
||||
proto: httpProtoMap.get(proto),
|
||||
headers: headers.filter((kv) => kv.key !== ""),
|
||||
body: body || undefined,
|
||||
},
|
||||
},
|
||||
onCompleted: ({ createOrUpdateSenderRequest }) => {
|
||||
const { id } = createOrUpdateSenderRequest;
|
||||
sendRequestAndPushRoute(id);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const sendRequestAndPushRoute = (id: string) => {
|
||||
sendRequest({
|
||||
errorPolicy: "all",
|
||||
onCompleted: () => {
|
||||
router.push(`/sender?id=${id}`);
|
||||
},
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormSubmit: React.FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
createOrUpdateRequestAndSend();
|
||||
};
|
||||
|
||||
const isMountedRef = useRef(false);
|
||||
const [Allotment, setAllotment] = useState<
|
||||
(React.ComponentType<AllotmentProps> & { Pane: React.ComponentType<PaneProps> }) | null
|
||||
>(null);
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
import("allotment")
|
||||
.then((mod) => {
|
||||
if (!isMountedRef.current) {
|
||||
return;
|
||||
}
|
||||
setAllotment(mod.Allotment);
|
||||
})
|
||||
.catch((err) => console.error(err, `could not import allotment ${err.message}`));
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
if (!Allotment) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" height="100%" gap={2}>
|
||||
<Box component="form" autoComplete="off" onSubmit={handleFormSubmit}>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
<UrlBar
|
||||
method={method}
|
||||
onMethodChange={setMethod}
|
||||
url={url.toString()}
|
||||
onUrlChange={handleURLChange}
|
||||
proto={proto}
|
||||
onProtoChange={setProto}
|
||||
sx={{ flex: "1 auto" }}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
disableElevation
|
||||
sx={{ width: "8rem" }}
|
||||
type="submit"
|
||||
disabled={createResult.loading || sendResult.loading}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</Box>
|
||||
{createResult.error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{createResult.error.message}
|
||||
</Alert>
|
||||
)}
|
||||
{sendResult.error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{sendResult.error.message}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flex="1 auto" overflow="hidden">
|
||||
<Allotment>
|
||||
<Box pr={2} pb={2} height="100%" overflow="hidden">
|
||||
<Box height="100%" position="relative">
|
||||
<Typography variant="overline" color="textSecondary" sx={{ position: "absolute", right: 0, mt: 1.2 }}>
|
||||
Request
|
||||
</Typography>
|
||||
<EditRequestTabs
|
||||
queryParams={queryParams}
|
||||
headers={headers}
|
||||
body={body}
|
||||
onQueryParamChange={handleQueryParamChange}
|
||||
onQueryParamDelete={handleQueryParamDelete}
|
||||
onHeaderChange={handleHeaderChange}
|
||||
onHeaderDelete={handleHeaderDelete}
|
||||
onBodyChange={setBody}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box pb={2} pl={2} height="100%" overflow="hidden">
|
||||
<Box height="100%" position="relative">
|
||||
<Response response={response} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Allotment>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
Reference in New Issue
Block a user