mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Add Sender module
This commit is contained in:
@ -1,6 +1,18 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "prettier"],
|
||||
"rules": {
|
||||
"@next/next/no-css-tags": "off"
|
||||
}
|
||||
"@next/next/no-css-tags": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"plugins": ["unused-imports", "prettier"]
|
||||
}
|
||||
|
9
admin/gqlcodegen.yml
Normal file
9
admin/gqlcodegen.yml
Normal file
@ -0,0 +1,9 @@
|
||||
overwrite: true
|
||||
schema: "../pkg/api/schema.graphql"
|
||||
documents: "src/**/*.graphql"
|
||||
generates:
|
||||
src/generated/graphql.tsx:
|
||||
plugins:
|
||||
- "typescript"
|
||||
- "typescript-operations"
|
||||
- "typescript-react-apollo"
|
@ -7,7 +7,8 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"export": "next build && next export -o dist"
|
||||
"export": "next build && next export -o dist",
|
||||
"generate": "graphql-codegen --config gqlcodegen.yml"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.2.0",
|
||||
@ -18,6 +19,7 @@
|
||||
"@mui/icons-material": "^5.3.1",
|
||||
"@mui/lab": "^5.0.0-alpha.66",
|
||||
"@mui/material": "^5.3.1",
|
||||
"allotment": "^1.9.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"graphql": "^16.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
@ -29,13 +31,21 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@graphql-codegen/cli": "2.6.1",
|
||||
"@graphql-codegen/introspection": "2.1.1",
|
||||
"@graphql-codegen/typescript": "2.4.3",
|
||||
"@graphql-codegen/typescript-operations": "2.3.0",
|
||||
"@graphql-codegen/typescript-react-apollo": "3.2.6",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/node": "^17.0.12",
|
||||
"@types/react": "^17.0.38",
|
||||
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
||||
"@typescript-eslint/parser": "^5.12.0",
|
||||
"eslint": "^8.7.0",
|
||||
"eslint-config-next": "12.0.8",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"prettier": "^2.1.2",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^5.67.0"
|
||||
|
@ -151,7 +151,7 @@ export function Layout({ title, page, children }: Props): JSX.Element {
|
||||
});
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<Box sx={{ display: "flex", height: "100%" }}>
|
||||
<AppBar position="fixed" open={open}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
@ -241,8 +241,7 @@ export function Layout({ title, page, children }: Props): JSX.Element {
|
||||
</Link>
|
||||
</List>
|
||||
</Drawer>
|
||||
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
||||
<DrawerHeader />
|
||||
<Box component="main" sx={{ flexGrow: 1, mx: 3, mt: 11 }}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import MonacoEditor from "@monaco-editor/react";
|
||||
import monaco from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import MonacoEditor, { EditorProps } from "@monaco-editor/react";
|
||||
|
||||
const monacoOptions: monaco.editor.IEditorOptions = {
|
||||
const defaultMonacoOptions: EditorProps["options"] = {
|
||||
readOnly: true,
|
||||
wordWrap: "on",
|
||||
minimap: {
|
||||
@ -12,8 +11,9 @@ const monacoOptions: monaco.editor.IEditorOptions = {
|
||||
type language = "html" | "typescript" | "json";
|
||||
|
||||
function languageForContentType(contentType?: string): language | undefined {
|
||||
switch (contentType) {
|
||||
switch (contentType?.toLowerCase()) {
|
||||
case "text/html":
|
||||
case "text/html; charset=utf-8":
|
||||
return "html";
|
||||
case "application/json":
|
||||
case "application/json; charset=utf-8":
|
||||
@ -29,16 +29,19 @@ function languageForContentType(contentType?: string): language | undefined {
|
||||
interface Props {
|
||||
content: string;
|
||||
contentType?: string;
|
||||
monacoOptions?: EditorProps["options"];
|
||||
onChange?: EditorProps["onChange"];
|
||||
}
|
||||
|
||||
function Editor({ content, contentType }: Props): JSX.Element {
|
||||
function Editor({ content, contentType, monacoOptions, onChange }: Props): JSX.Element {
|
||||
console.log(content);
|
||||
return (
|
||||
<MonacoEditor
|
||||
height={"600px"}
|
||||
language={languageForContentType(contentType)}
|
||||
theme="vs-dark"
|
||||
options={monacoOptions}
|
||||
options={{ ...defaultMonacoOptions, ...monacoOptions }}
|
||||
value={content}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
37
admin/src/components/common/ResponseStatus.tsx
Normal file
37
admin/src/components/common/ResponseStatus.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
import HttpStatusIcon from "./HttpStatusIcon";
|
||||
import { HttpProtocol } from "../../generated/graphql";
|
||||
|
||||
type ResponseStatusProps = {
|
||||
proto: HttpProtocol;
|
||||
statusCode: number;
|
||||
statusReason: string;
|
||||
};
|
||||
|
||||
function mapProto(proto: HttpProtocol): string {
|
||||
switch (proto) {
|
||||
case HttpProtocol.Http1:
|
||||
return "HTTP/1.1";
|
||||
case HttpProtocol.Http2:
|
||||
return "HTTP/2.0";
|
||||
default:
|
||||
return proto;
|
||||
}
|
||||
}
|
||||
|
||||
function ResponseStatus({ proto, statusCode, statusReason }: ResponseStatusProps): JSX.Element {
|
||||
return (
|
||||
<Typography variant="h6" style={{ fontSize: "1rem", whiteSpace: "nowrap" }}>
|
||||
<HttpStatusIcon status={statusCode} />{" "}
|
||||
<Typography component="span" color="textSecondary">
|
||||
<Typography component="span" color="textSecondary" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
|
||||
{mapProto(proto)}
|
||||
</Typography>
|
||||
</Typography>{" "}
|
||||
{statusCode} {statusReason}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResponseStatus;
|
47
admin/src/components/common/useContextMenu.tsx
Normal file
47
admin/src/components/common/useContextMenu.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import * as React from "react";
|
||||
import { Menu } from "@mui/material";
|
||||
|
||||
export interface ContextMenuProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
function useContextMenu(): [(props: ContextMenuProps) => JSX.Element, (e: React.MouseEvent) => void, () => void] {
|
||||
const [contextMenu, setContextMenu] = React.useState<{
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
setContextMenu(
|
||||
contextMenu === null
|
||||
? {
|
||||
mouseX: event.clientX - 2,
|
||||
mouseY: event.clientY - 4,
|
||||
}
|
||||
: // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu
|
||||
// Other native context menus might behave different.
|
||||
// With this behavior we prevent contextmenu from the backdrop to re-locale existing context menus.
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setContextMenu(null);
|
||||
};
|
||||
|
||||
const menu = ({ children }: ContextMenuProps): JSX.Element => (
|
||||
<Menu
|
||||
open={contextMenu !== null}
|
||||
onClose={handleClose}
|
||||
anchorReference="anchorPosition"
|
||||
anchorPosition={contextMenu !== null ? { top: contextMenu.mouseY, left: contextMenu.mouseX } : undefined}
|
||||
>
|
||||
{children}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return [menu, handleContextMenu, handleClose];
|
||||
}
|
||||
|
||||
export default useContextMenu;
|
@ -68,7 +68,13 @@ const DELETE_PROJECT = gql`
|
||||
|
||||
function ProjectList(): JSX.Element {
|
||||
const theme = useTheme();
|
||||
const { loading: projLoading, error: projErr, data: projData } = useQuery<{ projects: Project[] }>(PROJECTS);
|
||||
const {
|
||||
loading: projLoading,
|
||||
error: projErr,
|
||||
data: projData,
|
||||
} = useQuery<{ projects: Project[] }>(PROJECTS, {
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
const [openProject, { error: openProjErr, loading: openProjLoading }] = useMutation<{ openProject: Project }>(
|
||||
OPEN_PROJECT,
|
||||
{
|
||||
@ -114,9 +120,12 @@ function ProjectList(): JSX.Element {
|
||||
},
|
||||
}
|
||||
);
|
||||
const [closeProject, { error: closeProjErr }] = useMutation(CLOSE_PROJECT, {
|
||||
const [closeProject, { error: closeProjErr, client }] = useMutation(CLOSE_PROJECT, {
|
||||
errorPolicy: "all",
|
||||
onError: () => {},
|
||||
onCompleted() {
|
||||
client.resetStore();
|
||||
},
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
|
@ -5,7 +5,7 @@ import Alert from "@mui/lab/Alert";
|
||||
|
||||
import RequestList from "./RequestList";
|
||||
import LogDetail from "./LogDetail";
|
||||
import CenteredPaper from "../CenteredPaper";
|
||||
import CenteredPaper from "../common/CenteredPaper";
|
||||
import { useHttpRequestLogs } from "./hooks/useHttpRequestLogs";
|
||||
|
||||
function LogsOverview(): JSX.Element {
|
||||
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
import { Typography, Box, Divider } from "@mui/material";
|
||||
|
||||
import HttpHeadersTable from "./HttpHeadersTable";
|
||||
import Editor from "./Editor";
|
||||
import Editor from "../common/Editor";
|
||||
|
||||
interface Props {
|
||||
request: {
|
||||
|
@ -9,11 +9,18 @@ import {
|
||||
Typography,
|
||||
Box,
|
||||
useTheme,
|
||||
MenuItem,
|
||||
Snackbar,
|
||||
Alert,
|
||||
Link,
|
||||
} from "@mui/material";
|
||||
|
||||
import HttpStatusIcon from "./HttpStatusCode";
|
||||
import CenteredPaper from "../CenteredPaper";
|
||||
import HttpStatusIcon from "../common/HttpStatusIcon";
|
||||
import CenteredPaper from "../common/CenteredPaper";
|
||||
import { RequestLog } from "../../lib/requestLogs";
|
||||
import useContextMenu from "../common/useContextMenu";
|
||||
import React, { useState } from "react";
|
||||
import { useCreateSenderRequestFromHttpRequestLogMutation } from "../../generated/graphql";
|
||||
|
||||
interface Props {
|
||||
logs: RequestLog[];
|
||||
@ -45,69 +52,117 @@ interface RequestListTableProps {
|
||||
function RequestListTable({ logs, selectedReqLogId, onLogClick }: RequestListTableProps): JSX.Element {
|
||||
const theme = useTheme();
|
||||
|
||||
const [createSenderReqFromLog] = useCreateSenderRequestFromHttpRequestLogMutation({});
|
||||
|
||||
const [copyToSenderId, setCopyToSenderId] = useState("");
|
||||
const [Menu, handleContextMenu, handleContextMenuClose] = useContextMenu();
|
||||
|
||||
const handleCopyToSenderClick = () => {
|
||||
createSenderReqFromLog({
|
||||
variables: {
|
||||
id: copyToSenderId,
|
||||
},
|
||||
onCompleted({ createSenderRequestFromHttpRequestLog }) {
|
||||
const { id } = createSenderRequestFromHttpRequestLog;
|
||||
setNewSenderReqId(id);
|
||||
setCopiedReqNotifOpen(true);
|
||||
},
|
||||
});
|
||||
handleContextMenuClose();
|
||||
};
|
||||
|
||||
const [newSenderReqId, setNewSenderReqId] = React.useState("");
|
||||
const [copiedReqNotifOpen, setCopiedReqNotifOpen] = React.useState(false);
|
||||
const handleCloseCopiedNotif = (_: Event | React.SyntheticEvent, reason?: string) => {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
setCopiedReqNotifOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
style={{
|
||||
minHeight: logs.length ? 200 : 0,
|
||||
height: logs.length ? "24vh" : "inherit",
|
||||
}}
|
||||
>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Method</TableCell>
|
||||
<TableCell>Origin</TableCell>
|
||||
<TableCell>Path</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{logs.map(({ id, method, url, response }) => {
|
||||
const { origin, pathname, search, hash } = new URL(url);
|
||||
<div>
|
||||
<Menu>
|
||||
<MenuItem onClick={handleCopyToSenderClick}>Copy request to Sender</MenuItem>
|
||||
</Menu>
|
||||
<Snackbar
|
||||
open={copiedReqNotifOpen}
|
||||
autoHideDuration={3000}
|
||||
onClose={handleCloseCopiedNotif}
|
||||
anchorOrigin={{ horizontal: "center", vertical: "bottom" }}
|
||||
>
|
||||
<Alert onClose={handleCloseCopiedNotif} severity="info">
|
||||
Request was copied. <Link href={`/sender?id=${newSenderReqId}`}>Edit in Sender.</Link>
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
const cellStyle = {
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
} as any;
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
style={{
|
||||
minHeight: logs.length ? 200 : 0,
|
||||
height: logs.length ? "24vh" : "inherit",
|
||||
}}
|
||||
>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Method</TableCell>
|
||||
<TableCell>Origin</TableCell>
|
||||
<TableCell>Path</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{logs.map(({ id, method, url, response }) => {
|
||||
const { origin, pathname, search, hash } = new URL(url);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={id}
|
||||
sx={{
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
},
|
||||
...(id === selectedReqLogId && {
|
||||
bgcolor: theme.palette.action.selected,
|
||||
}),
|
||||
}}
|
||||
hover
|
||||
onClick={() => onLogClick(id)}
|
||||
>
|
||||
<TableCell style={{ ...cellStyle, width: "100px" }}>
|
||||
<code>{method}</code>
|
||||
</TableCell>
|
||||
<TableCell sx={{ ...cellStyle, maxWidth: "100px" }}>{origin}</TableCell>
|
||||
<TableCell sx={{ ...cellStyle, maxWidth: "200px" }}>
|
||||
{decodeURIComponent(pathname + search + hash)}
|
||||
</TableCell>
|
||||
<TableCell style={{ maxWidth: "100px" }}>
|
||||
{response && (
|
||||
<div>
|
||||
<HttpStatusIcon status={response.statusCode} />{" "}
|
||||
<code>
|
||||
{response.statusCode} {response.statusReason}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
const cellStyle = {
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
} as any;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={id}
|
||||
sx={{
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
},
|
||||
...(id === selectedReqLogId && {
|
||||
bgcolor: theme.palette.action.selected,
|
||||
}),
|
||||
}}
|
||||
hover
|
||||
onClick={() => onLogClick(id)}
|
||||
onContextMenu={(e) => {
|
||||
setCopyToSenderId(id);
|
||||
handleContextMenu(e);
|
||||
}}
|
||||
>
|
||||
<TableCell style={{ ...cellStyle, width: "100px" }}>
|
||||
<code>{method}</code>
|
||||
</TableCell>
|
||||
<TableCell sx={{ ...cellStyle, maxWidth: "100px" }}>{origin}</TableCell>
|
||||
<TableCell sx={{ ...cellStyle, maxWidth: "200px" }}>
|
||||
{decodeURIComponent(pathname + search + hash)}
|
||||
</TableCell>
|
||||
<TableCell style={{ maxWidth: "100px" }}>
|
||||
{response && (
|
||||
<div>
|
||||
<HttpStatusIcon status={response.statusCode} />{" "}
|
||||
<code>
|
||||
{response.statusCode} {response.statusReason}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Typography, Box, Divider } from "@mui/material";
|
||||
|
||||
import HttpStatusIcon from "./HttpStatusCode";
|
||||
import Editor from "./Editor";
|
||||
import HttpStatusIcon from "../common/HttpStatusIcon";
|
||||
import Editor from "../common/Editor";
|
||||
import HttpHeadersTable from "./HttpHeadersTable";
|
||||
|
||||
interface Props {
|
||||
|
13
admin/src/components/reqlog/hooks/useCreateSenderRequest.ts
Normal file
13
admin/src/components/reqlog/hooks/useCreateSenderRequest.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { gql, useMutation } from "@apollo/client";
|
||||
|
||||
const CREATE_SENDER_REQUEST = gql`
|
||||
mutation CreateSenderRequest($request: SenderRequestInput!) {
|
||||
createSenderRequest(request: $request) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default function useCreateSenderRequest() {
|
||||
return useMutation(CREATE_SENDER_REQUEST);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { gql, useApolloClient, useMutation, useQuery } from "@apollo/client";
|
||||
import { gql, useApolloClient, useMutation } from "@apollo/client";
|
||||
import {
|
||||
Avatar,
|
||||
Chip,
|
||||
|
398
admin/src/components/sender/EditRequest.tsx
Normal file
398
admin/src/components/sender/EditRequest.tsx
Normal file
@ -0,0 +1,398 @@
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
BoxProps,
|
||||
Button,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { ComponentType, FormEventHandler, useEffect, useRef, useState } from "react";
|
||||
import { AllotmentProps, PaneProps } from "allotment/dist/types/src/allotment";
|
||||
|
||||
import { KeyValuePair, sortKeyValuePairs } from "./KeyValuePair";
|
||||
import {
|
||||
GetSenderRequestQuery,
|
||||
HttpProtocol,
|
||||
useCreateOrUpdateSenderRequestMutation,
|
||||
useGetSenderRequestQuery,
|
||||
useSendRequestMutation,
|
||||
} from "../../generated/graphql";
|
||||
import EditRequestTabs from "./EditRequestTabs";
|
||||
import Response from "./Response";
|
||||
|
||||
import "allotment/dist/style.css";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
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: any[]): any[] {
|
||||
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 (let [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: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
createOrUpdateRequestAndSend();
|
||||
};
|
||||
|
||||
const isMountedRef = useRef(false);
|
||||
const [Allotment, setAllotment] = useState<
|
||||
(ComponentType<AllotmentProps> & { Pane: ComponentType<PaneProps> }) | null
|
||||
>(null);
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
import("allotment")
|
||||
.then((mod) => {
|
||||
if (!isMountedRef.current) {
|
||||
return;
|
||||
}
|
||||
setAllotment(mod.Allotment);
|
||||
})
|
||||
.catch((err) => console.error(err, `could not import allotment ${err.message}`));
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
if (!Allotment) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<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;
|
91
admin/src/components/sender/EditRequestTabs.tsx
Normal file
91
admin/src/components/sender/EditRequestTabs.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { TabContext, TabList, TabPanel } from "@mui/lab";
|
||||
import { Box, Tab } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import Editor from "../common/Editor";
|
||||
|
||||
import KeyValuePairTable, { KeyValuePair, KeyValuePairTableProps } from "./KeyValuePair";
|
||||
|
||||
enum TabValue {
|
||||
QueryParams = "queryParams",
|
||||
Headers = "headers",
|
||||
Body = "body",
|
||||
}
|
||||
|
||||
export type EditRequestTabsProps = {
|
||||
queryParams: KeyValuePair[];
|
||||
headers: KeyValuePair[];
|
||||
onQueryParamChange: KeyValuePairTableProps["onChange"];
|
||||
onQueryParamDelete: KeyValuePairTableProps["onDelete"];
|
||||
onHeaderChange: KeyValuePairTableProps["onChange"];
|
||||
onHeaderDelete: KeyValuePairTableProps["onDelete"];
|
||||
body: string;
|
||||
onBodyChange: (value: string) => void;
|
||||
};
|
||||
|
||||
function EditRequestTabs(props: EditRequestTabsProps): JSX.Element {
|
||||
const {
|
||||
queryParams,
|
||||
onQueryParamChange,
|
||||
onQueryParamDelete,
|
||||
headers,
|
||||
onHeaderChange,
|
||||
onHeaderDelete,
|
||||
body,
|
||||
onBodyChange,
|
||||
} = props;
|
||||
const [tabValue, setTabValue] = useState(TabValue.QueryParams);
|
||||
|
||||
const tabSx = {
|
||||
textTransform: "none",
|
||||
};
|
||||
|
||||
return (
|
||||
<Box height="100%" sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<TabContext value={tabValue}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 2 }}>
|
||||
<TabList onChange={(_, value) => setTabValue(value)}>
|
||||
<Tab
|
||||
value={TabValue.QueryParams}
|
||||
label={"Query Params" + (queryParams.length - 1 ? ` (${queryParams.length - 1})` : "")}
|
||||
sx={tabSx}
|
||||
/>
|
||||
<Tab
|
||||
value={TabValue.Headers}
|
||||
label={"Headers" + (headers.length - 1 ? ` (${headers.length - 1})` : "")}
|
||||
sx={tabSx}
|
||||
/>
|
||||
<Tab
|
||||
value={TabValue.Body}
|
||||
label={"Body" + (body.length ? ` (${body.length} byte` + (body.length > 1 ? "s" : "") + ")" : "")}
|
||||
sx={tabSx}
|
||||
/>
|
||||
</TabList>
|
||||
</Box>
|
||||
<Box flex="1 auto" overflow="hidden">
|
||||
<TabPanel value={TabValue.QueryParams} sx={{ p: 0, height: "100%", overflow: "scroll" }}>
|
||||
<Box>
|
||||
<KeyValuePairTable items={queryParams} onChange={onQueryParamChange} onDelete={onQueryParamDelete} />
|
||||
</Box>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabValue.Headers} sx={{ p: 0, height: "100%", overflow: "scroll" }}>
|
||||
<Box>
|
||||
<KeyValuePairTable items={headers} onChange={onHeaderChange} onDelete={onHeaderDelete} />
|
||||
</Box>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabValue.Body} sx={{ p: 0, height: "100%" }}>
|
||||
<Editor
|
||||
content={body}
|
||||
onChange={(value) => {
|
||||
onBodyChange(value || "");
|
||||
}}
|
||||
monacoOptions={{ readOnly: false }}
|
||||
contentType={headers.find(({ key }) => key.toLowerCase() === "content-type")?.value}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</TabContext>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditRequestTabs;
|
93
admin/src/components/sender/History.tsx
Normal file
93
admin/src/components/sender/History.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { TableContainer, Table, TableHead, TableRow, TableCell, Typography, Box, TableBody } from "@mui/material";
|
||||
import { useRouter } from "next/router";
|
||||
import { useGetSenderRequestsQuery } from "../../generated/graphql";
|
||||
import CenteredPaper from "../common/CenteredPaper";
|
||||
import HttpStatusIcon from "../common/HttpStatusIcon";
|
||||
|
||||
function History(): JSX.Element {
|
||||
const { data, loading } = useGetSenderRequestsQuery({
|
||||
pollInterval: 1000,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const activeId = router.query.id as string | undefined;
|
||||
|
||||
const handleRowClick = (id: string) => {
|
||||
router.push(`/sender?id=${id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{!loading && data?.senderRequests && data?.senderRequests.length > 0 && (
|
||||
<TableContainer sx={{ overflowX: "initial" }}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Method</TableCell>
|
||||
<TableCell>Origin</TableCell>
|
||||
<TableCell>Path</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data?.senderRequests &&
|
||||
data.senderRequests.map(({ id, method, url, response }) => {
|
||||
const { origin, pathname, search, hash } = new URL(url);
|
||||
|
||||
const cellStyle = {
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
} as any;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={id}
|
||||
sx={{
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
},
|
||||
...(id === activeId && {
|
||||
bgcolor: "action.selected",
|
||||
cursor: "inherit",
|
||||
}),
|
||||
}}
|
||||
hover
|
||||
onClick={() => handleRowClick(id)}
|
||||
>
|
||||
<TableCell style={{ ...cellStyle, width: "100px" }}>
|
||||
<code>{method}</code>
|
||||
</TableCell>
|
||||
<TableCell sx={{ ...cellStyle, maxWidth: "100px" }}>{origin}</TableCell>
|
||||
<TableCell sx={{ ...cellStyle, maxWidth: "200px" }}>
|
||||
{decodeURIComponent(pathname + search + hash)}
|
||||
</TableCell>
|
||||
<TableCell style={{ maxWidth: "100px" }}>
|
||||
{response && (
|
||||
<div>
|
||||
<HttpStatusIcon status={response.statusCode} />{" "}
|
||||
<code>
|
||||
{response.statusCode} {response.statusReason}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
<Box sx={{ mt: 2, height: "100%" }}>
|
||||
{!loading && data?.senderRequests.length === 0 && (
|
||||
<CenteredPaper>
|
||||
<Typography>No requests created yet.</Typography>
|
||||
</CenteredPaper>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default History;
|
130
admin/src/components/sender/KeyValuePair.tsx
Normal file
130
admin/src/components/sender/KeyValuePair.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
import { IconButton, InputBase, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material";
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
|
||||
export type KeyValuePair = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type KeyValuePairTableProps = {
|
||||
items: KeyValuePair[];
|
||||
onChange?: (key: string, value: string, index: number) => void;
|
||||
onDelete?: (index: number) => void;
|
||||
};
|
||||
|
||||
export function KeyValuePairTable({ items, onChange, onDelete }: KeyValuePairTableProps): JSX.Element {
|
||||
const inputSx = {
|
||||
fontSize: "0.875rem",
|
||||
"&.MuiInputBase-root input": {
|
||||
p: 0,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Key</TableCell>
|
||||
<TableCell>Value</TableCell>
|
||||
{onDelete && <TableCell padding="checkbox"></TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody
|
||||
sx={{
|
||||
"td, th, input": {
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontSize: "0.75rem",
|
||||
py: 0.2,
|
||||
},
|
||||
"td span, th span": {
|
||||
display: "block",
|
||||
py: 0.7,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{items.map(({ key, value }, idx) => (
|
||||
<TableRow
|
||||
key={idx}
|
||||
hover
|
||||
sx={{
|
||||
"& .delete-button": {
|
||||
visibility: "hidden",
|
||||
},
|
||||
"&:hover .delete-button": {
|
||||
visibility: "inherit",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TableCell component="th" scope="row">
|
||||
{!onChange && <span>{key}</span>}
|
||||
{onChange && (
|
||||
<InputBase
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="Key"
|
||||
value={key}
|
||||
onChange={(e) => {
|
||||
onChange && onChange(e.target.value, value, idx);
|
||||
}}
|
||||
sx={inputSx}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell sx={{ width: "60%", wordBreak: "break-all" }}>
|
||||
{!onChange && value}
|
||||
{onChange && (
|
||||
<InputBase
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="Value"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange && onChange(key, e.target.value, idx);
|
||||
}}
|
||||
sx={inputSx}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
{onDelete && (
|
||||
<TableCell>
|
||||
<div className="delete-button">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
onDelete && onDelete(idx);
|
||||
}}
|
||||
sx={{
|
||||
visibility: onDelete === undefined || items.length === idx + 1 ? "hidden" : "inherit",
|
||||
}}
|
||||
>
|
||||
<ClearIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function sortKeyValuePairs(items: KeyValuePair[]): KeyValuePair[] {
|
||||
const sorted = [...items];
|
||||
|
||||
sorted.sort((a, b) => {
|
||||
if (a.key < b.key) {
|
||||
return -1;
|
||||
}
|
||||
if (a.key > b.key) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
export default KeyValuePairTable;
|
40
admin/src/components/sender/Response.tsx
Normal file
40
admin/src/components/sender/Response.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
import { sortKeyValuePairs } from "./KeyValuePair";
|
||||
import ResponseTabs from "./ResponseTabs";
|
||||
import ResponseStatus from "../common/ResponseStatus";
|
||||
import { HttpResponseLog } from "../../generated/graphql";
|
||||
|
||||
export type ResponseProps = {
|
||||
response?: HttpResponseLog | null;
|
||||
};
|
||||
|
||||
function Response({ response }: ResponseProps): JSX.Element {
|
||||
return (
|
||||
<Box height="100%">
|
||||
<div>
|
||||
<Box sx={{ position: "absolute", right: 0, mt: 1.4 }}>
|
||||
<Typography variant="overline" color="textSecondary" sx={{ float: "right", ml: 3 }}>
|
||||
Response
|
||||
</Typography>
|
||||
{response && (
|
||||
<Box sx={{ float: "right", mt: 0.2 }}>
|
||||
<ResponseStatus
|
||||
proto={response.proto}
|
||||
statusCode={response.statusCode}
|
||||
statusReason={response.statusReason}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
<ResponseTabs
|
||||
body={response?.body}
|
||||
headers={sortKeyValuePairs(response?.headers || [])}
|
||||
hasResponse={response !== undefined && response !== null}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Response;
|
69
admin/src/components/sender/ResponseTabs.tsx
Normal file
69
admin/src/components/sender/ResponseTabs.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { TabContext, TabList, TabPanel } from "@mui/lab";
|
||||
import { Box, Tab, Typography } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { HttpResponseLog } from "../../generated/graphql";
|
||||
import CenteredPaper from "../common/CenteredPaper";
|
||||
import Editor from "../common/Editor";
|
||||
|
||||
import KeyValuePairTable from "./KeyValuePair";
|
||||
|
||||
export type ResponseTabsProps = {
|
||||
headers: HttpResponseLog["headers"];
|
||||
body: HttpResponseLog["body"];
|
||||
hasResponse: boolean;
|
||||
};
|
||||
|
||||
enum TabValue {
|
||||
Body = "body",
|
||||
Headers = "headers",
|
||||
}
|
||||
|
||||
const reqNotSent = (
|
||||
<CenteredPaper>
|
||||
<Typography>Response not received yet.</Typography>
|
||||
</CenteredPaper>
|
||||
);
|
||||
|
||||
function ResponseTabs(props: ResponseTabsProps): JSX.Element {
|
||||
const { headers, body, hasResponse } = props;
|
||||
const [tabValue, setTabValue] = useState(TabValue.Body);
|
||||
|
||||
const contentType = headers.find((header) => header.key.toLowerCase() === "content-type")?.value;
|
||||
|
||||
const tabSx = {
|
||||
textTransform: "none",
|
||||
};
|
||||
|
||||
return (
|
||||
<Box height="100%" sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<TabContext value={tabValue}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 2 }}>
|
||||
<TabList onChange={(_, value) => setTabValue(value)}>
|
||||
<Tab
|
||||
value={TabValue.Body}
|
||||
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}
|
||||
/>
|
||||
</TabList>
|
||||
</Box>
|
||||
<Box flex="1 auto" overflow="hidden">
|
||||
<TabPanel value={TabValue.Body} sx={{ p: 0, height: "100%" }}>
|
||||
{body && <Editor content={body} contentType={contentType} />}
|
||||
{!hasResponse && reqNotSent}
|
||||
</TabPanel>
|
||||
<TabPanel value={TabValue.Headers} sx={{ p: 0, height: "100%", overflow: "scroll" }}>
|
||||
{headers.length > 0 && <KeyValuePairTable items={headers} />}
|
||||
{!hasResponse && reqNotSent}
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</TabContext>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResponseTabs;
|
@ -0,0 +1,5 @@
|
||||
mutation CreateOrUpdateSenderRequest($request: SenderRequestInput!) {
|
||||
createOrUpdateSenderRequest(request: $request) {
|
||||
id
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
mutation CreateSenderRequestFromHttpRequestLog($id: ID!) {
|
||||
createSenderRequestFromHttpRequestLog(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
5
admin/src/components/sender/sendRequest.graphql
Normal file
5
admin/src/components/sender/sendRequest.graphql
Normal file
@ -0,0 +1,5 @@
|
||||
mutation SendRequest($id: ID!) {
|
||||
sendRequest(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
26
admin/src/components/sender/senderRequest.graphql
Normal file
26
admin/src/components/sender/senderRequest.graphql
Normal file
@ -0,0 +1,26 @@
|
||||
query GetSenderRequest($id: ID!) {
|
||||
senderRequest(id: $id) {
|
||||
id
|
||||
sourceRequestLogID
|
||||
url
|
||||
method
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
body
|
||||
timestamp
|
||||
response {
|
||||
id
|
||||
proto
|
||||
statusCode
|
||||
statusReason
|
||||
body
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
admin/src/components/sender/senderRequests.graphql
Normal file
12
admin/src/components/sender/senderRequests.graphql
Normal file
@ -0,0 +1,12 @@
|
||||
query GetSenderRequests {
|
||||
senderRequests {
|
||||
id
|
||||
url
|
||||
method
|
||||
response {
|
||||
id
|
||||
statusCode
|
||||
statusReason
|
||||
}
|
||||
}
|
||||
}
|
479
admin/src/generated/graphql.tsx
Normal file
479
admin/src/generated/graphql.tsx
Normal file
@ -0,0 +1,479 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
export type Maybe<T> = T | null;
|
||||
export type InputMaybe<T> = Maybe<T>;
|
||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
|
||||
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
|
||||
const defaultOptions = {} as const;
|
||||
/** All built-in and custom scalars, mapped to their actual values */
|
||||
export type Scalars = {
|
||||
ID: string;
|
||||
String: string;
|
||||
Boolean: boolean;
|
||||
Int: number;
|
||||
Float: number;
|
||||
Regexp: any;
|
||||
Time: any;
|
||||
URL: any;
|
||||
};
|
||||
|
||||
export type ClearHttpRequestLogResult = {
|
||||
__typename?: 'ClearHTTPRequestLogResult';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type CloseProjectResult = {
|
||||
__typename?: 'CloseProjectResult';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type DeleteProjectResult = {
|
||||
__typename?: 'DeleteProjectResult';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type DeleteSenderRequestsResult = {
|
||||
__typename?: 'DeleteSenderRequestsResult';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type HttpHeader = {
|
||||
__typename?: 'HttpHeader';
|
||||
key: Scalars['String'];
|
||||
value: Scalars['String'];
|
||||
};
|
||||
|
||||
export type HttpHeaderInput = {
|
||||
key: Scalars['String'];
|
||||
value: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum HttpMethod {
|
||||
Connect = 'CONNECT',
|
||||
Delete = 'DELETE',
|
||||
Get = 'GET',
|
||||
Head = 'HEAD',
|
||||
Options = 'OPTIONS',
|
||||
Patch = 'PATCH',
|
||||
Post = 'POST',
|
||||
Put = 'PUT',
|
||||
Trace = 'TRACE'
|
||||
}
|
||||
|
||||
export enum HttpProtocol {
|
||||
Http1 = 'HTTP1',
|
||||
Http2 = 'HTTP2'
|
||||
}
|
||||
|
||||
export type HttpRequestLog = {
|
||||
__typename?: 'HttpRequestLog';
|
||||
body?: Maybe<Scalars['String']>;
|
||||
headers: Array<HttpHeader>;
|
||||
id: Scalars['ID'];
|
||||
method: HttpMethod;
|
||||
proto: Scalars['String'];
|
||||
response?: Maybe<HttpResponseLog>;
|
||||
timestamp: Scalars['Time'];
|
||||
url: Scalars['String'];
|
||||
};
|
||||
|
||||
export type HttpRequestLogFilter = {
|
||||
__typename?: 'HttpRequestLogFilter';
|
||||
onlyInScope: Scalars['Boolean'];
|
||||
searchExpression?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type HttpRequestLogFilterInput = {
|
||||
onlyInScope?: InputMaybe<Scalars['Boolean']>;
|
||||
searchExpression?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type HttpResponseLog = {
|
||||
__typename?: 'HttpResponseLog';
|
||||
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 Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
clearHTTPRequestLog: ClearHttpRequestLogResult;
|
||||
closeProject: CloseProjectResult;
|
||||
createOrUpdateSenderRequest: SenderRequest;
|
||||
createProject?: Maybe<Project>;
|
||||
createSenderRequestFromHttpRequestLog: SenderRequest;
|
||||
deleteProject: DeleteProjectResult;
|
||||
deleteSenderRequests: DeleteSenderRequestsResult;
|
||||
openProject?: Maybe<Project>;
|
||||
sendRequest: SenderRequest;
|
||||
setHttpRequestLogFilter?: Maybe<HttpRequestLogFilter>;
|
||||
setScope: Array<ScopeRule>;
|
||||
setSenderRequestFilter?: Maybe<SenderRequestFilter>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOrUpdateSenderRequestArgs = {
|
||||
request: SenderRequestInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateProjectArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateSenderRequestFromHttpRequestLogArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteProjectArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationOpenProjectArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationSendRequestArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetHttpRequestLogFilterArgs = {
|
||||
filter?: InputMaybe<HttpRequestLogFilterInput>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetScopeArgs = {
|
||||
scope: Array<ScopeRuleInput>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetSenderRequestFilterArgs = {
|
||||
filter?: InputMaybe<SenderRequestFilterInput>;
|
||||
};
|
||||
|
||||
export type Project = {
|
||||
__typename?: 'Project';
|
||||
id: Scalars['ID'];
|
||||
isActive: Scalars['Boolean'];
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
activeProject?: Maybe<Project>;
|
||||
httpRequestLog?: Maybe<HttpRequestLog>;
|
||||
httpRequestLogFilter?: Maybe<HttpRequestLogFilter>;
|
||||
httpRequestLogs: Array<HttpRequestLog>;
|
||||
projects: Array<Project>;
|
||||
scope: Array<ScopeRule>;
|
||||
senderRequest?: Maybe<SenderRequest>;
|
||||
senderRequests: Array<SenderRequest>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryHttpRequestLogArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type QuerySenderRequestArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type ScopeHeader = {
|
||||
__typename?: 'ScopeHeader';
|
||||
key?: Maybe<Scalars['Regexp']>;
|
||||
value?: Maybe<Scalars['Regexp']>;
|
||||
};
|
||||
|
||||
export type ScopeHeaderInput = {
|
||||
key?: InputMaybe<Scalars['Regexp']>;
|
||||
value?: InputMaybe<Scalars['Regexp']>;
|
||||
};
|
||||
|
||||
export type ScopeRule = {
|
||||
__typename?: 'ScopeRule';
|
||||
body?: Maybe<Scalars['Regexp']>;
|
||||
header?: Maybe<ScopeHeader>;
|
||||
url?: Maybe<Scalars['Regexp']>;
|
||||
};
|
||||
|
||||
export type ScopeRuleInput = {
|
||||
body?: InputMaybe<Scalars['Regexp']>;
|
||||
header?: InputMaybe<ScopeHeaderInput>;
|
||||
url?: InputMaybe<Scalars['Regexp']>;
|
||||
};
|
||||
|
||||
export type SenderRequest = {
|
||||
__typename?: 'SenderRequest';
|
||||
body?: Maybe<Scalars['String']>;
|
||||
headers?: Maybe<Array<HttpHeader>>;
|
||||
id: Scalars['ID'];
|
||||
method: HttpMethod;
|
||||
proto: HttpProtocol;
|
||||
response?: Maybe<HttpResponseLog>;
|
||||
sourceRequestLogID?: Maybe<Scalars['ID']>;
|
||||
timestamp: Scalars['Time'];
|
||||
url: Scalars['URL'];
|
||||
};
|
||||
|
||||
export type SenderRequestFilter = {
|
||||
__typename?: 'SenderRequestFilter';
|
||||
onlyInScope: Scalars['Boolean'];
|
||||
searchExpression?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type SenderRequestFilterInput = {
|
||||
onlyInScope?: InputMaybe<Scalars['Boolean']>;
|
||||
searchExpression?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type SenderRequestInput = {
|
||||
body?: InputMaybe<Scalars['String']>;
|
||||
headers?: InputMaybe<Array<HttpHeaderInput>>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
method?: InputMaybe<HttpMethod>;
|
||||
proto?: InputMaybe<HttpProtocol>;
|
||||
url: Scalars['URL'];
|
||||
};
|
||||
|
||||
export type CreateOrUpdateSenderRequestMutationVariables = Exact<{
|
||||
request: SenderRequestInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateOrUpdateSenderRequestMutation = { __typename?: 'Mutation', createOrUpdateSenderRequest: { __typename?: 'SenderRequest', id: string } };
|
||||
|
||||
export type CreateSenderRequestFromHttpRequestLogMutationVariables = Exact<{
|
||||
id: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateSenderRequestFromHttpRequestLogMutation = { __typename?: 'Mutation', createSenderRequestFromHttpRequestLog: { __typename?: 'SenderRequest', id: string } };
|
||||
|
||||
export type SendRequestMutationVariables = Exact<{
|
||||
id: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SendRequestMutation = { __typename?: 'Mutation', sendRequest: { __typename?: 'SenderRequest', id: string } };
|
||||
|
||||
export type GetSenderRequestQueryVariables = Exact<{
|
||||
id: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetSenderRequestQuery = { __typename?: 'Query', senderRequest?: { __typename?: 'SenderRequest', id: string, sourceRequestLogID?: string | null, url: any, method: HttpMethod, proto: HttpProtocol, body?: string | null, timestamp: any, headers?: Array<{ __typename?: 'HttpHeader', key: string, value: string }> | null, response?: { __typename?: 'HttpResponseLog', id: string, proto: HttpProtocol, statusCode: number, statusReason: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null } | null };
|
||||
|
||||
export type 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 const CreateOrUpdateSenderRequestDocument = gql`
|
||||
mutation CreateOrUpdateSenderRequest($request: SenderRequestInput!) {
|
||||
createOrUpdateSenderRequest(request: $request) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type CreateOrUpdateSenderRequestMutationFn = Apollo.MutationFunction<CreateOrUpdateSenderRequestMutation, CreateOrUpdateSenderRequestMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useCreateOrUpdateSenderRequestMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useCreateOrUpdateSenderRequestMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCreateOrUpdateSenderRequestMutation` 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 [createOrUpdateSenderRequestMutation, { data, loading, error }] = useCreateOrUpdateSenderRequestMutation({
|
||||
* variables: {
|
||||
* request: // value for 'request'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCreateOrUpdateSenderRequestMutation(baseOptions?: Apollo.MutationHookOptions<CreateOrUpdateSenderRequestMutation, CreateOrUpdateSenderRequestMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CreateOrUpdateSenderRequestMutation, CreateOrUpdateSenderRequestMutationVariables>(CreateOrUpdateSenderRequestDocument, options);
|
||||
}
|
||||
export type CreateOrUpdateSenderRequestMutationHookResult = ReturnType<typeof useCreateOrUpdateSenderRequestMutation>;
|
||||
export type CreateOrUpdateSenderRequestMutationResult = Apollo.MutationResult<CreateOrUpdateSenderRequestMutation>;
|
||||
export type CreateOrUpdateSenderRequestMutationOptions = Apollo.BaseMutationOptions<CreateOrUpdateSenderRequestMutation, CreateOrUpdateSenderRequestMutationVariables>;
|
||||
export const CreateSenderRequestFromHttpRequestLogDocument = gql`
|
||||
mutation CreateSenderRequestFromHttpRequestLog($id: ID!) {
|
||||
createSenderRequestFromHttpRequestLog(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type CreateSenderRequestFromHttpRequestLogMutationFn = Apollo.MutationFunction<CreateSenderRequestFromHttpRequestLogMutation, CreateSenderRequestFromHttpRequestLogMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useCreateSenderRequestFromHttpRequestLogMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useCreateSenderRequestFromHttpRequestLogMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCreateSenderRequestFromHttpRequestLogMutation` 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 [createSenderRequestFromHttpRequestLogMutation, { data, loading, error }] = useCreateSenderRequestFromHttpRequestLogMutation({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCreateSenderRequestFromHttpRequestLogMutation(baseOptions?: Apollo.MutationHookOptions<CreateSenderRequestFromHttpRequestLogMutation, CreateSenderRequestFromHttpRequestLogMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CreateSenderRequestFromHttpRequestLogMutation, CreateSenderRequestFromHttpRequestLogMutationVariables>(CreateSenderRequestFromHttpRequestLogDocument, options);
|
||||
}
|
||||
export type CreateSenderRequestFromHttpRequestLogMutationHookResult = ReturnType<typeof useCreateSenderRequestFromHttpRequestLogMutation>;
|
||||
export type CreateSenderRequestFromHttpRequestLogMutationResult = Apollo.MutationResult<CreateSenderRequestFromHttpRequestLogMutation>;
|
||||
export type CreateSenderRequestFromHttpRequestLogMutationOptions = Apollo.BaseMutationOptions<CreateSenderRequestFromHttpRequestLogMutation, CreateSenderRequestFromHttpRequestLogMutationVariables>;
|
||||
export const SendRequestDocument = gql`
|
||||
mutation SendRequest($id: ID!) {
|
||||
sendRequest(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type SendRequestMutationFn = Apollo.MutationFunction<SendRequestMutation, SendRequestMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useSendRequestMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSendRequestMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSendRequestMutation` 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 [sendRequestMutation, { data, loading, error }] = useSendRequestMutation({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSendRequestMutation(baseOptions?: Apollo.MutationHookOptions<SendRequestMutation, SendRequestMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SendRequestMutation, SendRequestMutationVariables>(SendRequestDocument, options);
|
||||
}
|
||||
export type SendRequestMutationHookResult = ReturnType<typeof useSendRequestMutation>;
|
||||
export type SendRequestMutationResult = Apollo.MutationResult<SendRequestMutation>;
|
||||
export type SendRequestMutationOptions = Apollo.BaseMutationOptions<SendRequestMutation, SendRequestMutationVariables>;
|
||||
export const GetSenderRequestDocument = gql`
|
||||
query GetSenderRequest($id: ID!) {
|
||||
senderRequest(id: $id) {
|
||||
id
|
||||
sourceRequestLogID
|
||||
url
|
||||
method
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
body
|
||||
timestamp
|
||||
response {
|
||||
id
|
||||
proto
|
||||
statusCode
|
||||
statusReason
|
||||
body
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetSenderRequestQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetSenderRequestQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetSenderRequestQuery` 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 } = useGetSenderRequestQuery({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetSenderRequestQuery(baseOptions: Apollo.QueryHookOptions<GetSenderRequestQuery, GetSenderRequestQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetSenderRequestQuery, GetSenderRequestQueryVariables>(GetSenderRequestDocument, options);
|
||||
}
|
||||
export function useGetSenderRequestLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetSenderRequestQuery, GetSenderRequestQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetSenderRequestQuery, GetSenderRequestQueryVariables>(GetSenderRequestDocument, options);
|
||||
}
|
||||
export type GetSenderRequestQueryHookResult = ReturnType<typeof useGetSenderRequestQuery>;
|
||||
export type GetSenderRequestLazyQueryHookResult = ReturnType<typeof useGetSenderRequestLazyQuery>;
|
||||
export type GetSenderRequestQueryResult = Apollo.QueryResult<GetSenderRequestQuery, GetSenderRequestQueryVariables>;
|
||||
export const GetSenderRequestsDocument = gql`
|
||||
query GetSenderRequests {
|
||||
senderRequests {
|
||||
id
|
||||
url
|
||||
method
|
||||
response {
|
||||
id
|
||||
statusCode
|
||||
statusReason
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetSenderRequestsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetSenderRequestsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetSenderRequestsQuery` 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 } = useGetSenderRequestsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetSenderRequestsQuery(baseOptions?: Apollo.QueryHookOptions<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>(GetSenderRequestsDocument, options);
|
||||
}
|
||||
export function useGetSenderRequestsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>(GetSenderRequestsDocument, options);
|
||||
}
|
||||
export type GetSenderRequestsQueryHookResult = ReturnType<typeof useGetSenderRequestsQuery>;
|
||||
export type GetSenderRequestsLazyQueryHookResult = ReturnType<typeof useGetSenderRequestsLazyQuery>;
|
||||
export type GetSenderRequestsQueryResult = Apollo.QueryResult<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>;
|
@ -9,6 +9,7 @@ import { CacheProvider, EmotionCache } from "@emotion/react";
|
||||
import createEmotionCache from "../lib/createEmotionCache";
|
||||
import theme from "../lib/theme";
|
||||
import { useApollo } from "../lib/graphql";
|
||||
import "../styles.css";
|
||||
|
||||
// Client-side cache, shared for the whole session of the user in the browser.
|
||||
const clientSideEmotionCache = createEmotionCache();
|
||||
|
@ -1,11 +1,47 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { Box } from "@mui/system";
|
||||
import { AllotmentProps } from "allotment";
|
||||
import { PaneProps } from "allotment/dist/types/src/allotment";
|
||||
import { ComponentType, useEffect, useRef, useState } from "react";
|
||||
|
||||
import Layout, { Page } from "../../components/Layout";
|
||||
import EditRequest from "../../components/sender/EditRequest";
|
||||
import History from "../../components/sender/History";
|
||||
|
||||
function Index(): JSX.Element {
|
||||
const isMountedRef = useRef(false);
|
||||
const [Allotment, setAllotment] = useState<
|
||||
(ComponentType<AllotmentProps> & { Pane: ComponentType<PaneProps> }) | null
|
||||
>(null);
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
import("allotment")
|
||||
.then((mod) => {
|
||||
if (!isMountedRef.current) {
|
||||
return;
|
||||
}
|
||||
setAllotment(mod.Allotment);
|
||||
})
|
||||
.catch((err) => console.error(err, `could not import allotment ${err.message}`));
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
if (!Allotment) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout page={Page.Sender} title="Sender">
|
||||
<Typography paragraph>Coming soon…</Typography>
|
||||
<Allotment vertical={true} defaultSizes={[70, 30]}>
|
||||
<Box sx={{ pt: 0.75, height: "100%" }}>
|
||||
<EditRequest />
|
||||
</Box>
|
||||
<Box sx={{ height: "100%", py: 2, overflow: "hidden" }}>
|
||||
<Box sx={{ height: "100%", overflow: "scroll" }}>
|
||||
<History />
|
||||
</Box>
|
||||
</Box>
|
||||
</Allotment>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
5
admin/src/styles.css
Normal file
5
admin/src/styles.css
Normal file
@ -0,0 +1,5 @@
|
||||
html,
|
||||
body,
|
||||
#__next {
|
||||
height: 100%;
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"downlevelIteration": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
|
2870
admin/yarn.lock
2870
admin/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user