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:
51
admin/src/features/reqlog/components/ConfirmationDialog.tsx
Normal file
51
admin/src/features/reqlog/components/ConfirmationDialog.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import Button from "@mui/material/Button";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import React, { useState } from "react";
|
||||
|
||||
export function useConfirmationDialog() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const close = () => setIsOpen(false);
|
||||
const open = () => setIsOpen(true);
|
||||
|
||||
return { open, close, isOpen };
|
||||
}
|
||||
|
||||
interface ConfirmationDialog {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ConfirmationDialog(props: ConfirmationDialog) {
|
||||
const { onClose, onConfirm, isOpen, children } = props;
|
||||
|
||||
function confirm() {
|
||||
onConfirm();
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">{children}</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button onClick={confirm} autoFocus>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
104
admin/src/features/reqlog/components/HttpHeadersTable.tsx
Normal file
104
admin/src/features/reqlog/components/HttpHeadersTable.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import { Alert } from "@mui/lab";
|
||||
import { Table, TableBody, TableCell, TableContainer, TableRow, Snackbar, SxProps, Theme } from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
|
||||
const baseCellStyle: SxProps<Theme> = {
|
||||
px: 0,
|
||||
py: 0.33,
|
||||
verticalAlign: "top",
|
||||
border: "none",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
"&:hover": {
|
||||
color: "primary.main",
|
||||
whiteSpace: "inherit",
|
||||
overflow: "inherit",
|
||||
textOverflow: "inherit",
|
||||
cursor: "copy",
|
||||
},
|
||||
};
|
||||
|
||||
const keyCellStyle = {
|
||||
...baseCellStyle,
|
||||
pr: 1,
|
||||
width: "40%",
|
||||
fontWeight: "bold",
|
||||
fontSize: ".75rem",
|
||||
};
|
||||
|
||||
const valueCellStyle = {
|
||||
...baseCellStyle,
|
||||
width: "60%",
|
||||
border: "none",
|
||||
fontSize: ".75rem",
|
||||
};
|
||||
|
||||
interface Props {
|
||||
headers: Array<{ key: string; value: string }>;
|
||||
}
|
||||
|
||||
function HttpHeadersTable({ headers }: Props): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const windowSel = window.getSelection();
|
||||
|
||||
if (!windowSel || !document) {
|
||||
return;
|
||||
}
|
||||
|
||||
const r = document.createRange();
|
||||
r.selectNode(e.currentTarget);
|
||||
windowSel.removeAllRanges();
|
||||
windowSel.addRange(r);
|
||||
document.execCommand("copy");
|
||||
windowSel.removeAllRanges();
|
||||
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = (event: Event | React.SyntheticEvent, reason?: string) => {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Snackbar open={open} autoHideDuration={3000} onClose={handleClose}>
|
||||
<Alert onClose={handleClose} severity="info">
|
||||
Copied to clipboard.
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<TableContainer>
|
||||
<Table
|
||||
sx={{
|
||||
tableLayout: "fixed",
|
||||
width: "100%",
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<TableBody>
|
||||
{headers.map(({ key, value }, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell component="th" scope="row" sx={keyCellStyle} onClick={handleClick}>
|
||||
<code>{key}:</code>
|
||||
</TableCell>
|
||||
<TableCell sx={valueCellStyle} onClick={handleClick}>
|
||||
<code>{value}</code>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HttpHeadersTable;
|
59
admin/src/features/reqlog/components/LogDetail.tsx
Normal file
59
admin/src/features/reqlog/components/LogDetail.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import Alert from "@mui/lab/Alert";
|
||||
import { Box, Grid, Paper, CircularProgress } from "@mui/material";
|
||||
|
||||
import RequestDetail from "./RequestDetail";
|
||||
import ResponseDetail from "./ResponseDetail";
|
||||
|
||||
import { useHttpRequestLogQuery } from "lib/graphql/generated";
|
||||
|
||||
interface Props {
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
function LogDetail({ requestId: id }: Props): JSX.Element {
|
||||
const { loading, error, data } = useHttpRequestLogQuery({
|
||||
variables: { id },
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <CircularProgress />;
|
||||
}
|
||||
if (error) {
|
||||
return <Alert severity="error">Error fetching logs details: {error.message}</Alert>;
|
||||
}
|
||||
|
||||
if (data && !data.httpRequestLog) {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
Request <strong>{id}</strong> was not found.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data?.httpRequestLog) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
const httpRequestLog = data.httpRequestLog;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container item spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<Box component={Paper}>
|
||||
<RequestDetail request={httpRequestLog} />
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
{httpRequestLog.response && (
|
||||
<Box component={Paper}>
|
||||
<ResponseDetail response={httpRequestLog.response} />
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogDetail;
|
60
admin/src/features/reqlog/components/LogsOverview.tsx
Normal file
60
admin/src/features/reqlog/components/LogsOverview.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import Alert from "@mui/lab/Alert";
|
||||
import { Box, CircularProgress, Link as MaterialLink, Typography } from "@mui/material";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import LogDetail from "./LogDetail";
|
||||
import RequestList from "./RequestList";
|
||||
|
||||
import CenteredPaper from "lib/components/CenteredPaper";
|
||||
import { useHttpRequestLogsQuery } from "lib/graphql/generated";
|
||||
|
||||
export default function LogsOverview(): JSX.Element {
|
||||
const router = useRouter();
|
||||
const detailReqLogId = router.query.id as string | undefined;
|
||||
const { loading, error, data } = useHttpRequestLogsQuery({
|
||||
pollInterval: 1000,
|
||||
});
|
||||
|
||||
const handleLogClick = (reqId: string) => {
|
||||
router.push("/proxy/logs?id=" + reqId, undefined, {
|
||||
shallow: false,
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <CircularProgress />;
|
||||
}
|
||||
if (error) {
|
||||
if (error.graphQLErrors[0]?.extensions?.code === "no_active_project") {
|
||||
return (
|
||||
<Alert severity="info">
|
||||
There is no project active.{" "}
|
||||
<Link href="/projects" passHref>
|
||||
<MaterialLink color="primary">Create or open</MaterialLink>
|
||||
</Link>{" "}
|
||||
one first.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return <Alert severity="error">Error fetching logs: {error.message}</Alert>;
|
||||
}
|
||||
|
||||
const logs = data?.httpRequestLogs || [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Box mb={2}>
|
||||
<RequestList logs={logs} selectedReqLogId={detailReqLogId} onLogClick={handleLogClick} />
|
||||
</Box>
|
||||
<Box>
|
||||
{detailReqLogId && <LogDetail requestId={detailReqLogId} />}
|
||||
{logs.length !== 0 && !detailReqLogId && (
|
||||
<CenteredPaper>
|
||||
<Typography>Select a log entry…</Typography>
|
||||
</CenteredPaper>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
52
admin/src/features/reqlog/components/RequestDetail.tsx
Normal file
52
admin/src/features/reqlog/components/RequestDetail.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { Typography, Box, Divider } from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
import HttpHeadersTable from "./HttpHeadersTable";
|
||||
|
||||
import Editor from "lib/components/Editor";
|
||||
import { HttpRequestLogQuery } from "lib/graphql/generated";
|
||||
|
||||
interface Props {
|
||||
request: NonNullable<HttpRequestLogQuery["httpRequestLog"]>;
|
||||
}
|
||||
|
||||
function RequestDetail({ request }: Props): JSX.Element {
|
||||
const { method, url, proto, headers, body } = request;
|
||||
|
||||
const contentType = headers.find((header) => header.key === "Content-Type")?.value;
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Box p={2}>
|
||||
<Typography variant="overline" color="textSecondary" style={{ float: "right" }}>
|
||||
Request
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
width: "calc(100% - 80px)",
|
||||
fontSize: "1rem",
|
||||
wordBreak: "break-all",
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
variant="h6"
|
||||
>
|
||||
{method} {decodeURIComponent(parsedUrl.pathname + parsedUrl.search)}{" "}
|
||||
<Typography component="span" color="textSecondary" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
|
||||
{proto}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box p={2}>
|
||||
<HttpHeadersTable headers={headers} />
|
||||
</Box>
|
||||
|
||||
{body && <Editor content={body} contentType={contentType} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RequestDetail;
|
167
admin/src/features/reqlog/components/RequestList.tsx
Normal file
167
admin/src/features/reqlog/components/RequestList.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import {
|
||||
TableContainer,
|
||||
Paper,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Typography,
|
||||
Box,
|
||||
useTheme,
|
||||
MenuItem,
|
||||
Snackbar,
|
||||
Alert,
|
||||
Link,
|
||||
} from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import CenteredPaper from "lib/components/CenteredPaper";
|
||||
import HttpStatusIcon from "lib/components/HttpStatusIcon";
|
||||
import useContextMenu from "lib/components/useContextMenu";
|
||||
import { HttpRequestLogsQuery, useCreateSenderRequestFromHttpRequestLogMutation } from "lib/graphql/generated";
|
||||
|
||||
interface Props {
|
||||
logs: NonNullable<HttpRequestLogsQuery["httpRequestLogs"]>;
|
||||
selectedReqLogId?: string;
|
||||
onLogClick(requestId: string): void;
|
||||
}
|
||||
|
||||
export default function RequestList({ logs, onLogClick, selectedReqLogId }: Props): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<RequestListTable onLogClick={onLogClick} logs={logs} selectedReqLogId={selectedReqLogId} />
|
||||
{logs.length === 0 && (
|
||||
<Box my={1}>
|
||||
<CenteredPaper>
|
||||
<Typography>No logs found.</Typography>
|
||||
</CenteredPaper>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface RequestListTableProps {
|
||||
logs: HttpRequestLogsQuery["httpRequestLogs"];
|
||||
selectedReqLogId?: string;
|
||||
onLogClick(requestId: string): void;
|
||||
}
|
||||
|
||||
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] = useState("");
|
||||
const [copiedReqNotifOpen, setCopiedReqNotifOpen] = useState(false);
|
||||
const handleCloseCopiedNotif = (_: Event | React.SyntheticEvent, reason?: string) => {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
setCopiedReqNotifOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
<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);
|
||||
|
||||
const cellStyle = {
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
};
|
||||
|
||||
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 sx={{ ...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>
|
||||
);
|
||||
}
|
44
admin/src/features/reqlog/components/ResponseDetail.tsx
Normal file
44
admin/src/features/reqlog/components/ResponseDetail.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { Typography, Box, Divider } from "@mui/material";
|
||||
|
||||
import HttpHeadersTable from "./HttpHeadersTable";
|
||||
|
||||
import Editor from "lib/components/Editor";
|
||||
import HttpStatusIcon from "lib/components/HttpStatusIcon";
|
||||
import { HttpRequestLogQuery } from "lib/graphql/generated";
|
||||
|
||||
interface Props {
|
||||
response: NonNullable<NonNullable<HttpRequestLogQuery["httpRequestLog"]>["response"]>;
|
||||
}
|
||||
|
||||
function ResponseDetail({ response }: Props): JSX.Element {
|
||||
const contentType = response.headers.find((header) => header.key.toLowerCase() === "content-type")?.value;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Box p={2}>
|
||||
<Typography variant="overline" color="textSecondary" style={{ float: "right" }}>
|
||||
Response
|
||||
</Typography>
|
||||
<Typography variant="h6" style={{ fontSize: "1rem", whiteSpace: "nowrap" }}>
|
||||
<HttpStatusIcon status={response.statusCode} />{" "}
|
||||
<Typography component="span" color="textSecondary">
|
||||
<Typography component="span" color="textSecondary" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
|
||||
{response.proto}
|
||||
</Typography>
|
||||
</Typography>{" "}
|
||||
{response.statusCode} {response.statusReason}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box p={2}>
|
||||
<HttpHeadersTable headers={response.headers} />
|
||||
</Box>
|
||||
|
||||
{response.body && <Editor content={response.body} contentType={contentType} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResponseDetail;
|
196
admin/src/features/reqlog/components/Search.tsx
Normal file
196
admin/src/features/reqlog/components/Search.tsx
Normal file
@ -0,0 +1,196 @@
|
||||
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";
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
CircularProgress,
|
||||
ClickAwayListener,
|
||||
FormControlLabel,
|
||||
InputBase,
|
||||
Paper,
|
||||
Popper,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
import { ConfirmationDialog, useConfirmationDialog } from "./ConfirmationDialog";
|
||||
|
||||
import {
|
||||
HttpRequestLogFilterDocument,
|
||||
HttpRequestLogsDocument,
|
||||
useClearHttpRequestLogMutation,
|
||||
useHttpRequestLogFilterQuery,
|
||||
useSetHttpRequestLogFilterMutation,
|
||||
} from "lib/graphql/generated";
|
||||
import { withoutTypename } from "lib/graphql/omitTypename";
|
||||
|
||||
function Search(): JSX.Element {
|
||||
const theme = useTheme();
|
||||
|
||||
const [searchExpr, setSearchExpr] = useState("");
|
||||
const filterResult = useHttpRequestLogFilterQuery({
|
||||
onCompleted: (data) => {
|
||||
setSearchExpr(data.httpRequestLogFilter?.searchExpression || "");
|
||||
},
|
||||
});
|
||||
const filter = filterResult.data?.httpRequestLogFilter;
|
||||
|
||||
const [setFilterMutate, setFilterResult] = useSetHttpRequestLogFilterMutation({
|
||||
update(cache, { data }) {
|
||||
cache.writeQuery({
|
||||
query: HttpRequestLogFilterDocument,
|
||||
data: {
|
||||
httpRequestLogFilter: data?.setHttpRequestLogFilter,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const [clearHTTPRequestLog, clearHTTPRequestLogResult] = useClearHttpRequestLogMutation({
|
||||
refetchQueries: [{ query: HttpRequestLogsDocument }],
|
||||
});
|
||||
const clearHTTPConfirmationDialog = useConfirmationDialog();
|
||||
|
||||
const filterRef = useRef<HTMLFormElement>(null);
|
||||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.SyntheticEvent) => {
|
||||
setFilterMutate({
|
||||
variables: {
|
||||
filter: {
|
||||
...withoutTypename(filter),
|
||||
searchExpression: searchExpr,
|
||||
},
|
||||
},
|
||||
});
|
||||
setFilterOpen(false);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleClickAway = (event: MouseEvent | TouchEvent) => {
|
||||
if (filterRef?.current && filterRef.current.contains(event.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
setFilterOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<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
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
ref={filterRef}
|
||||
sx={{
|
||||
padding: "2px 4px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: 400,
|
||||
}}
|
||||
>
|
||||
<Tooltip title="Toggle filter options">
|
||||
<IconButton
|
||||
onClick={() => setFilterOpen(!filterOpen)}
|
||||
sx={{
|
||||
p: 1,
|
||||
color: filter?.onlyInScope ? "primary.main" : "inherit",
|
||||
}}
|
||||
>
|
||||
{filterResult.loading || setFilterResult.loading ? (
|
||||
<CircularProgress sx={{ color: theme.palette.text.primary }} size={23} />
|
||||
) : (
|
||||
<FilterListIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<InputBase
|
||||
sx={{
|
||||
ml: 1,
|
||||
flex: 1,
|
||||
}}
|
||||
placeholder="Search proxy logs…"
|
||||
value={searchExpr}
|
||||
onChange={(e) => setSearchExpr(e.target.value)}
|
||||
onFocus={() => setFilterOpen(true)}
|
||||
/>
|
||||
<Tooltip title="Search">
|
||||
<IconButton type="submit" sx={{ padding: 1.25 }}>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Popper
|
||||
open={filterOpen}
|
||||
anchorEl={filterRef.current}
|
||||
placement="bottom"
|
||||
style={{ zIndex: theme.zIndex.appBar }}
|
||||
>
|
||||
<Paper
|
||||
sx={{
|
||||
width: 400,
|
||||
marginTop: 0.5,
|
||||
p: 1.5,
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={filter?.onlyInScope ? true : false}
|
||||
disabled={filterResult.loading || setFilterResult.loading}
|
||||
onChange={(e) =>
|
||||
setFilterMutate({
|
||||
variables: {
|
||||
filter: {
|
||||
...withoutTypename(filter),
|
||||
onlyInScope: e.target.checked,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Only show in-scope requests"
|
||||
/>
|
||||
</Paper>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
function Error(props: { prefix: string; error?: Error }) {
|
||||
if (!props.error) return null;
|
||||
|
||||
return (
|
||||
<Box mb={4}>
|
||||
<Alert severity="error">
|
||||
{props.prefix}: {props.error.message}
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Search;
|
@ -0,0 +1,5 @@
|
||||
mutation ClearHTTPRequestLog {
|
||||
clearHTTPRequestLog {
|
||||
success
|
||||
}
|
||||
}
|
23
admin/src/features/reqlog/graphql/httpRequestLog.graphql
Normal file
23
admin/src/features/reqlog/graphql/httpRequestLog.graphql
Normal file
@ -0,0 +1,23 @@
|
||||
query HttpRequestLog($id: ID!) {
|
||||
httpRequestLog(id: $id) {
|
||||
id
|
||||
method
|
||||
url
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
body
|
||||
response {
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
statusCode
|
||||
statusReason
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
query HttpRequestLogFilter {
|
||||
httpRequestLogFilter {
|
||||
onlyInScope
|
||||
searchExpression
|
||||
}
|
||||
}
|
12
admin/src/features/reqlog/graphql/httpRequestLogs.graphql
Normal file
12
admin/src/features/reqlog/graphql/httpRequestLogs.graphql
Normal file
@ -0,0 +1,12 @@
|
||||
query HttpRequestLogs {
|
||||
httpRequestLogs {
|
||||
id
|
||||
method
|
||||
url
|
||||
timestamp
|
||||
response {
|
||||
statusCode
|
||||
statusReason
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
mutation SetHttpRequestLogFilter($filter: HttpRequestLogFilterInput) {
|
||||
setHttpRequestLogFilter(filter: $filter) {
|
||||
onlyInScope
|
||||
searchExpression
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user