Update Next.js, Material UI

This commit is contained in:
David Stotijn
2022-01-28 20:20:15 +01:00
parent 73ebb89863
commit aa8ddf4122
42 changed files with 2777 additions and 6026 deletions

View File

@ -1,10 +1,10 @@
import React, { useState } from "react";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
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";
export function useConfirmationDialog() {
const [isOpen, setIsOpen] = useState(false);
@ -38,12 +38,10 @@ export function ConfirmationDialog(props: ConfirmationDialog) {
>
<DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{children}
</DialogContentText>
<DialogContentText id="alert-dialog-description">{children}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Abort</Button>
<Button onClick={onClose}>Cancel</Button>
<Button onClick={confirm} autoFocus>
Confirm
</Button>

View File

@ -1,7 +1,7 @@
import dynamic from "next/dynamic";
const MonacoEditor = dynamic(import("react-monaco-editor"), { ssr: false });
import MonacoEditor from "@monaco-editor/react";
import monaco from "monaco-editor/esm/vs/editor/editor.api";
const monacoOptions = {
const monacoOptions: monaco.editor.IEditorOptions = {
readOnly: true,
wordWrap: "on",
minimap: {
@ -11,20 +11,7 @@ const monacoOptions = {
type language = "html" | "typescript" | "json";
function editorDidMount() {
return ((window as any).MonacoEnvironment.getWorkerUrl = (
moduleId,
label
) => {
if (label === "json") return "/_next/static/json.worker.js";
if (label === "html") return "/_next/static/html.worker.js";
if (label === "javascript") return "/_next/static/ts.worker.js";
return "/_next/static/editor.worker.js";
});
}
function languageForContentType(contentType: string): language {
function languageForContentType(contentType?: string): language | undefined {
switch (contentType) {
case "text/html":
return "html";
@ -41,7 +28,7 @@ function languageForContentType(contentType: string): language {
interface Props {
content: string;
contentType: string;
contentType?: string;
}
function Editor({ content, contentType }: Props): JSX.Element {
@ -50,8 +37,7 @@ function Editor({ content, contentType }: Props): JSX.Element {
height={"600px"}
language={languageForContentType(contentType)}
theme="vs-dark"
editorDidMount={editorDidMount}
options={monacoOptions as any}
options={monacoOptions}
value={content}
/>
);

View File

@ -1,83 +1,66 @@
import {
makeStyles,
Theme,
createStyles,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
Snackbar,
} from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { Table, TableBody, TableCell, TableContainer, TableRow, Snackbar } from "@mui/material";
import { Alert } from "@mui/lab";
import React, { useState } from "react";
const useStyles = makeStyles((theme: Theme) => {
const paddingX = 0;
const paddingY = theme.spacing(1) / 3;
const tableCell = {
paddingLeft: paddingX,
paddingRight: paddingX,
paddingTop: paddingY,
paddingBottom: paddingY,
verticalAlign: "top",
border: "none",
whiteSpace: "nowrap" as any,
overflow: "hidden",
textOverflow: "ellipsis",
"&:hover": {
color: theme.palette.secondary.main,
whiteSpace: "inherit" as any,
overflow: "inherit",
textOverflow: "inherit",
cursor: "copy",
},
};
return createStyles({
root: {},
table: {
tableLayout: "fixed",
width: "100%",
},
keyCell: {
...tableCell,
paddingRight: theme.spacing(1),
width: "40%",
fontWeight: "bold",
fontSize: ".75rem",
},
valueCell: {
...tableCell,
width: "60%",
border: "none",
fontSize: ".75rem",
},
});
});
const baseCellStyle = {
px: 0,
py: 0.33,
verticalAlign: "top",
border: "none",
whiteSpace: "nowrap" as any,
overflow: "hidden",
textOverflow: "ellipsis",
"&:hover": {
color: "primary.main",
whiteSpace: "inherit" as any,
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 classes = useStyles();
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);
window.getSelection().removeAllRanges();
window.getSelection().addRange(r);
windowSel.removeAllRanges();
windowSel.addRange(r);
document.execCommand("copy");
window.getSelection().removeAllRanges();
windowSel.removeAllRanges();
setOpen(true);
};
const handleClose = (event?: React.SyntheticEvent, reason?: string) => {
const handleClose = (event: Event | React.SyntheticEvent, reason?: string) => {
if (reason === "clickaway") {
return;
}
@ -92,20 +75,21 @@ function HttpHeadersTable({ headers }: Props): JSX.Element {
Copied to clipboard.
</Alert>
</Snackbar>
<TableContainer className={classes.root}>
<Table className={classes.table} size="small">
<TableContainer>
<Table
sx={{
tableLayout: "fixed",
width: "100%",
}}
size="small"
>
<TableBody>
{headers.map(({ key, value }, index) => (
<TableRow key={index}>
<TableCell
component="th"
scope="row"
className={classes.keyCell}
onClick={handleClick}
>
<TableCell component="th" scope="row" sx={keyCellStyle} onClick={handleClick}>
<code>{key}:</code>
</TableCell>
<TableCell className={classes.valueCell} onClick={handleClick}>
<TableCell sx={valueCellStyle} onClick={handleClick}>
<code>{value}</code>
</TableCell>
</TableRow>

View File

@ -1,31 +1,25 @@
import { Theme, withTheme } from "@material-ui/core";
import { orange, red } from "@material-ui/core/colors";
import FiberManualRecordIcon from "@material-ui/icons/FiberManualRecord";
import { SvgIconTypeMap } from "@mui/material";
import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
interface Props {
status: number;
theme: Theme;
}
function HttpStatusIcon({ status, theme }: Props): JSX.Element {
const style = { marginTop: "-.25rem", verticalAlign: "middle" };
export default function HttpStatusIcon({ status }: Props): JSX.Element {
let color: SvgIconTypeMap["props"]["color"] = "inherit";
switch (Math.floor(status / 100)) {
case 2:
case 3:
return (
<FiberManualRecordIcon
style={{ ...style, color: theme.palette.secondary.main }}
/>
);
color = "primary";
break;
case 4:
return (
<FiberManualRecordIcon style={{ ...style, color: orange["A400"] }} />
);
color = "warning";
break;
case 5:
return <FiberManualRecordIcon style={{ ...style, color: red["A400"] }} />;
default:
return <FiberManualRecordIcon style={style} />;
color = "error";
break;
}
}
export default withTheme(HttpStatusIcon);
return <FiberManualRecordIcon sx={{ marginTop: "-.25rem", verticalAlign: "middle" }} color={color} />;
}

View File

@ -1,9 +1,9 @@
import { gql, useQuery } from "@apollo/client";
import { Box, Grid, Paper, CircularProgress } from "@material-ui/core";
import { Box, Grid, Paper, CircularProgress } from "@mui/material";
import ResponseDetail from "./ResponseDetail";
import RequestDetail from "./RequestDetail";
import Alert from "@material-ui/lab/Alert";
import Alert from "@mui/lab/Alert";
const HTTP_REQUEST_LOG = gql`
query HttpRequestLog($id: ID!) {
@ -44,11 +44,7 @@ function LogDetail({ requestId: id }: Props): JSX.Element {
return <CircularProgress />;
}
if (error) {
return (
<Alert severity="error">
Error fetching logs details: {error.message}
</Alert>
);
return <Alert severity="error">Error fetching logs details: {error.message}</Alert>;
}
if (!data.httpRequestLog) {

View File

@ -1,12 +1,7 @@
import { useRouter } from "next/router";
import Link from "next/link";
import {
Box,
CircularProgress,
Link as MaterialLink,
Typography,
} from "@material-ui/core";
import Alert from "@material-ui/lab/Alert";
import { Box, CircularProgress, Link as MaterialLink, Typography } from "@mui/material";
import Alert from "@mui/lab/Alert";
import RequestList from "./RequestList";
import LogDetail from "./LogDetail";
@ -33,7 +28,7 @@ function LogsOverview(): JSX.Element {
<Alert severity="info">
There is no project active.{" "}
<Link href="/projects" passHref>
<MaterialLink color="secondary">Create or open</MaterialLink>
<MaterialLink color="primary">Create or open</MaterialLink>
</Link>{" "}
one first.
</Alert>
@ -47,11 +42,7 @@ function LogsOverview(): JSX.Element {
return (
<div>
<Box mb={2}>
<RequestList
logs={logs}
selectedReqLogId={detailReqLogId}
onLogClick={handleLogClick}
/>
<RequestList logs={logs || []} selectedReqLogId={detailReqLogId} onLogClick={handleLogClick} />
</Box>
<Box>
{detailReqLogId && <LogDetail requestId={detailReqLogId} />}

View File

@ -1,42 +1,9 @@
import React from "react";
import {
Typography,
Box,
createStyles,
makeStyles,
Theme,
Divider,
} from "@material-ui/core";
import { Typography, Box, Divider } from "@mui/material";
import HttpHeadersTable from "./HttpHeadersTable";
import Editor from "./Editor";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
requestTitle: {
width: "calc(100% - 80px)",
fontSize: "1rem",
wordBreak: "break-all",
whiteSpace: "pre-wrap",
},
headersTable: {
tableLayout: "fixed",
width: "100%",
},
headerKeyCell: {
verticalAlign: "top",
width: "30%",
fontWeight: "bold",
},
headerValueCell: {
width: "70%",
verticalAlign: "top",
wordBreak: "break-all",
whiteSpace: "pre-wrap",
},
})
);
interface Props {
request: {
method: string;
@ -49,29 +16,27 @@ interface Props {
function RequestDetail({ request }: Props): JSX.Element {
const { method, url, proto, headers, body } = request;
const classes = useStyles();
const contentType = headers.find((header) => header.key === "Content-Type")
?.value;
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" }}
>
<Typography variant="overline" color="textSecondary" style={{ float: "right" }}>
Request
</Typography>
<Typography className={classes.requestTitle} variant="h6">
<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" }}
>
<Typography component="span" color="textSecondary" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
{proto}
</Typography>
</Typography>

View File

@ -8,48 +8,23 @@ import {
TableBody,
Typography,
Box,
createStyles,
makeStyles,
Theme,
withTheme,
} from "@material-ui/core";
useTheme,
} from "@mui/material";
import HttpStatusIcon from "./HttpStatusCode";
import CenteredPaper from "../CenteredPaper";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
row: {
"&:hover": {
cursor: "pointer",
},
},
/* Pseudo-class applied to the root element if `hover={true}`. */
hover: {},
})
);
import { RequestLog } from "../../lib/requestLogs";
interface Props {
logs: Array<any>;
logs: RequestLog[];
selectedReqLogId?: string;
onLogClick(requestId: string): void;
theme: Theme;
}
function RequestList({
logs,
onLogClick,
selectedReqLogId,
theme,
}: Props): JSX.Element {
export default function RequestList({ logs, onLogClick, selectedReqLogId }: Props): JSX.Element {
return (
<div>
<RequestListTable
onLogClick={onLogClick}
logs={logs}
selectedReqLogId={selectedReqLogId}
theme={theme}
/>
<RequestListTable onLogClick={onLogClick} logs={logs} selectedReqLogId={selectedReqLogId} />
{logs.length === 0 && (
<Box my={1}>
<CenteredPaper>
@ -62,19 +37,14 @@ function RequestList({
}
interface RequestListTableProps {
logs?: any;
logs: RequestLog[];
selectedReqLogId?: string;
onLogClick(requestId: string): void;
theme: Theme;
}
function RequestListTable({
logs,
selectedReqLogId,
onLogClick,
theme,
}: RequestListTableProps): JSX.Element {
const classes = useStyles();
function RequestListTable({ logs, selectedReqLogId, onLogClick }: RequestListTableProps): JSX.Element {
const theme = useTheme();
return (
<TableContainer
component={Paper}
@ -102,26 +72,25 @@ function RequestListTable({
textOverflow: "ellipsis",
} as any;
const rowStyle = {
backgroundColor:
id === selectedReqLogId && theme.palette.action.selected,
};
return (
<TableRow
key={id}
className={classes.row}
style={rowStyle}
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 style={{ ...cellStyle, maxWidth: "100px" }}>
{origin}
</TableCell>
<TableCell style={{ ...cellStyle, maxWidth: "200px" }}>
<TableCell sx={{ ...cellStyle, maxWidth: "100px" }}>{origin}</TableCell>
<TableCell sx={{ ...cellStyle, maxWidth: "200px" }}>
{decodeURIComponent(pathname + search + hash)}
</TableCell>
<TableCell style={{ maxWidth: "100px" }}>
@ -142,5 +111,3 @@ function RequestListTable({
</TableContainer>
);
}
export default withTheme(RequestList);

View File

@ -1,4 +1,4 @@
import { Typography, Box, Divider } from "@material-ui/core";
import { Typography, Box, Divider } from "@mui/material";
import HttpStatusIcon from "./HttpStatusCode";
import Editor from "./Editor";
@ -15,30 +15,17 @@ interface Props {
}
function ResponseDetail({ response }: Props): JSX.Element {
const contentType = response.headers.find(
(header) => header.key === "Content-Type"
)?.value;
const contentType = response.headers.find((header) => header.key === "Content-Type")?.value;
return (
<div>
<Box p={2}>
<Typography
variant="overline"
color="textSecondary"
style={{ float: "right" }}
>
<Typography variant="overline" color="textSecondary" style={{ float: "right" }}>
Response
</Typography>
<Typography
variant="h6"
style={{ fontSize: "1rem", whiteSpace: "nowrap" }}
>
<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" }}
>
<Typography component="span" color="textSecondary" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
{response.proto}
</Typography>
</Typography>{" "}
@ -52,9 +39,7 @@ function ResponseDetail({ response }: Props): JSX.Element {
<HttpHeadersTable headers={response.headers} />
</Box>
{response.body && (
<Editor content={response.body} contentType={contentType} />
)}
{response.body && <Editor content={response.body} contentType={contentType} />}
</div>
);
}

View File

@ -3,29 +3,23 @@ import {
Checkbox,
CircularProgress,
ClickAwayListener,
createStyles,
FormControlLabel,
InputBase,
makeStyles,
Paper,
Popper,
Theme,
Tooltip,
useTheme,
} from "@material-ui/core";
import IconButton from "@material-ui/core/IconButton";
import SearchIcon from "@material-ui/icons/Search";
import FilterListIcon from "@material-ui/icons/FilterList";
import DeleteIcon from "@material-ui/icons/Delete";
} from "@mui/material";
import IconButton from "@mui/material/IconButton";
import SearchIcon from "@mui/icons-material/Search";
import FilterListIcon from "@mui/icons-material/FilterList";
import DeleteIcon from "@mui/icons-material/Delete";
import React, { useRef, useState } from "react";
import { gql, useMutation, useQuery } from "@apollo/client";
import { withoutTypename } from "../../lib/omitTypename";
import { Alert } from "@material-ui/lab";
import { Alert } from "@mui/lab";
import { useClearHTTPRequestLog } from "./hooks/useClearHTTPRequestLog";
import {
ConfirmationDialog,
useConfirmationDialog,
} from "./ConfirmationDialog";
import { ConfirmationDialog, useConfirmationDialog } from "./ConfirmationDialog";
const FILTER = gql`
query HttpRequestLogFilter {
@ -45,79 +39,43 @@ const SET_FILTER = gql`
}
`;
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
padding: "2px 4px",
display: "flex",
alignItems: "center",
width: 400,
},
input: {
marginLeft: theme.spacing(1),
flex: 1,
},
iconButton: {
padding: 10,
},
filterPopper: {
width: 400,
marginTop: 6,
zIndex: 99,
},
filterOptions: {
padding: theme.spacing(2),
},
filterLoading: {
marginRight: 1,
color: theme.palette.text.primary,
},
})
);
export interface SearchFilter {
onlyInScope: boolean;
searchExpression: string;
}
function Search(): JSX.Element {
const classes = useStyles();
const theme = useTheme();
const [searchExpr, setSearchExpr] = useState("");
const { loading: filterLoading, error: filterErr, data: filter } = useQuery(
FILTER,
{
onCompleted: (data) => {
setSearchExpr(data.httpRequestLogFilter?.searchExpression || "");
},
}
);
const {
loading: filterLoading,
error: filterErr,
data: filter,
} = useQuery(FILTER, {
onCompleted: (data) => {
setSearchExpr(data.httpRequestLogFilter?.searchExpression || "");
},
});
const [
setFilterMutate,
{ error: setFilterErr, loading: setFilterLoading },
] = useMutation<{
const [setFilterMutate, { error: setFilterErr, loading: setFilterLoading }] = useMutation<{
setHttpRequestLogFilter: SearchFilter | null;
}>(SET_FILTER, {
update(cache, { data: { setHttpRequestLogFilter } }) {
update(cache, { data }) {
cache.writeQuery({
query: FILTER,
data: {
httpRequestLogFilter: setHttpRequestLogFilter,
httpRequestLogFilter: data?.setHttpRequestLogFilter,
},
});
},
onError: () => {},
});
const [
clearHTTPRequestLog,
clearHTTPRequestLogResult,
] = useClearHTTPRequestLog();
const [clearHTTPRequestLog, clearHTTPRequestLogResult] = useClearHTTPRequestLog();
const clearHTTPConfirmationDialog = useConfirmationDialog();
const filterRef = useRef<HTMLElement | null>();
const filterRef = useRef<HTMLFormElement>(null);
const [filterOpen, setFilterOpen] = useState(false);
const handleSubmit = (e: React.SyntheticEvent) => {
@ -133,8 +91,8 @@ function Search(): JSX.Element {
e.preventDefault();
};
const handleClickAway = (event: React.MouseEvent<EventTarget>) => {
if (filterRef.current.contains(event.target as HTMLElement)) {
const handleClickAway = (event: MouseEvent | TouchEvent) => {
if (filterRef?.current && filterRef.current.contains(event.target as HTMLElement)) {
return;
}
setFilterOpen(false);
@ -144,63 +102,67 @@ function Search(): JSX.Element {
<Box>
<Error prefix="Error fetching filter" error={filterErr} />
<Error prefix="Error setting filter" error={setFilterErr} />
<Error
prefix="Error clearing all HTTP logs"
error={clearHTTPRequestLogResult.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}
className={classes.root}
sx={{
padding: "2px 4px",
display: "flex",
alignItems: "center",
width: 400,
}}
>
<Tooltip title="Toggle filter options">
<IconButton
className={classes.iconButton}
onClick={() => setFilterOpen(!filterOpen)}
style={{
color: filter?.httpRequestLogFilter?.onlyInScope
? theme.palette.secondary.main
: "inherit",
sx={{
p: 1,
color: filter?.httpRequestLogFilter?.onlyInScope ? "primary.main" : "inherit",
}}
>
{filterLoading || setFilterLoading ? (
<CircularProgress
className={classes.filterLoading}
size={23}
/>
<CircularProgress sx={{ color: theme.palette.text.primary }} size={23} />
) : (
<FilterListIcon />
)}
</IconButton>
</Tooltip>
<InputBase
className={classes.input}
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" className={classes.iconButton}>
<IconButton type="submit" sx={{ padding: 1.25 }}>
<SearchIcon />
</IconButton>
</Tooltip>
<Popper
className={classes.filterPopper}
open={filterOpen}
anchorEl={filterRef.current}
placement="bottom-start"
placement="bottom"
style={{ zIndex: theme.zIndex.appBar }}
>
<Paper className={classes.filterOptions}>
<Paper
sx={{
width: 400,
marginTop: 0.5,
p: 1.5,
}}
>
<FormControlLabel
control={
<Checkbox
checked={
filter?.httpRequestLogFilter?.onlyInScope ? true : false
}
checked={filter?.httpRequestLogFilter?.onlyInScope ? true : false}
disabled={filterLoading || setFilterLoading}
onChange={(e) =>
setFilterMutate({