mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Update Next.js, Material UI
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
- make clean
|
- make clean
|
||||||
- make build-admin
|
- sh -c "NEXT_PUBLIC_VERSION={{ .Version}} make build-admin"
|
||||||
- go mod tidy
|
- go mod tidy
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
|
6
admin/.eslintrc.json
Normal file
6
admin/.eslintrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals",
|
||||||
|
"rules": {
|
||||||
|
"@next/next/no-css-tags": "off"
|
||||||
|
}
|
||||||
|
}
|
4
admin/.prettierignore
Normal file
4
admin/.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
/build
|
||||||
|
/coverage
|
3
admin/.prettierrc.json
Normal file
3
admin/.prettierrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
5
admin/next-env.d.ts
vendored
5
admin/next-env.d.ts
vendored
@ -1,2 +1,5 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
const withCSS = require("@zeit/next-css");
|
// @ts-check
|
||||||
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
|
|
||||||
|
|
||||||
module.exports = withCSS({
|
/**
|
||||||
|
* @type {import('next').NextConfig}
|
||||||
|
**/
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
trailingSlash: true,
|
trailingSlash: true,
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
@ -11,24 +14,6 @@ module.exports = withCSS({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
webpack: (config) => {
|
};
|
||||||
config.module.rules.push({
|
|
||||||
test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/,
|
|
||||||
use: {
|
|
||||||
loader: "url-loader",
|
|
||||||
options: {
|
|
||||||
limit: 100000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
config.plugins.push(
|
module.exports = nextConfig;
|
||||||
new MonacoWebpackPlugin({
|
|
||||||
languages: ["html", "json", "javascript"],
|
|
||||||
filename: "static/[name].worker.js",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
@ -6,31 +6,38 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
"export": "next build && next export -o dist"
|
"export": "next build && next export -o dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.2.0",
|
"@apollo/client": "^3.2.0",
|
||||||
"@material-ui/core": "^4.11.0",
|
"@emotion/react": "^11.7.1",
|
||||||
"@material-ui/icons": "^4.9.1",
|
"@emotion/server": "^11.4.0",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.56",
|
"@emotion/styled": "^11.6.0",
|
||||||
"@zeit/next-css": "^1.0.1",
|
"@monaco-editor/react": "^4.3.1",
|
||||||
"graphql": "^15.3.0",
|
"@mui/icons-material": "^5.3.1",
|
||||||
"monaco-editor": "^0.20.0",
|
"@mui/lab": "^5.0.0-alpha.66",
|
||||||
"monaco-editor-webpack-plugin": "^1.9.0",
|
"@mui/material": "^5.3.1",
|
||||||
"next": "^9.5.4",
|
"deepmerge": "^4.2.2",
|
||||||
|
"graphql": "^16.2.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"monaco-editor": "^0.31.1",
|
||||||
|
"next": "^12.0.8",
|
||||||
"next-fonts": "^1.0.3",
|
"next-fonts": "^1.0.3",
|
||||||
"react": "^16.13.1",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^17.0.2",
|
||||||
"react-monaco-editor": "^0.34.0",
|
|
||||||
"react-syntax-highlighter": "^13.5.3",
|
|
||||||
"typescript": "^4.0.3"
|
"typescript": "^4.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^14.11.1",
|
"@babel/core": "^7.0.0",
|
||||||
"@types/react": "^16.9.49",
|
"@types/lodash": "^4.14.178",
|
||||||
"eslint": "^7.9.0",
|
"@types/node": "^17.0.12",
|
||||||
"eslint-config-prettier": "^6.11.0",
|
"@types/react": "^17.0.38",
|
||||||
"eslint-plugin-prettier": "^3.1.4",
|
"eslint": "^8.7.0",
|
||||||
"prettier": "^2.1.2"
|
"eslint-config-next": "12.0.8",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"prettier": "^2.1.2",
|
||||||
|
"webpack": "^5.67.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import { Paper } from "@material-ui/core";
|
import { Paper } from "@mui/material";
|
||||||
|
|
||||||
function CenteredPaper({
|
function CenteredPaper({ children }: { children: React.ReactNode }): JSX.Element {
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}): JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Paper
|
<Paper
|
||||||
|
@ -1,31 +1,31 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
makeStyles,
|
|
||||||
Theme,
|
Theme,
|
||||||
createStyles,
|
|
||||||
useTheme,
|
useTheme,
|
||||||
AppBar,
|
|
||||||
Toolbar,
|
Toolbar,
|
||||||
IconButton,
|
IconButton,
|
||||||
Typography,
|
Typography,
|
||||||
Drawer,
|
|
||||||
Divider,
|
Divider,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
|
||||||
ListItemIcon,
|
|
||||||
ListItemText,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@material-ui/core";
|
styled,
|
||||||
|
CSSObject,
|
||||||
|
Box,
|
||||||
|
ListItemText,
|
||||||
|
} from "@mui/material";
|
||||||
|
import MuiAppBar, { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar";
|
||||||
|
import MuiDrawer from "@mui/material/Drawer";
|
||||||
|
import MuiListItemButton, { ListItemButtonProps } from "@mui/material/ListItemButton";
|
||||||
|
import MuiListItemIcon, { ListItemIconProps } from "@mui/material/ListItemIcon";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import MenuIcon from "@material-ui/icons/Menu";
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import HomeIcon from "@material-ui/icons/Home";
|
import HomeIcon from "@mui/icons-material/Home";
|
||||||
import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet";
|
import SettingsEthernetIcon from "@mui/icons-material/SettingsEthernet";
|
||||||
import SendIcon from "@material-ui/icons/Send";
|
import SendIcon from "@mui/icons-material/Send";
|
||||||
import FolderIcon from "@material-ui/icons/Folder";
|
import FolderIcon from "@mui/icons-material/Folder";
|
||||||
import LocationSearchingIcon from "@material-ui/icons/LocationSearching";
|
import LocationSearchingIcon from "@mui/icons-material/LocationSearching";
|
||||||
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
|
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||||
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
|
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
export enum Page {
|
export enum Page {
|
||||||
Home,
|
Home,
|
||||||
@ -39,85 +39,91 @@ export enum Page {
|
|||||||
|
|
||||||
const drawerWidth = 240;
|
const drawerWidth = 240;
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const openedMixin = (theme: Theme): CSSObject => ({
|
||||||
createStyles({
|
width: drawerWidth,
|
||||||
root: {
|
transition: theme.transitions.create("width", {
|
||||||
display: "flex",
|
easing: theme.transitions.easing.sharp,
|
||||||
width: "100%",
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
},
|
}),
|
||||||
appBar: {
|
overflowX: "hidden",
|
||||||
zIndex: theme.zIndex.drawer + 1,
|
});
|
||||||
transition: theme.transitions.create(["width", "margin"], {
|
|
||||||
easing: theme.transitions.easing.sharp,
|
const closedMixin = (theme: Theme): CSSObject => ({
|
||||||
duration: theme.transitions.duration.leavingScreen,
|
transition: theme.transitions.create("width", {
|
||||||
}),
|
easing: theme.transitions.easing.sharp,
|
||||||
},
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
appBarShift: {
|
}),
|
||||||
marginLeft: drawerWidth,
|
overflowX: "hidden",
|
||||||
width: `calc(100% - ${drawerWidth}px)`,
|
width: 56,
|
||||||
transition: theme.transitions.create(["width", "margin"], {
|
});
|
||||||
easing: theme.transitions.easing.sharp,
|
|
||||||
duration: theme.transitions.duration.enteringScreen,
|
const DrawerHeader = styled("div")(({ theme }) => ({
|
||||||
}),
|
display: "flex",
|
||||||
},
|
alignItems: "center",
|
||||||
menuButton: {
|
justifyContent: "flex-start",
|
||||||
marginRight: 28,
|
padding: theme.spacing(0, 1),
|
||||||
},
|
// necessary for content to be below app bar
|
||||||
hide: {
|
...theme.mixins.toolbar,
|
||||||
display: "none",
|
}));
|
||||||
},
|
|
||||||
drawer: {
|
interface AppBarProps extends MuiAppBarProps {
|
||||||
width: drawerWidth,
|
open?: boolean;
|
||||||
flexShrink: 0,
|
}
|
||||||
whiteSpace: "nowrap",
|
|
||||||
},
|
const AppBar = styled(MuiAppBar, {
|
||||||
drawerOpen: {
|
shouldForwardProp: (prop) => prop !== "open",
|
||||||
width: drawerWidth,
|
})<AppBarProps>(({ theme, open }) => ({
|
||||||
transition: theme.transitions.create("width", {
|
backgroundColor: theme.palette.secondary.dark,
|
||||||
easing: theme.transitions.easing.sharp,
|
zIndex: theme.zIndex.drawer + 1,
|
||||||
duration: theme.transitions.duration.enteringScreen,
|
transition: theme.transitions.create(["width", "margin"], {
|
||||||
}),
|
easing: theme.transitions.easing.sharp,
|
||||||
},
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
drawerClose: {
|
}),
|
||||||
transition: theme.transitions.create("width", {
|
...(open && {
|
||||||
easing: theme.transitions.easing.sharp,
|
marginLeft: drawerWidth,
|
||||||
duration: theme.transitions.duration.leavingScreen,
|
width: `calc(100% - ${drawerWidth}px)`,
|
||||||
}),
|
transition: theme.transitions.create(["width", "margin"], {
|
||||||
overflowX: "hidden",
|
easing: theme.transitions.easing.sharp,
|
||||||
width: theme.spacing(7) + 1,
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
[theme.breakpoints.up("sm")]: {
|
}),
|
||||||
width: theme.spacing(7) + 8,
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== "open" })(({ theme, open }) => ({
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
...(open && {
|
||||||
|
...openedMixin(theme),
|
||||||
|
"& .MuiDrawer-paper": openedMixin(theme),
|
||||||
|
}),
|
||||||
|
...(!open && {
|
||||||
|
...closedMixin(theme),
|
||||||
|
"& .MuiDrawer-paper": closedMixin(theme),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ListItemButton = styled(MuiListItemButton)<ListItemButtonProps>(({ theme }) => ({
|
||||||
|
[theme.breakpoints.up("sm")]: {
|
||||||
|
px: 1,
|
||||||
|
},
|
||||||
|
"&.MuiListItemButton-root": {
|
||||||
|
"&.Mui-selected": {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
"& .MuiListItemIcon-root": {
|
||||||
|
color: theme.palette.secondary.dark,
|
||||||
|
},
|
||||||
|
"& .MuiListItemText-root": {
|
||||||
|
color: theme.palette.secondary.dark,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
toolbar: {
|
},
|
||||||
display: "flex",
|
}));
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "flex-end",
|
const ListItemIcon = styled(MuiListItemIcon)<ListItemIconProps>(() => ({
|
||||||
padding: theme.spacing(0, 1),
|
minWidth: 42,
|
||||||
// necessary for content to be below app bar
|
}));
|
||||||
...theme.mixins.toolbar,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flexGrow: 1,
|
|
||||||
padding: theme.spacing(3),
|
|
||||||
},
|
|
||||||
listItem: {
|
|
||||||
paddingLeft: 16,
|
|
||||||
paddingRight: 16,
|
|
||||||
[theme.breakpoints.up("sm")]: {
|
|
||||||
paddingLeft: 20,
|
|
||||||
paddingRight: 20,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
listItemIcon: {
|
|
||||||
minWidth: 42,
|
|
||||||
},
|
|
||||||
titleHighlight: {
|
|
||||||
color: theme.palette.secondary.main,
|
|
||||||
marginRight: 4,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -126,7 +132,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Layout({ title, page, children }: Props): JSX.Element {
|
export function Layout({ title, page, children }: Props): JSX.Element {
|
||||||
const classes = useStyles();
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
@ -138,145 +143,109 @@ export function Layout({ title, page, children }: Props): JSX.Element {
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SiteTitle = styled("span")({
|
||||||
|
...(title !== "" && {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
marginRight: 4,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<Box sx={{ display: "flex" }}>
|
||||||
<AppBar
|
<AppBar position="fixed" open={open}>
|
||||||
position="fixed"
|
|
||||||
className={clsx(classes.appBar, {
|
|
||||||
[classes.appBarShift]: open,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="open drawer"
|
aria-label="Open drawer"
|
||||||
onClick={handleDrawerOpen}
|
onClick={handleDrawerOpen}
|
||||||
edge="start"
|
edge="start"
|
||||||
className={clsx(classes.menuButton, {
|
sx={{
|
||||||
[classes.hide]: open,
|
mr: 5,
|
||||||
})}
|
...(open && { display: "none" }),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h5" noWrap>
|
<Box
|
||||||
<span className={title !== "" ? classes.titleHighlight : ""}>
|
sx={{
|
||||||
Hetty://
|
display: "flex",
|
||||||
</span>
|
justifyContent: "space-around",
|
||||||
{title}
|
width: "100%",
|
||||||
</Typography>
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h5" noWrap sx={{ width: "100%" }}>
|
||||||
|
<SiteTitle>Hetty://</SiteTitle>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ flexShrink: 0, pt: 0.75 }}>v{process.env.NEXT_PUBLIC_VERSION || "0.0"}</Box>
|
||||||
|
</Box>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<Drawer
|
<Drawer variant="permanent" open={open}>
|
||||||
variant="permanent"
|
<DrawerHeader>
|
||||||
className={clsx(classes.drawer, {
|
|
||||||
[classes.drawerOpen]: open,
|
|
||||||
[classes.drawerClose]: !open,
|
|
||||||
})}
|
|
||||||
classes={{
|
|
||||||
paper: clsx({
|
|
||||||
[classes.drawerOpen]: open,
|
|
||||||
[classes.drawerClose]: !open,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={classes.toolbar}>
|
|
||||||
<IconButton onClick={handleDrawerClose}>
|
<IconButton onClick={handleDrawerClose}>
|
||||||
{theme.direction === "rtl" ? (
|
{theme.direction === "rtl" ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
||||||
<ChevronRightIcon />
|
|
||||||
) : (
|
|
||||||
<ChevronLeftIcon />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</DrawerHeader>
|
||||||
<Divider />
|
<Divider />
|
||||||
<List>
|
<List sx={{ p: 0 }}>
|
||||||
<Link href="/" passHref>
|
<Link href="/" passHref>
|
||||||
<ListItem
|
<ListItemButton key="home" selected={page === Page.Home}>
|
||||||
button
|
|
||||||
component="a"
|
|
||||||
key="home"
|
|
||||||
selected={page === Page.Home}
|
|
||||||
className={classes.listItem}
|
|
||||||
>
|
|
||||||
<Tooltip title="Home">
|
<Tooltip title="Home">
|
||||||
<ListItemIcon className={classes.listItemIcon}>
|
<ListItemIcon>
|
||||||
<HomeIcon />
|
<HomeIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ListItemText primary="Home" />
|
<ListItemText primary="Home" />
|
||||||
</ListItem>
|
</ListItemButton>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/proxy/logs" passHref>
|
<Link href="/proxy/logs" passHref>
|
||||||
<ListItem
|
<ListItemButton key="proxyLogs" selected={page === Page.ProxyLogs}>
|
||||||
button
|
|
||||||
component="a"
|
|
||||||
key="proxyLogs"
|
|
||||||
selected={page === Page.ProxyLogs}
|
|
||||||
className={classes.listItem}
|
|
||||||
>
|
|
||||||
<Tooltip title="Proxy">
|
<Tooltip title="Proxy">
|
||||||
<ListItemIcon className={classes.listItemIcon}>
|
<ListItemIcon>
|
||||||
<SettingsEthernetIcon />
|
<SettingsEthernetIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ListItemText primary="Proxy" />
|
<ListItemText primary="Proxy" />
|
||||||
</ListItem>
|
</ListItemButton>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/sender" passHref>
|
<Link href="/sender" passHref>
|
||||||
<ListItem
|
<ListItemButton key="sender" selected={page === Page.Sender}>
|
||||||
button
|
|
||||||
component="a"
|
|
||||||
key="sender"
|
|
||||||
selected={page === Page.Sender}
|
|
||||||
className={classes.listItem}
|
|
||||||
>
|
|
||||||
<Tooltip title="Sender">
|
<Tooltip title="Sender">
|
||||||
<ListItemIcon className={classes.listItemIcon}>
|
<ListItemIcon>
|
||||||
<SendIcon />
|
<SendIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ListItemText primary="Sender" />
|
<ListItemText primary="Sender" />
|
||||||
</ListItem>
|
</ListItemButton>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/scope" passHref>
|
<Link href="/scope" passHref>
|
||||||
<ListItem
|
<ListItemButton key="scope" selected={page === Page.Scope}>
|
||||||
button
|
|
||||||
component="a"
|
|
||||||
key="scope"
|
|
||||||
selected={page === Page.Scope}
|
|
||||||
className={classes.listItem}
|
|
||||||
>
|
|
||||||
<Tooltip title="Scope">
|
<Tooltip title="Scope">
|
||||||
<ListItemIcon className={classes.listItemIcon}>
|
<ListItemIcon>
|
||||||
<LocationSearchingIcon />
|
<LocationSearchingIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ListItemText primary="Scope" />
|
<ListItemText primary="Scope" />
|
||||||
</ListItem>
|
</ListItemButton>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/projects" passHref>
|
<Link href="/projects" passHref>
|
||||||
<ListItem
|
<ListItemButton key="projects" selected={page === Page.Projects}>
|
||||||
button
|
|
||||||
component="a"
|
|
||||||
key="projects"
|
|
||||||
selected={page === Page.Projects}
|
|
||||||
className={classes.listItem}
|
|
||||||
>
|
|
||||||
<Tooltip title="Projects">
|
<Tooltip title="Projects">
|
||||||
<ListItemIcon className={classes.listItemIcon}>
|
<ListItemIcon>
|
||||||
<FolderIcon />
|
<FolderIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ListItemText primary="Projects" />
|
<ListItemText primary="Projects" />
|
||||||
</ListItem>
|
</ListItemButton>
|
||||||
</Link>
|
</Link>
|
||||||
</List>
|
</List>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
<main className={classes.content}>
|
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
||||||
<div className={classes.toolbar} />
|
<DrawerHeader />
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</Box>
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,29 +1,8 @@
|
|||||||
import { gql, useMutation } from "@apollo/client";
|
import { gql, useMutation } from "@apollo/client";
|
||||||
import {
|
import { Box, Button, CircularProgress, TextField, Typography } from "@mui/material";
|
||||||
Box,
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
Button,
|
|
||||||
CircularProgress,
|
|
||||||
createStyles,
|
|
||||||
makeStyles,
|
|
||||||
TextField,
|
|
||||||
Theme,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import AddIcon from "@material-ui/icons/Add";
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
|
||||||
createStyles({
|
|
||||||
projectName: {
|
|
||||||
marginTop: -6,
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const CREATE_PROJECT = gql`
|
const CREATE_PROJECT = gql`
|
||||||
mutation CreateProject($name: String!) {
|
mutation CreateProject($name: String!) {
|
||||||
createProject(name: $name) {
|
createProject(name: $name) {
|
||||||
@ -44,21 +23,17 @@ const OPEN_PROJECT = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function NewProject(): JSX.Element {
|
function NewProject(): JSX.Element {
|
||||||
const classes = useStyles();
|
const [name, setName] = useState("");
|
||||||
const [input, setInput] = useState(null);
|
|
||||||
|
|
||||||
const [createProject, { error: createProjErr, loading: createProjLoading }] = useMutation(CREATE_PROJECT, {
|
const [createProject, { error: createProjErr, loading: createProjLoading }] = useMutation(CREATE_PROJECT, {
|
||||||
onError: () => { },
|
onError: () => {},
|
||||||
onCompleted(data) {
|
onCompleted(data) {
|
||||||
input.value = "";
|
setName("");
|
||||||
openProject({ variables: { id: data.createProject.id } });
|
openProject({ variables: { id: data.createProject.id } });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [openProject, { error: openProjErr, loading: openProjLoading }] = useMutation(OPEN_PROJECT, {
|
const [openProject, { error: openProjErr, loading: openProjLoading }] = useMutation(OPEN_PROJECT, {
|
||||||
onError: () => { },
|
onError: () => {},
|
||||||
onCompleted() {
|
|
||||||
input.value = "";
|
|
||||||
},
|
|
||||||
update(cache, { data: { openProject } }) {
|
update(cache, { data: { openProject } }) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
fields: {
|
fields: {
|
||||||
@ -99,7 +74,7 @@ function NewProject(): JSX.Element {
|
|||||||
|
|
||||||
const handleCreateAndOpenProjectForm = (e: React.SyntheticEvent) => {
|
const handleCreateAndOpenProjectForm = (e: React.SyntheticEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
createProject({ variables: { name: input.value } });
|
createProject({ variables: { name } });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -109,25 +84,26 @@ function NewProject(): JSX.Element {
|
|||||||
</Box>
|
</Box>
|
||||||
<form onSubmit={handleCreateAndOpenProjectForm} autoComplete="off">
|
<form onSubmit={handleCreateAndOpenProjectForm} autoComplete="off">
|
||||||
<TextField
|
<TextField
|
||||||
className={classes.projectName}
|
sx={{
|
||||||
color="secondary"
|
mr: 2,
|
||||||
inputProps={{
|
|
||||||
id: "projectName",
|
|
||||||
ref: (node) => {
|
|
||||||
setInput(node);
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
label="Project name"
|
label="Project name"
|
||||||
placeholder="Project name…"
|
placeholder="Project name…"
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
error={Boolean(createProjErr || openProjErr)}
|
error={Boolean(createProjErr || openProjErr)}
|
||||||
helperText={createProjErr && createProjErr.message || openProjErr && openProjErr.message}
|
helperText={(createProjErr && createProjErr.message) || (openProjErr && openProjErr.message)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className={classes.button}
|
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
|
sx={{
|
||||||
|
pt: 0.9,
|
||||||
|
pb: 0.7,
|
||||||
|
}}
|
||||||
disabled={createProjLoading || openProjLoading}
|
disabled={createProjLoading || openProjLoading}
|
||||||
startIcon={createProjLoading || openProjLoading ? <CircularProgress size={22} /> : <AddIcon />}
|
startIcon={createProjLoading || openProjLoading ? <CircularProgress size={22} /> : <AddIcon />}
|
||||||
>
|
>
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
createStyles,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -16,34 +15,20 @@ import {
|
|||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
ListItemSecondaryAction,
|
ListItemSecondaryAction,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
makeStyles,
|
Paper,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
Theme,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@material-ui/core";
|
useTheme,
|
||||||
import CloseIcon from "@material-ui/icons/Close";
|
} from "@mui/material";
|
||||||
import DescriptionIcon from "@material-ui/icons/Description";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import DeleteIcon from "@material-ui/icons/Delete";
|
import DescriptionIcon from "@mui/icons-material/Description";
|
||||||
import LaunchIcon from "@material-ui/icons/Launch";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import { Alert } from "@material-ui/lab";
|
import LaunchIcon from "@mui/icons-material/Launch";
|
||||||
|
import { Alert } from "@mui/lab";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
import { Project } from "../../lib/Project";
|
||||||
createStyles({
|
|
||||||
projectsList: {
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
},
|
|
||||||
activeProject: {
|
|
||||||
color: theme.palette.getContrastText(theme.palette.secondary.main),
|
|
||||||
backgroundColor: theme.palette.secondary.main,
|
|
||||||
},
|
|
||||||
deleteProjectButton: {
|
|
||||||
color: theme.palette.error.main,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const PROJECTS = gql`
|
const PROJECTS = gql`
|
||||||
query Projects {
|
query Projects {
|
||||||
@ -82,58 +67,56 @@ const DELETE_PROJECT = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function ProjectList(): JSX.Element {
|
function ProjectList(): JSX.Element {
|
||||||
const classes = useStyles();
|
const theme = useTheme();
|
||||||
const { loading: projLoading, error: projErr, data: projData } = useQuery(
|
const { loading: projLoading, error: projErr, data: projData } = useQuery<{ projects: Project[] }>(PROJECTS);
|
||||||
PROJECTS
|
const [openProject, { error: openProjErr, loading: openProjLoading }] = useMutation<{ openProject: Project }>(
|
||||||
|
OPEN_PROJECT,
|
||||||
|
{
|
||||||
|
errorPolicy: "all",
|
||||||
|
onError: () => {},
|
||||||
|
update(cache, { data }) {
|
||||||
|
cache.modify({
|
||||||
|
fields: {
|
||||||
|
activeProject() {
|
||||||
|
const activeProjRef = cache.writeFragment({
|
||||||
|
data: data?.openProject,
|
||||||
|
fragment: gql`
|
||||||
|
fragment ActiveProject on Project {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
isActive
|
||||||
|
type
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
return activeProjRef;
|
||||||
|
},
|
||||||
|
projects(_, { DELETE }) {
|
||||||
|
cache.writeFragment({
|
||||||
|
id: data?.openProject.id,
|
||||||
|
data: openProject,
|
||||||
|
fragment: gql`
|
||||||
|
fragment OpenProject on Project {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
isActive
|
||||||
|
type
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
return DELETE;
|
||||||
|
},
|
||||||
|
httpRequestLogFilter(_, { DELETE }) {
|
||||||
|
return DELETE;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
const [
|
|
||||||
openProject,
|
|
||||||
{ error: openProjErr, loading: openProjLoading },
|
|
||||||
] = useMutation(OPEN_PROJECT, {
|
|
||||||
errorPolicy: "all",
|
|
||||||
onError: () => { },
|
|
||||||
update(cache, { data: { openProject } }) {
|
|
||||||
cache.modify({
|
|
||||||
fields: {
|
|
||||||
activeProject() {
|
|
||||||
const activeProjRef = cache.writeFragment({
|
|
||||||
data: openProject,
|
|
||||||
fragment: gql`
|
|
||||||
fragment ActiveProject on Project {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
isActive
|
|
||||||
type
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
return activeProjRef;
|
|
||||||
},
|
|
||||||
projects(_, { DELETE }) {
|
|
||||||
cache.writeFragment({
|
|
||||||
id: openProject.id,
|
|
||||||
data: openProject,
|
|
||||||
fragment: gql`
|
|
||||||
fragment OpenProject on Project {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
isActive
|
|
||||||
type
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
return DELETE;
|
|
||||||
},
|
|
||||||
httpRequestLogFilter(_, { DELETE }) {
|
|
||||||
return DELETE;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [closeProject, { error: closeProjErr }] = useMutation(CLOSE_PROJECT, {
|
const [closeProject, { error: closeProjErr }] = useMutation(CLOSE_PROJECT, {
|
||||||
errorPolicy: "all",
|
errorPolicy: "all",
|
||||||
onError: () => { },
|
onError: () => {},
|
||||||
update(cache) {
|
update(cache) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
fields: {
|
fields: {
|
||||||
@ -150,12 +133,9 @@ function ProjectList(): JSX.Element {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [
|
const [deleteProject, { loading: deleteProjLoading, error: deleteProjErr }] = useMutation(DELETE_PROJECT, {
|
||||||
deleteProject,
|
|
||||||
{ loading: deleteProjLoading, error: deleteProjErr },
|
|
||||||
] = useMutation(DELETE_PROJECT, {
|
|
||||||
errorPolicy: "all",
|
errorPolicy: "all",
|
||||||
onError: () => { },
|
onError: () => {},
|
||||||
update(cache) {
|
update(cache) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
fields: {
|
fields: {
|
||||||
@ -169,21 +149,21 @@ function ProjectList(): JSX.Element {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [deleteProj, setDeleteProj] = useState(null);
|
const [deleteProj, setDeleteProj] = useState<Project>();
|
||||||
const [deleteDiagOpen, setDeleteDiagOpen] = useState(false);
|
const [deleteDiagOpen, setDeleteDiagOpen] = useState(false);
|
||||||
const handleDeleteButtonClick = (project: any) => {
|
const handleDeleteButtonClick = (project: any) => {
|
||||||
setDeleteProj(project);
|
setDeleteProj(project);
|
||||||
setDeleteDiagOpen(true);
|
setDeleteDiagOpen(true);
|
||||||
};
|
};
|
||||||
const handleDeleteConfirm = () => {
|
const handleDeleteConfirm = () => {
|
||||||
deleteProject({ variables: { id: deleteProj.id } });
|
deleteProject({ variables: { id: deleteProj?.id } });
|
||||||
};
|
};
|
||||||
const handleDeleteCancel = () => {
|
const handleDeleteCancel = () => {
|
||||||
setDeleteDiagOpen(false);
|
setDeleteDiagOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [deleteNotifOpen, setDeleteNotifOpen] = useState(false);
|
const [deleteNotifOpen, setDeleteNotifOpen] = useState(false);
|
||||||
const handleCloseDeleteNotif = (_, reason?: string) => {
|
const handleCloseDeleteNotif = (_: Event | React.SyntheticEvent, reason?: string) => {
|
||||||
if (reason === "clickaway") {
|
if (reason === "clickaway") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -198,23 +178,25 @@ function ProjectList(): JSX.Element {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
Deleting a project permanently removes its database file from disk.
|
Deleting a project permanently removes all its data from the database. This action is irreversible.
|
||||||
This action is irreversible.
|
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
{deleteProjErr && (
|
{deleteProjErr && <Alert severity="error">Error closing project: {deleteProjErr.message}</Alert>}
|
||||||
<Alert severity="error">
|
|
||||||
Error closing project: {deleteProjErr.message}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleDeleteCancel} autoFocus>
|
<Button onClick={handleDeleteCancel} autoFocus color="secondary" variant="contained">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={classes.deleteProjectButton}
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
backgroundColor: "error.main",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "error.dark",
|
||||||
|
},
|
||||||
|
}}
|
||||||
onClick={handleDeleteConfirm}
|
onClick={handleDeleteConfirm}
|
||||||
disabled={deleteProjLoading}
|
disabled={deleteProjLoading}
|
||||||
|
variant="contained"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
@ -225,6 +207,7 @@ function ProjectList(): JSX.Element {
|
|||||||
open={deleteNotifOpen}
|
open={deleteNotifOpen}
|
||||||
autoHideDuration={3000}
|
autoHideDuration={3000}
|
||||||
onClose={handleCloseDeleteNotif}
|
onClose={handleCloseDeleteNotif}
|
||||||
|
anchorOrigin={{ horizontal: "center", vertical: "bottom" }}
|
||||||
>
|
>
|
||||||
<Alert onClose={handleCloseDeleteNotif} severity="info">
|
<Alert onClose={handleCloseDeleteNotif} severity="info">
|
||||||
Project <strong>{deleteProj?.name}</strong> was deleted.
|
Project <strong>{deleteProj?.name}</strong> was deleted.
|
||||||
@ -237,82 +220,70 @@ function ProjectList(): JSX.Element {
|
|||||||
|
|
||||||
<Box mb={4}>
|
<Box mb={4}>
|
||||||
{projLoading && <CircularProgress />}
|
{projLoading && <CircularProgress />}
|
||||||
{projErr && (
|
{projErr && <Alert severity="error">Error fetching projects: {projErr.message}</Alert>}
|
||||||
<Alert severity="error">
|
{openProjErr && <Alert severity="error">Error opening project: {openProjErr.message}</Alert>}
|
||||||
Error fetching projects: {projErr.message}
|
{closeProjErr && <Alert severity="error">Error closing project: {closeProjErr.message}</Alert>}
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{openProjErr && (
|
|
||||||
<Alert severity="error">
|
|
||||||
Error opening project: {openProjErr.message}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{closeProjErr && (
|
|
||||||
<Alert severity="error">
|
|
||||||
Error closing project: {closeProjErr.message}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{projData?.projects.length > 0 && (
|
{projData && projData.projects.length > 0 && (
|
||||||
<List className={classes.projectsList}>
|
<Paper>
|
||||||
{projData.projects.map((project) => (
|
<List>
|
||||||
<ListItem key={project.id}>
|
{projData.projects.map((project) => (
|
||||||
<ListItemAvatar>
|
<ListItem key={project.id}>
|
||||||
<Avatar
|
<ListItemAvatar>
|
||||||
className={
|
<Avatar
|
||||||
project.isActive ? classes.activeProject : undefined
|
sx={{
|
||||||
}
|
...(project.isActive && {
|
||||||
>
|
color: theme.palette.secondary.dark,
|
||||||
<DescriptionIcon />
|
backgroundColor: theme.palette.primary.main,
|
||||||
</Avatar>
|
}),
|
||||||
</ListItemAvatar>
|
}}
|
||||||
<ListItemText>
|
>
|
||||||
{project.name} {project.isActive && <em>(Active)</em>}
|
<DescriptionIcon />
|
||||||
</ListItemText>
|
</Avatar>
|
||||||
<ListItemSecondaryAction>
|
</ListItemAvatar>
|
||||||
{project.isActive && (
|
<ListItemText>
|
||||||
<Tooltip title="Close project">
|
{project.name} {project.isActive && <em>(Active)</em>}
|
||||||
<IconButton onClick={() => closeProject()}>
|
</ListItemText>
|
||||||
<CloseIcon />
|
<ListItemSecondaryAction>
|
||||||
</IconButton>
|
{project.isActive && (
|
||||||
</Tooltip>
|
<Tooltip title="Close project">
|
||||||
)}
|
<IconButton onClick={() => closeProject()}>
|
||||||
{!project.isActive && (
|
<CloseIcon />
|
||||||
<Tooltip title="Open project">
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{!project.isActive && (
|
||||||
|
<Tooltip title="Open project">
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
disabled={openProjLoading || projLoading}
|
||||||
|
onClick={() =>
|
||||||
|
openProject({
|
||||||
|
variables: { id: project.id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LaunchIcon />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip title="Delete project">
|
||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton onClick={() => handleDeleteButtonClick(project)} disabled={project.isActive}>
|
||||||
disabled={openProjLoading || projLoading}
|
<DeleteIcon />
|
||||||
onClick={() =>
|
|
||||||
openProject({
|
|
||||||
variables: { id: project.id },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<LaunchIcon />
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
</ListItemSecondaryAction>
|
||||||
<Tooltip title="Delete project">
|
</ListItem>
|
||||||
<span>
|
))}
|
||||||
<IconButton
|
</List>
|
||||||
onClick={() => handleDeleteButtonClick(project)}
|
</Paper>
|
||||||
disabled={project.isActive}
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
)}
|
)}
|
||||||
{projData?.projects.length === 0 && (
|
{projData?.projects.length === 0 && (
|
||||||
<Alert severity="info">
|
<Alert severity="info">There are no projects. Create one to get started.</Alert>
|
||||||
There are no projects. Create one to get started.
|
|
||||||
</Alert>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import Button from "@material-ui/core/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Dialog from "@material-ui/core/Dialog";
|
import Dialog from "@mui/material/Dialog";
|
||||||
import DialogActions from "@material-ui/core/DialogActions";
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
import DialogContent from "@material-ui/core/DialogContent";
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
import DialogContentText from "@material-ui/core/DialogContentText";
|
import DialogContentText from "@mui/material/DialogContentText";
|
||||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
|
||||||
export function useConfirmationDialog() {
|
export function useConfirmationDialog() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@ -38,12 +38,10 @@ export function ConfirmationDialog(props: ConfirmationDialog) {
|
|||||||
>
|
>
|
||||||
<DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle>
|
<DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText id="alert-dialog-description">
|
<DialogContentText id="alert-dialog-description">{children}</DialogContentText>
|
||||||
{children}
|
|
||||||
</DialogContentText>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose}>Abort</Button>
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
<Button onClick={confirm} autoFocus>
|
<Button onClick={confirm} autoFocus>
|
||||||
Confirm
|
Confirm
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import dynamic from "next/dynamic";
|
import MonacoEditor from "@monaco-editor/react";
|
||||||
const MonacoEditor = dynamic(import("react-monaco-editor"), { ssr: false });
|
import monaco from "monaco-editor/esm/vs/editor/editor.api";
|
||||||
|
|
||||||
const monacoOptions = {
|
const monacoOptions: monaco.editor.IEditorOptions = {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
wordWrap: "on",
|
wordWrap: "on",
|
||||||
minimap: {
|
minimap: {
|
||||||
@ -11,20 +11,7 @@ const monacoOptions = {
|
|||||||
|
|
||||||
type language = "html" | "typescript" | "json";
|
type language = "html" | "typescript" | "json";
|
||||||
|
|
||||||
function editorDidMount() {
|
function languageForContentType(contentType?: string): language | undefined {
|
||||||
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 {
|
|
||||||
switch (contentType) {
|
switch (contentType) {
|
||||||
case "text/html":
|
case "text/html":
|
||||||
return "html";
|
return "html";
|
||||||
@ -41,7 +28,7 @@ function languageForContentType(contentType: string): language {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
content: string;
|
content: string;
|
||||||
contentType: string;
|
contentType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Editor({ content, contentType }: Props): JSX.Element {
|
function Editor({ content, contentType }: Props): JSX.Element {
|
||||||
@ -50,8 +37,7 @@ function Editor({ content, contentType }: Props): JSX.Element {
|
|||||||
height={"600px"}
|
height={"600px"}
|
||||||
language={languageForContentType(contentType)}
|
language={languageForContentType(contentType)}
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
editorDidMount={editorDidMount}
|
options={monacoOptions}
|
||||||
options={monacoOptions as any}
|
|
||||||
value={content}
|
value={content}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,83 +1,66 @@
|
|||||||
import {
|
import { Table, TableBody, TableCell, TableContainer, TableRow, Snackbar } from "@mui/material";
|
||||||
makeStyles,
|
import { Alert } from "@mui/lab";
|
||||||
Theme,
|
|
||||||
createStyles,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableRow,
|
|
||||||
Snackbar,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { Alert } from "@material-ui/lab";
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => {
|
const baseCellStyle = {
|
||||||
const paddingX = 0;
|
px: 0,
|
||||||
const paddingY = theme.spacing(1) / 3;
|
py: 0.33,
|
||||||
const tableCell = {
|
verticalAlign: "top",
|
||||||
paddingLeft: paddingX,
|
border: "none",
|
||||||
paddingRight: paddingX,
|
whiteSpace: "nowrap" as any,
|
||||||
paddingTop: paddingY,
|
overflow: "hidden",
|
||||||
paddingBottom: paddingY,
|
textOverflow: "ellipsis",
|
||||||
verticalAlign: "top",
|
"&:hover": {
|
||||||
border: "none",
|
color: "primary.main",
|
||||||
whiteSpace: "nowrap" as any,
|
whiteSpace: "inherit" as any,
|
||||||
overflow: "hidden",
|
overflow: "inherit",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "inherit",
|
||||||
"&:hover": {
|
cursor: "copy",
|
||||||
color: theme.palette.secondary.main,
|
},
|
||||||
whiteSpace: "inherit" as any,
|
};
|
||||||
overflow: "inherit",
|
|
||||||
textOverflow: "inherit",
|
const keyCellStyle = {
|
||||||
cursor: "copy",
|
...baseCellStyle,
|
||||||
},
|
pr: 1,
|
||||||
};
|
width: "40%",
|
||||||
return createStyles({
|
fontWeight: "bold",
|
||||||
root: {},
|
fontSize: ".75rem",
|
||||||
table: {
|
};
|
||||||
tableLayout: "fixed",
|
|
||||||
width: "100%",
|
const valueCellStyle = {
|
||||||
},
|
...baseCellStyle,
|
||||||
keyCell: {
|
width: "60%",
|
||||||
...tableCell,
|
border: "none",
|
||||||
paddingRight: theme.spacing(1),
|
fontSize: ".75rem",
|
||||||
width: "40%",
|
};
|
||||||
fontWeight: "bold",
|
|
||||||
fontSize: ".75rem",
|
|
||||||
},
|
|
||||||
valueCell: {
|
|
||||||
...tableCell,
|
|
||||||
width: "60%",
|
|
||||||
border: "none",
|
|
||||||
fontSize: ".75rem",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
headers: Array<{ key: string; value: string }>;
|
headers: Array<{ key: string; value: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function HttpHeadersTable({ headers }: Props): JSX.Element {
|
function HttpHeadersTable({ headers }: Props): JSX.Element {
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
const windowSel = window.getSelection();
|
||||||
|
|
||||||
|
if (!windowSel || !document) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const r = document.createRange();
|
const r = document.createRange();
|
||||||
r.selectNode(e.currentTarget);
|
r.selectNode(e.currentTarget);
|
||||||
window.getSelection().removeAllRanges();
|
windowSel.removeAllRanges();
|
||||||
window.getSelection().addRange(r);
|
windowSel.addRange(r);
|
||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
window.getSelection().removeAllRanges();
|
windowSel.removeAllRanges();
|
||||||
|
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = (event?: React.SyntheticEvent, reason?: string) => {
|
const handleClose = (event: Event | React.SyntheticEvent, reason?: string) => {
|
||||||
if (reason === "clickaway") {
|
if (reason === "clickaway") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -92,20 +75,21 @@ function HttpHeadersTable({ headers }: Props): JSX.Element {
|
|||||||
Copied to clipboard.
|
Copied to clipboard.
|
||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
<TableContainer className={classes.root}>
|
<TableContainer>
|
||||||
<Table className={classes.table} size="small">
|
<Table
|
||||||
|
sx={{
|
||||||
|
tableLayout: "fixed",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{headers.map(({ key, value }, index) => (
|
{headers.map(({ key, value }, index) => (
|
||||||
<TableRow key={index}>
|
<TableRow key={index}>
|
||||||
<TableCell
|
<TableCell component="th" scope="row" sx={keyCellStyle} onClick={handleClick}>
|
||||||
component="th"
|
|
||||||
scope="row"
|
|
||||||
className={classes.keyCell}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<code>{key}:</code>
|
<code>{key}:</code>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className={classes.valueCell} onClick={handleClick}>
|
<TableCell sx={valueCellStyle} onClick={handleClick}>
|
||||||
<code>{value}</code>
|
<code>{value}</code>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
@ -1,31 +1,25 @@
|
|||||||
import { Theme, withTheme } from "@material-ui/core";
|
import { SvgIconTypeMap } from "@mui/material";
|
||||||
import { orange, red } from "@material-ui/core/colors";
|
import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
|
||||||
import FiberManualRecordIcon from "@material-ui/icons/FiberManualRecord";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
status: number;
|
status: number;
|
||||||
theme: Theme;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function HttpStatusIcon({ status, theme }: Props): JSX.Element {
|
export default function HttpStatusIcon({ status }: Props): JSX.Element {
|
||||||
const style = { marginTop: "-.25rem", verticalAlign: "middle" };
|
let color: SvgIconTypeMap["props"]["color"] = "inherit";
|
||||||
|
|
||||||
switch (Math.floor(status / 100)) {
|
switch (Math.floor(status / 100)) {
|
||||||
case 2:
|
case 2:
|
||||||
case 3:
|
case 3:
|
||||||
return (
|
color = "primary";
|
||||||
<FiberManualRecordIcon
|
break;
|
||||||
style={{ ...style, color: theme.palette.secondary.main }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 4:
|
case 4:
|
||||||
return (
|
color = "warning";
|
||||||
<FiberManualRecordIcon style={{ ...style, color: orange["A400"] }} />
|
break;
|
||||||
);
|
|
||||||
case 5:
|
case 5:
|
||||||
return <FiberManualRecordIcon style={{ ...style, color: red["A400"] }} />;
|
color = "error";
|
||||||
default:
|
break;
|
||||||
return <FiberManualRecordIcon style={style} />;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default withTheme(HttpStatusIcon);
|
return <FiberManualRecordIcon sx={{ marginTop: "-.25rem", verticalAlign: "middle" }} color={color} />;
|
||||||
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { gql, useQuery } from "@apollo/client";
|
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 ResponseDetail from "./ResponseDetail";
|
||||||
import RequestDetail from "./RequestDetail";
|
import RequestDetail from "./RequestDetail";
|
||||||
import Alert from "@material-ui/lab/Alert";
|
import Alert from "@mui/lab/Alert";
|
||||||
|
|
||||||
const HTTP_REQUEST_LOG = gql`
|
const HTTP_REQUEST_LOG = gql`
|
||||||
query HttpRequestLog($id: ID!) {
|
query HttpRequestLog($id: ID!) {
|
||||||
@ -44,11 +44,7 @@ function LogDetail({ requestId: id }: Props): JSX.Element {
|
|||||||
return <CircularProgress />;
|
return <CircularProgress />;
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return <Alert severity="error">Error fetching logs details: {error.message}</Alert>;
|
||||||
<Alert severity="error">
|
|
||||||
Error fetching logs details: {error.message}
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.httpRequestLog) {
|
if (!data.httpRequestLog) {
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import { Box, CircularProgress, Link as MaterialLink, Typography } from "@mui/material";
|
||||||
Box,
|
import Alert from "@mui/lab/Alert";
|
||||||
CircularProgress,
|
|
||||||
Link as MaterialLink,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import Alert from "@material-ui/lab/Alert";
|
|
||||||
|
|
||||||
import RequestList from "./RequestList";
|
import RequestList from "./RequestList";
|
||||||
import LogDetail from "./LogDetail";
|
import LogDetail from "./LogDetail";
|
||||||
@ -33,7 +28,7 @@ function LogsOverview(): JSX.Element {
|
|||||||
<Alert severity="info">
|
<Alert severity="info">
|
||||||
There is no project active.{" "}
|
There is no project active.{" "}
|
||||||
<Link href="/projects" passHref>
|
<Link href="/projects" passHref>
|
||||||
<MaterialLink color="secondary">Create or open</MaterialLink>
|
<MaterialLink color="primary">Create or open</MaterialLink>
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
one first.
|
one first.
|
||||||
</Alert>
|
</Alert>
|
||||||
@ -47,11 +42,7 @@ function LogsOverview(): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Box mb={2}>
|
<Box mb={2}>
|
||||||
<RequestList
|
<RequestList logs={logs || []} selectedReqLogId={detailReqLogId} onLogClick={handleLogClick} />
|
||||||
logs={logs}
|
|
||||||
selectedReqLogId={detailReqLogId}
|
|
||||||
onLogClick={handleLogClick}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
{detailReqLogId && <LogDetail requestId={detailReqLogId} />}
|
{detailReqLogId && <LogDetail requestId={detailReqLogId} />}
|
||||||
|
@ -1,42 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import { Typography, Box, Divider } from "@mui/material";
|
||||||
Typography,
|
|
||||||
Box,
|
|
||||||
createStyles,
|
|
||||||
makeStyles,
|
|
||||||
Theme,
|
|
||||||
Divider,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
|
|
||||||
import HttpHeadersTable from "./HttpHeadersTable";
|
import HttpHeadersTable from "./HttpHeadersTable";
|
||||||
import Editor from "./Editor";
|
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 {
|
interface Props {
|
||||||
request: {
|
request: {
|
||||||
method: string;
|
method: string;
|
||||||
@ -49,29 +16,27 @@ interface Props {
|
|||||||
|
|
||||||
function RequestDetail({ request }: Props): JSX.Element {
|
function RequestDetail({ request }: Props): JSX.Element {
|
||||||
const { method, url, proto, headers, body } = request;
|
const { method, url, proto, headers, body } = request;
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
const contentType = headers.find((header) => header.key === "Content-Type")
|
const contentType = headers.find((header) => header.key === "Content-Type")?.value;
|
||||||
?.value;
|
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Box p={2}>
|
<Box p={2}>
|
||||||
<Typography
|
<Typography variant="overline" color="textSecondary" style={{ float: "right" }}>
|
||||||
variant="overline"
|
|
||||||
color="textSecondary"
|
|
||||||
style={{ float: "right" }}
|
|
||||||
>
|
|
||||||
Request
|
Request
|
||||||
</Typography>
|
</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)}{" "}
|
{method} {decodeURIComponent(parsedUrl.pathname + parsedUrl.search)}{" "}
|
||||||
<Typography
|
<Typography component="span" color="textSecondary" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
|
||||||
component="span"
|
|
||||||
color="textSecondary"
|
|
||||||
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
||||||
>
|
|
||||||
{proto}
|
{proto}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@ -8,48 +8,23 @@ import {
|
|||||||
TableBody,
|
TableBody,
|
||||||
Typography,
|
Typography,
|
||||||
Box,
|
Box,
|
||||||
createStyles,
|
useTheme,
|
||||||
makeStyles,
|
} from "@mui/material";
|
||||||
Theme,
|
|
||||||
withTheme,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
|
|
||||||
import HttpStatusIcon from "./HttpStatusCode";
|
import HttpStatusIcon from "./HttpStatusCode";
|
||||||
import CenteredPaper from "../CenteredPaper";
|
import CenteredPaper from "../CenteredPaper";
|
||||||
|
import { RequestLog } from "../../lib/requestLogs";
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
|
||||||
createStyles({
|
|
||||||
row: {
|
|
||||||
"&:hover": {
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/* Pseudo-class applied to the root element if `hover={true}`. */
|
|
||||||
hover: {},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logs: Array<any>;
|
logs: RequestLog[];
|
||||||
selectedReqLogId?: string;
|
selectedReqLogId?: string;
|
||||||
onLogClick(requestId: string): void;
|
onLogClick(requestId: string): void;
|
||||||
theme: Theme;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function RequestList({
|
export default function RequestList({ logs, onLogClick, selectedReqLogId }: Props): JSX.Element {
|
||||||
logs,
|
|
||||||
onLogClick,
|
|
||||||
selectedReqLogId,
|
|
||||||
theme,
|
|
||||||
}: Props): JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<RequestListTable
|
<RequestListTable onLogClick={onLogClick} logs={logs} selectedReqLogId={selectedReqLogId} />
|
||||||
onLogClick={onLogClick}
|
|
||||||
logs={logs}
|
|
||||||
selectedReqLogId={selectedReqLogId}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
{logs.length === 0 && (
|
{logs.length === 0 && (
|
||||||
<Box my={1}>
|
<Box my={1}>
|
||||||
<CenteredPaper>
|
<CenteredPaper>
|
||||||
@ -62,19 +37,14 @@ function RequestList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface RequestListTableProps {
|
interface RequestListTableProps {
|
||||||
logs?: any;
|
logs: RequestLog[];
|
||||||
selectedReqLogId?: string;
|
selectedReqLogId?: string;
|
||||||
onLogClick(requestId: string): void;
|
onLogClick(requestId: string): void;
|
||||||
theme: Theme;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function RequestListTable({
|
function RequestListTable({ logs, selectedReqLogId, onLogClick }: RequestListTableProps): JSX.Element {
|
||||||
logs,
|
const theme = useTheme();
|
||||||
selectedReqLogId,
|
|
||||||
onLogClick,
|
|
||||||
theme,
|
|
||||||
}: RequestListTableProps): JSX.Element {
|
|
||||||
const classes = useStyles();
|
|
||||||
return (
|
return (
|
||||||
<TableContainer
|
<TableContainer
|
||||||
component={Paper}
|
component={Paper}
|
||||||
@ -102,26 +72,25 @@ function RequestListTable({
|
|||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const rowStyle = {
|
|
||||||
backgroundColor:
|
|
||||||
id === selectedReqLogId && theme.palette.action.selected,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={id}
|
key={id}
|
||||||
className={classes.row}
|
sx={{
|
||||||
style={rowStyle}
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
...(id === selectedReqLogId && {
|
||||||
|
bgcolor: theme.palette.action.selected,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
hover
|
hover
|
||||||
onClick={() => onLogClick(id)}
|
onClick={() => onLogClick(id)}
|
||||||
>
|
>
|
||||||
<TableCell style={{ ...cellStyle, width: "100px" }}>
|
<TableCell style={{ ...cellStyle, width: "100px" }}>
|
||||||
<code>{method}</code>
|
<code>{method}</code>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell style={{ ...cellStyle, maxWidth: "100px" }}>
|
<TableCell sx={{ ...cellStyle, maxWidth: "100px" }}>{origin}</TableCell>
|
||||||
{origin}
|
<TableCell sx={{ ...cellStyle, maxWidth: "200px" }}>
|
||||||
</TableCell>
|
|
||||||
<TableCell style={{ ...cellStyle, maxWidth: "200px" }}>
|
|
||||||
{decodeURIComponent(pathname + search + hash)}
|
{decodeURIComponent(pathname + search + hash)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell style={{ maxWidth: "100px" }}>
|
<TableCell style={{ maxWidth: "100px" }}>
|
||||||
@ -142,5 +111,3 @@ function RequestListTable({
|
|||||||
</TableContainer>
|
</TableContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(RequestList);
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Typography, Box, Divider } from "@material-ui/core";
|
import { Typography, Box, Divider } from "@mui/material";
|
||||||
|
|
||||||
import HttpStatusIcon from "./HttpStatusCode";
|
import HttpStatusIcon from "./HttpStatusCode";
|
||||||
import Editor from "./Editor";
|
import Editor from "./Editor";
|
||||||
@ -15,30 +15,17 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ResponseDetail({ response }: Props): JSX.Element {
|
function ResponseDetail({ response }: Props): JSX.Element {
|
||||||
const contentType = response.headers.find(
|
const contentType = response.headers.find((header) => header.key === "Content-Type")?.value;
|
||||||
(header) => header.key === "Content-Type"
|
|
||||||
)?.value;
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Box p={2}>
|
<Box p={2}>
|
||||||
<Typography
|
<Typography variant="overline" color="textSecondary" style={{ float: "right" }}>
|
||||||
variant="overline"
|
|
||||||
color="textSecondary"
|
|
||||||
style={{ float: "right" }}
|
|
||||||
>
|
|
||||||
Response
|
Response
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography variant="h6" style={{ fontSize: "1rem", whiteSpace: "nowrap" }}>
|
||||||
variant="h6"
|
|
||||||
style={{ fontSize: "1rem", whiteSpace: "nowrap" }}
|
|
||||||
>
|
|
||||||
<HttpStatusIcon status={response.statusCode} />{" "}
|
<HttpStatusIcon status={response.statusCode} />{" "}
|
||||||
<Typography component="span" color="textSecondary">
|
<Typography component="span" color="textSecondary">
|
||||||
<Typography
|
<Typography component="span" color="textSecondary" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
|
||||||
component="span"
|
|
||||||
color="textSecondary"
|
|
||||||
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
||||||
>
|
|
||||||
{response.proto}
|
{response.proto}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Typography>{" "}
|
</Typography>{" "}
|
||||||
@ -52,9 +39,7 @@ function ResponseDetail({ response }: Props): JSX.Element {
|
|||||||
<HttpHeadersTable headers={response.headers} />
|
<HttpHeadersTable headers={response.headers} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{response.body && (
|
{response.body && <Editor content={response.body} contentType={contentType} />}
|
||||||
<Editor content={response.body} contentType={contentType} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,29 +3,23 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
ClickAwayListener,
|
ClickAwayListener,
|
||||||
createStyles,
|
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
InputBase,
|
InputBase,
|
||||||
makeStyles,
|
|
||||||
Paper,
|
Paper,
|
||||||
Popper,
|
Popper,
|
||||||
Theme,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@material-ui/core";
|
} from "@mui/material";
|
||||||
import IconButton from "@material-ui/core/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import SearchIcon from "@material-ui/icons/Search";
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
import FilterListIcon from "@material-ui/icons/FilterList";
|
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||||
import DeleteIcon from "@material-ui/icons/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import { gql, useMutation, useQuery } from "@apollo/client";
|
import { gql, useMutation, useQuery } from "@apollo/client";
|
||||||
import { withoutTypename } from "../../lib/omitTypename";
|
import { withoutTypename } from "../../lib/omitTypename";
|
||||||
import { Alert } from "@material-ui/lab";
|
import { Alert } from "@mui/lab";
|
||||||
import { useClearHTTPRequestLog } from "./hooks/useClearHTTPRequestLog";
|
import { useClearHTTPRequestLog } from "./hooks/useClearHTTPRequestLog";
|
||||||
import {
|
import { ConfirmationDialog, useConfirmationDialog } from "./ConfirmationDialog";
|
||||||
ConfirmationDialog,
|
|
||||||
useConfirmationDialog,
|
|
||||||
} from "./ConfirmationDialog";
|
|
||||||
|
|
||||||
const FILTER = gql`
|
const FILTER = gql`
|
||||||
query HttpRequestLogFilter {
|
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 {
|
export interface SearchFilter {
|
||||||
onlyInScope: boolean;
|
onlyInScope: boolean;
|
||||||
searchExpression: string;
|
searchExpression: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Search(): JSX.Element {
|
function Search(): JSX.Element {
|
||||||
const classes = useStyles();
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const [searchExpr, setSearchExpr] = useState("");
|
const [searchExpr, setSearchExpr] = useState("");
|
||||||
const { loading: filterLoading, error: filterErr, data: filter } = useQuery(
|
const {
|
||||||
FILTER,
|
loading: filterLoading,
|
||||||
{
|
error: filterErr,
|
||||||
onCompleted: (data) => {
|
data: filter,
|
||||||
setSearchExpr(data.httpRequestLogFilter?.searchExpression || "");
|
} = useQuery(FILTER, {
|
||||||
},
|
onCompleted: (data) => {
|
||||||
}
|
setSearchExpr(data.httpRequestLogFilter?.searchExpression || "");
|
||||||
);
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const [
|
const [setFilterMutate, { error: setFilterErr, loading: setFilterLoading }] = useMutation<{
|
||||||
setFilterMutate,
|
|
||||||
{ error: setFilterErr, loading: setFilterLoading },
|
|
||||||
] = useMutation<{
|
|
||||||
setHttpRequestLogFilter: SearchFilter | null;
|
setHttpRequestLogFilter: SearchFilter | null;
|
||||||
}>(SET_FILTER, {
|
}>(SET_FILTER, {
|
||||||
update(cache, { data: { setHttpRequestLogFilter } }) {
|
update(cache, { data }) {
|
||||||
cache.writeQuery({
|
cache.writeQuery({
|
||||||
query: FILTER,
|
query: FILTER,
|
||||||
data: {
|
data: {
|
||||||
httpRequestLogFilter: setHttpRequestLogFilter,
|
httpRequestLogFilter: data?.setHttpRequestLogFilter,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: () => {},
|
onError: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [
|
const [clearHTTPRequestLog, clearHTTPRequestLogResult] = useClearHTTPRequestLog();
|
||||||
clearHTTPRequestLog,
|
|
||||||
clearHTTPRequestLogResult,
|
|
||||||
] = useClearHTTPRequestLog();
|
|
||||||
const clearHTTPConfirmationDialog = useConfirmationDialog();
|
const clearHTTPConfirmationDialog = useConfirmationDialog();
|
||||||
|
|
||||||
const filterRef = useRef<HTMLElement | null>();
|
const filterRef = useRef<HTMLFormElement>(null);
|
||||||
const [filterOpen, setFilterOpen] = useState(false);
|
const [filterOpen, setFilterOpen] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = (e: React.SyntheticEvent) => {
|
const handleSubmit = (e: React.SyntheticEvent) => {
|
||||||
@ -133,8 +91,8 @@ function Search(): JSX.Element {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickAway = (event: React.MouseEvent<EventTarget>) => {
|
const handleClickAway = (event: MouseEvent | TouchEvent) => {
|
||||||
if (filterRef.current.contains(event.target as HTMLElement)) {
|
if (filterRef?.current && filterRef.current.contains(event.target as HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setFilterOpen(false);
|
setFilterOpen(false);
|
||||||
@ -144,63 +102,67 @@ function Search(): JSX.Element {
|
|||||||
<Box>
|
<Box>
|
||||||
<Error prefix="Error fetching filter" error={filterErr} />
|
<Error prefix="Error fetching filter" error={filterErr} />
|
||||||
<Error prefix="Error setting filter" error={setFilterErr} />
|
<Error prefix="Error setting filter" error={setFilterErr} />
|
||||||
<Error
|
<Error prefix="Error clearing all HTTP logs" error={clearHTTPRequestLogResult.error} />
|
||||||
prefix="Error clearing all HTTP logs"
|
|
||||||
error={clearHTTPRequestLogResult.error}
|
|
||||||
/>
|
|
||||||
<Box style={{ display: "flex", flex: 1 }}>
|
<Box style={{ display: "flex", flex: 1 }}>
|
||||||
<ClickAwayListener onClickAway={handleClickAway}>
|
<ClickAwayListener onClickAway={handleClickAway}>
|
||||||
<Paper
|
<Paper
|
||||||
component="form"
|
component="form"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
ref={filterRef}
|
ref={filterRef}
|
||||||
className={classes.root}
|
sx={{
|
||||||
|
padding: "2px 4px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
width: 400,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip title="Toggle filter options">
|
<Tooltip title="Toggle filter options">
|
||||||
<IconButton
|
<IconButton
|
||||||
className={classes.iconButton}
|
|
||||||
onClick={() => setFilterOpen(!filterOpen)}
|
onClick={() => setFilterOpen(!filterOpen)}
|
||||||
style={{
|
sx={{
|
||||||
color: filter?.httpRequestLogFilter?.onlyInScope
|
p: 1,
|
||||||
? theme.palette.secondary.main
|
color: filter?.httpRequestLogFilter?.onlyInScope ? "primary.main" : "inherit",
|
||||||
: "inherit",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{filterLoading || setFilterLoading ? (
|
{filterLoading || setFilterLoading ? (
|
||||||
<CircularProgress
|
<CircularProgress sx={{ color: theme.palette.text.primary }} size={23} />
|
||||||
className={classes.filterLoading}
|
|
||||||
size={23}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<FilterListIcon />
|
<FilterListIcon />
|
||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<InputBase
|
<InputBase
|
||||||
className={classes.input}
|
sx={{
|
||||||
|
ml: 1,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
placeholder="Search proxy logs…"
|
placeholder="Search proxy logs…"
|
||||||
value={searchExpr}
|
value={searchExpr}
|
||||||
onChange={(e) => setSearchExpr(e.target.value)}
|
onChange={(e) => setSearchExpr(e.target.value)}
|
||||||
onFocus={() => setFilterOpen(true)}
|
onFocus={() => setFilterOpen(true)}
|
||||||
/>
|
/>
|
||||||
<Tooltip title="Search">
|
<Tooltip title="Search">
|
||||||
<IconButton type="submit" className={classes.iconButton}>
|
<IconButton type="submit" sx={{ padding: 1.25 }}>
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popper
|
<Popper
|
||||||
className={classes.filterPopper}
|
|
||||||
open={filterOpen}
|
open={filterOpen}
|
||||||
anchorEl={filterRef.current}
|
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
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={
|
checked={filter?.httpRequestLogFilter?.onlyInScope ? true : false}
|
||||||
filter?.httpRequestLogFilter?.onlyInScope ? true : false
|
|
||||||
}
|
|
||||||
disabled={filterLoading || setFilterLoading}
|
disabled={filterLoading || setFilterLoading}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFilterMutate({
|
setFilterMutate({
|
||||||
|
@ -3,20 +3,18 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
createStyles,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
makeStyles,
|
|
||||||
Radio,
|
Radio,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
TextField,
|
TextField,
|
||||||
Theme,
|
} from "@mui/material";
|
||||||
} from "@material-ui/core";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
import AddIcon from "@material-ui/icons/Add";
|
import { Alert } from "@mui/lab";
|
||||||
import { Alert } from "@material-ui/lab";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { SCOPE } from "./Rules";
|
import { SCOPE } from "./Rules";
|
||||||
|
import { ScopeRule } from "../../lib/scope";
|
||||||
|
|
||||||
const SET_SCOPE = gql`
|
const SET_SCOPE = gql`
|
||||||
mutation SetScope($scope: [ScopeRuleInput!]!) {
|
mutation SetScope($scope: [ScopeRuleInput!]!) {
|
||||||
@ -26,25 +24,15 @@ const SET_SCOPE = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
|
||||||
createStyles({
|
|
||||||
ruleExpression: {
|
|
||||||
fontFamily: "'JetBrains Mono', monospace",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
function AddRule(): JSX.Element {
|
function AddRule(): JSX.Element {
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
const [ruleType, setRuleType] = React.useState("url");
|
const [ruleType, setRuleType] = React.useState("url");
|
||||||
const [expression, setExpression] = React.useState(null);
|
const [expression, setExpression] = React.useState("");
|
||||||
|
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const [setScope, { error, loading }] = useMutation(SET_SCOPE, {
|
const [setScope, { error, loading }] = useMutation(SET_SCOPE, {
|
||||||
onError() {},
|
onError() {},
|
||||||
onCompleted() {
|
onCompleted() {
|
||||||
expression.value = "";
|
setExpression("");
|
||||||
},
|
},
|
||||||
update(_, { data: { setScope } }) {
|
update(_, { data: { setScope } }) {
|
||||||
client.writeQuery({
|
client.writeQuery({
|
||||||
@ -59,21 +47,20 @@ function AddRule(): JSX.Element {
|
|||||||
};
|
};
|
||||||
const handleSubmit = (e: React.SyntheticEvent) => {
|
const handleSubmit = (e: React.SyntheticEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let scope = [];
|
let scope: ScopeRule[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = client.readQuery({
|
const data = client.readQuery<{ scope: ScopeRule[] }>({
|
||||||
query: SCOPE,
|
query: SCOPE,
|
||||||
});
|
});
|
||||||
scope = data.scope;
|
if (data) {
|
||||||
|
scope = data.scope;
|
||||||
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
setScope({
|
setScope({
|
||||||
variables: {
|
variables: {
|
||||||
scope: [
|
scope: [...scope.map(({ url }) => ({ url })), { url: expression }],
|
||||||
...scope.map(({ url }) => ({ url })),
|
|
||||||
{ url: expression.value },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -87,15 +74,10 @@ function AddRule(): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
<form onSubmit={handleSubmit} autoComplete="off">
|
<form onSubmit={handleSubmit} autoComplete="off">
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<FormLabel color="secondary" component="legend">
|
<FormLabel color="primary" component="legend">
|
||||||
Rule Type
|
Rule Type
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<RadioGroup
|
<RadioGroup row name="ruleType" value={ruleType} onChange={handleTypeChange}>
|
||||||
row
|
|
||||||
name="ruleType"
|
|
||||||
value={ruleType}
|
|
||||||
onChange={handleTypeChange}
|
|
||||||
>
|
|
||||||
<FormControlLabel value="url" control={<Radio />} label="URL" />
|
<FormControlLabel value="url" control={<Radio />} label="URL" />
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -104,20 +86,17 @@ function AddRule(): JSX.Element {
|
|||||||
label="Expression"
|
label="Expression"
|
||||||
placeholder="^https:\/\/(.*)example.com(.*)"
|
placeholder="^https:\/\/(.*)example.com(.*)"
|
||||||
helperText="Regular expression to match on."
|
helperText="Regular expression to match on."
|
||||||
color="secondary"
|
color="primary"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
required
|
required
|
||||||
|
value={expression}
|
||||||
|
onChange={(e) => setExpression(e.target.value)}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
className: classes.ruleExpression,
|
sx: { fontFamily: "'JetBrains Mono', monospace" },
|
||||||
}}
|
}}
|
||||||
InputLabelProps={{
|
InputLabelProps={{
|
||||||
shrink: true,
|
shrink: true,
|
||||||
}}
|
}}
|
||||||
inputProps={{
|
|
||||||
ref: (node) => {
|
|
||||||
setExpression(node);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -125,7 +104,7 @@ function AddRule(): JSX.Element {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="primary"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
startIcon={loading ? <CircularProgress size={22} /> : <AddIcon />}
|
startIcon={loading ? <CircularProgress size={22} /> : <AddIcon />}
|
||||||
>
|
>
|
||||||
|
@ -8,11 +8,12 @@ import {
|
|||||||
ListItemSecondaryAction,
|
ListItemSecondaryAction,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@material-ui/core";
|
} from "@mui/material";
|
||||||
import CodeIcon from "@material-ui/icons/Code";
|
import CodeIcon from "@mui/icons-material/Code";
|
||||||
import DeleteIcon from "@material-ui/icons/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { SCOPE } from "./Rules";
|
import { SCOPE } from "./Rules";
|
||||||
|
import { ScopeRule } from "../../lib/scope";
|
||||||
|
|
||||||
const SET_SCOPE = gql`
|
const SET_SCOPE = gql`
|
||||||
mutation SetScope($scope: [ScopeRuleInput!]!) {
|
mutation SetScope($scope: [ScopeRuleInput!]!) {
|
||||||
@ -22,7 +23,13 @@ const SET_SCOPE = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function RuleListItem({ scope, rule, index }): JSX.Element {
|
type RuleListItemProps = {
|
||||||
|
scope: ScopeRule[];
|
||||||
|
rule: ScopeRule;
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function RuleListItem({ scope, rule, index }: RuleListItemProps): JSX.Element {
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const [setScope, { loading }] = useMutation(SET_SCOPE, {
|
const [setScope, { loading }] = useMutation(SET_SCOPE, {
|
||||||
update(_, { data: { setScope } }) {
|
update(_, { data: { setScope } }) {
|
||||||
@ -65,8 +72,8 @@ function RuleListItem({ scope, rule, index }): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RuleListItemText({ rule }): JSX.Element {
|
function RuleListItemText({ rule }: { rule: ScopeRule }): JSX.Element {
|
||||||
let text: JSX.Element;
|
let text: JSX.Element = <div></div>;
|
||||||
|
|
||||||
if (rule.url) {
|
if (rule.url) {
|
||||||
text = <code>{rule.url}</code>;
|
text = <code>{rule.url}</code>;
|
||||||
@ -77,10 +84,14 @@ function RuleListItemText({ rule }): JSX.Element {
|
|||||||
return <ListItemText>{text}</ListItemText>;
|
return <ListItemText>{text}</ListItemText>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RuleTypeChip({ rule }): JSX.Element {
|
function RuleTypeChip({ rule }: { rule: ScopeRule }): JSX.Element {
|
||||||
|
let label = "Unknown";
|
||||||
|
|
||||||
if (rule.url) {
|
if (rule.url) {
|
||||||
return <Chip label="URL" variant="outlined" />;
|
label = "URL";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return <Chip label={label} variant="outlined" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RuleListItem;
|
export default RuleListItem;
|
||||||
|
@ -1,22 +1,9 @@
|
|||||||
import { gql, useQuery } from "@apollo/client";
|
import { gql, useQuery } from "@apollo/client";
|
||||||
import {
|
import { CircularProgress, List } from "@mui/material";
|
||||||
CircularProgress,
|
import { Alert } from "@mui/lab";
|
||||||
createStyles,
|
|
||||||
List,
|
|
||||||
makeStyles,
|
|
||||||
Theme,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { Alert } from "@material-ui/lab";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import RuleListItem from "./RuleListItem";
|
import RuleListItem from "./RuleListItem";
|
||||||
|
import { ScopeRule } from "../../lib/scope";
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
|
||||||
createStyles({
|
|
||||||
rulesList: {
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const SCOPE = gql`
|
export const SCOPE = gql`
|
||||||
query Scope {
|
query Scope {
|
||||||
@ -27,24 +14,20 @@ export const SCOPE = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function Rules(): JSX.Element {
|
function Rules(): JSX.Element {
|
||||||
const classes = useStyles();
|
const { loading, error, data } = useQuery<{ scope: ScopeRule[] }>(SCOPE);
|
||||||
const { loading, error, data } = useQuery(SCOPE);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{loading && <CircularProgress />}
|
{loading && <CircularProgress />}
|
||||||
{error && (
|
{error && <Alert severity="error">Error fetching scope: {error.message}</Alert>}
|
||||||
<Alert severity="error">Error fetching scope: {error.message}</Alert>
|
{data && data.scope.length > 0 && (
|
||||||
)}
|
<List
|
||||||
{data?.scope.length > 0 && (
|
sx={{
|
||||||
<List className={classes.rulesList}>
|
bgcolor: "background.paper",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{data.scope.map((rule, index) => (
|
{data.scope.map((rule, index) => (
|
||||||
<RuleListItem
|
<RuleListItem key={index} rule={rule} scope={data.scope} index={index} />
|
||||||
key={index}
|
|
||||||
rule={rule}
|
|
||||||
scope={data.scope}
|
|
||||||
index={index}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
|
5
admin/src/lib/Project.ts
Normal file
5
admin/src/lib/Project.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type Project = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
isActive: boolean
|
||||||
|
}
|
7
admin/src/lib/createEmotionCache.ts
Normal file
7
admin/src/lib/createEmotionCache.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import createCache from "@emotion/cache";
|
||||||
|
|
||||||
|
// prepend: true moves MUI styles to the top of the <head> so they're loaded first.
|
||||||
|
// It allows developers to easily override MUI styles with other styling solutions, like CSS modules.
|
||||||
|
export default function createEmotionCache() {
|
||||||
|
return createCache({ key: "css", prepend: true });
|
||||||
|
}
|
@ -1,7 +1,11 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
|
import { ApolloClient, HttpLink, InMemoryCache, NormalizedCacheObject } from "@apollo/client";
|
||||||
|
import merge from "deepmerge";
|
||||||
|
import isEqual from "lodash/isEqual";
|
||||||
|
|
||||||
let apolloClient;
|
export const APOLLO_STATE_PROP_NAME = "__APOLLO_STATE__";
|
||||||
|
|
||||||
|
let apolloClient: ApolloClient<NormalizedCacheObject>;
|
||||||
|
|
||||||
function createApolloClient() {
|
function createApolloClient() {
|
||||||
return new ApolloClient({
|
return new ApolloClient({
|
||||||
@ -21,9 +25,18 @@ export function initializeApollo(initialState = null) {
|
|||||||
if (initialState) {
|
if (initialState) {
|
||||||
// Get existing cache, loaded during client side data fetching
|
// Get existing cache, loaded during client side data fetching
|
||||||
const existingCache = _apolloClient.extract();
|
const existingCache = _apolloClient.extract();
|
||||||
// Restore the cache using the data passed from getStaticProps/getServerSideProps
|
|
||||||
// combined with the existing cached data
|
// Merge the existing cache into data passed from getStaticProps/getServerSideProps
|
||||||
_apolloClient.cache.restore({ ...existingCache, ...initialState });
|
const data = merge(initialState, existingCache, {
|
||||||
|
// combine arrays using object equality (like in sets)
|
||||||
|
arrayMerge: (destinationArray, sourceArray) => [
|
||||||
|
...sourceArray,
|
||||||
|
...destinationArray.filter((d) => sourceArray.every((s) => !isEqual(d, s))),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore the cache with the merged data
|
||||||
|
_apolloClient.cache.restore(data);
|
||||||
}
|
}
|
||||||
// For SSG and SSR always create a new Apollo Client
|
// For SSG and SSR always create a new Apollo Client
|
||||||
if (typeof window === "undefined") return _apolloClient;
|
if (typeof window === "undefined") return _apolloClient;
|
||||||
@ -33,7 +46,16 @@ export function initializeApollo(initialState = null) {
|
|||||||
return _apolloClient;
|
return _apolloClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useApollo(initialState) {
|
export function addApolloState(client: ApolloClient<NormalizedCacheObject>, pageProps: any) {
|
||||||
const store = useMemo(() => initializeApollo(initialState), [initialState]);
|
if (pageProps?.props) {
|
||||||
|
pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApollo(pageProps: any) {
|
||||||
|
const state = pageProps[APOLLO_STATE_PROP_NAME];
|
||||||
|
const store = useMemo(() => initializeApollo(state), [state]);
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const omitTypename = (key, value) => (key === "__typename" ? undefined : value);
|
const omitTypename = (key: string, value: any) => (key === "__typename" ? undefined : value);
|
||||||
|
|
||||||
export function withoutTypename(input: any): any {
|
export function withoutTypename(input: any): any {
|
||||||
return JSON.parse(JSON.stringify(input), omitTypename);
|
return JSON.parse(JSON.stringify(input), omitTypename);
|
||||||
|
24
admin/src/lib/requestLogs.ts
Normal file
24
admin/src/lib/requestLogs.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
export type RequestLog = {
|
||||||
|
id: string
|
||||||
|
url: string
|
||||||
|
method: string
|
||||||
|
proto: string
|
||||||
|
headers: HTTPHeader[]
|
||||||
|
body?: string
|
||||||
|
timestamp: string
|
||||||
|
response?: ResponseLog
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResponseLog = {
|
||||||
|
proto: string
|
||||||
|
statusCode: number
|
||||||
|
statusReason: string
|
||||||
|
body?: string
|
||||||
|
headers: HTTPHeader[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HTTPHeader = {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
}
|
3
admin/src/lib/scope.ts
Normal file
3
admin/src/lib/scope.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export type ScopeRule = {
|
||||||
|
url?: string
|
||||||
|
}
|
@ -1,49 +1,51 @@
|
|||||||
import { createMuiTheme } from "@material-ui/core/styles";
|
import { createTheme } from "@mui/material/styles";
|
||||||
import grey from "@material-ui/core/colors/grey";
|
import * as colors from "@mui/material/colors";
|
||||||
import teal from "@material-ui/core/colors/teal";
|
|
||||||
|
|
||||||
const theme = createMuiTheme({
|
const heading = {
|
||||||
|
fontFamily: "'JetBrains Mono', monospace",
|
||||||
|
fontWeight: 600,
|
||||||
|
};
|
||||||
|
|
||||||
|
let theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
type: "dark",
|
mode: "dark",
|
||||||
primary: {
|
primary: {
|
||||||
main: grey[900],
|
main: colors.teal["A400"],
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: teal["A400"],
|
main: colors.grey[900],
|
||||||
},
|
light: "#333",
|
||||||
info: {
|
dark: colors.common.black,
|
||||||
main: teal["A400"],
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
main: teal["A400"],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
h2: {
|
h2: heading,
|
||||||
fontFamily: "'JetBrains Mono', monospace",
|
h3: heading,
|
||||||
fontWeight: 600,
|
h4: heading,
|
||||||
|
h5: heading,
|
||||||
|
h6: heading,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
theme = createTheme(theme, {
|
||||||
|
palette: {
|
||||||
|
background: {
|
||||||
|
default: theme.palette.secondary.main,
|
||||||
|
paper: theme.palette.secondary.light,
|
||||||
},
|
},
|
||||||
h3: {
|
info: {
|
||||||
fontFamily: "'JetBrains Mono', monospace",
|
main: theme.palette.primary.main,
|
||||||
fontWeight: 600,
|
|
||||||
},
|
},
|
||||||
h4: {
|
success: {
|
||||||
fontFamily: "'JetBrains Mono', monospace",
|
main: theme.palette.primary.main,
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
h5: {
|
|
||||||
fontFamily: "'JetBrains Mono', monospace",
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
h6: {
|
|
||||||
fontFamily: "'JetBrains Mono', monospace",
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
overrides: {
|
components: {
|
||||||
MuiTableCell: {
|
MuiTableCell: {
|
||||||
stickyHeader: {
|
styleOverrides: {
|
||||||
backgroundColor: grey[900],
|
stickyHeader: {
|
||||||
|
backgroundColor: theme.palette.secondary.dark,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,32 +1,31 @@
|
|||||||
import React from "react";
|
import * as React from "react";
|
||||||
|
import Head from "next/head";
|
||||||
import { AppProps } from "next/app";
|
import { AppProps } from "next/app";
|
||||||
import { ApolloProvider } from "@apollo/client";
|
import { ApolloProvider } from "@apollo/client";
|
||||||
import Head from "next/head";
|
import { ThemeProvider } from "@mui/material/styles";
|
||||||
import { ThemeProvider } from "@material-ui/core/styles";
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
import CssBaseline from "@material-ui/core/CssBaseline";
|
import { CacheProvider, EmotionCache } from "@emotion/react";
|
||||||
|
|
||||||
|
import createEmotionCache from "../lib/createEmotionCache";
|
||||||
import theme from "../lib/theme";
|
import theme from "../lib/theme";
|
||||||
import { useApollo } from "../lib/graphql";
|
import { useApollo } from "../lib/graphql";
|
||||||
|
|
||||||
function App({ Component, pageProps }: AppProps): JSX.Element {
|
// Client-side cache, shared for the whole session of the user in the browser.
|
||||||
const apolloClient = useApollo(pageProps.initialApolloState);
|
const clientSideEmotionCache = createEmotionCache();
|
||||||
|
|
||||||
React.useEffect(() => {
|
interface MyAppProps extends AppProps {
|
||||||
// Remove the server-side injected CSS.
|
emotionCache?: EmotionCache;
|
||||||
const jssStyles = document.querySelector("#jss-server-side");
|
}
|
||||||
if (jssStyles) {
|
|
||||||
jssStyles.parentElement.removeChild(jssStyles);
|
export default function MyApp(props: MyAppProps) {
|
||||||
}
|
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
|
||||||
}, []);
|
const apolloClient = useApollo(pageProps);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<CacheProvider value={emotionCache}>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Hetty://</title>
|
<title>Hetty://</title>
|
||||||
<meta
|
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||||
name="viewport"
|
|
||||||
content="minimum-scale=1, initial-scale=1, width=device-width"
|
|
||||||
/>
|
|
||||||
</Head>
|
</Head>
|
||||||
<ApolloProvider client={apolloClient}>
|
<ApolloProvider client={apolloClient}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
@ -34,8 +33,6 @@ function App({ Component, pageProps }: AppProps): JSX.Element {
|
|||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
</React.Fragment>
|
</CacheProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React from "react";
|
import * as React from "react";
|
||||||
import Document, { Html, Head, Main, NextScript } from "next/document";
|
import Document, { Html, Head, Main, NextScript } from "next/document";
|
||||||
import { ServerStyleSheets } from "@material-ui/core/styles";
|
import createEmotionServer from "@emotion/server/create-instance";
|
||||||
|
|
||||||
|
import createEmotionCache from "../lib/createEmotionCache";
|
||||||
import theme from "../lib/theme";
|
import theme from "../lib/theme";
|
||||||
|
|
||||||
export default class MyDocument extends Document {
|
export default class MyDocument extends Document {
|
||||||
@ -11,14 +12,9 @@ export default class MyDocument extends Document {
|
|||||||
<Head>
|
<Head>
|
||||||
<meta name="theme-color" content={theme.palette.primary.main} />
|
<meta name="theme-color" content={theme.palette.primary.main} />
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css" />
|
||||||
<link
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
|
||||||
rel="stylesheet"
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" />
|
||||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
{(this.props as any).emotionStyleTags}
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap"
|
|
||||||
/>
|
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
<Main />
|
<Main />
|
||||||
@ -30,25 +26,60 @@ export default class MyDocument extends Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// `getInitialProps` belongs to `_document` (instead of `_app`),
|
// `getInitialProps` belongs to `_document` (instead of `_app`),
|
||||||
// it's compatible with server-side generation (SSG).
|
// it's compatible with static-site generation (SSG).
|
||||||
MyDocument.getInitialProps = async (ctx) => {
|
MyDocument.getInitialProps = async (ctx) => {
|
||||||
// Render app and page and get the context of the page with collected side effects.
|
// Resolution order
|
||||||
const sheets = new ServerStyleSheets();
|
//
|
||||||
|
// On the server:
|
||||||
|
// 1. app.getInitialProps
|
||||||
|
// 2. page.getInitialProps
|
||||||
|
// 3. document.getInitialProps
|
||||||
|
// 4. app.render
|
||||||
|
// 5. page.render
|
||||||
|
// 6. document.render
|
||||||
|
//
|
||||||
|
// On the server with error:
|
||||||
|
// 1. document.getInitialProps
|
||||||
|
// 2. app.render
|
||||||
|
// 3. page.render
|
||||||
|
// 4. document.render
|
||||||
|
//
|
||||||
|
// On the client
|
||||||
|
// 1. app.getInitialProps
|
||||||
|
// 2. page.getInitialProps
|
||||||
|
// 3. app.render
|
||||||
|
// 4. page.render
|
||||||
|
|
||||||
const originalRenderPage = ctx.renderPage;
|
const originalRenderPage = ctx.renderPage;
|
||||||
|
|
||||||
|
// You can consider sharing the same emotion cache between all the SSR requests to speed up performance.
|
||||||
|
// However, be aware that it can have global side effects.
|
||||||
|
const cache = createEmotionCache();
|
||||||
|
const { extractCriticalToChunks } = createEmotionServer(cache);
|
||||||
|
|
||||||
ctx.renderPage = () =>
|
ctx.renderPage = () =>
|
||||||
originalRenderPage({
|
originalRenderPage({
|
||||||
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
|
enhanceApp: (App: any) =>
|
||||||
|
function EnhanceApp(props) {
|
||||||
|
return <App emotionCache={cache} {...props} />;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialProps = await Document.getInitialProps(ctx);
|
const initialProps = await Document.getInitialProps(ctx);
|
||||||
|
// This is important. It prevents emotion to render invalid HTML.
|
||||||
|
// See https://github.com/mui-org/material-ui/issues/26561#issuecomment-855286153
|
||||||
|
const emotionStyles = extractCriticalToChunks(initialProps.html);
|
||||||
|
const emotionStyleTags = emotionStyles.styles.map((style) => (
|
||||||
|
<style
|
||||||
|
data-emotion={`${style.key} ${style.ids.join(" ")}`}
|
||||||
|
key={style.key}
|
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
|
dangerouslySetInnerHTML={{ __html: style.css }}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...initialProps,
|
...initialProps,
|
||||||
// Styles fragment is rendered after the app and page rendering finish.
|
emotionStyleTags,
|
||||||
styles: [
|
|
||||||
...React.Children.toArray(initialProps.styles),
|
|
||||||
sheets.getStyleElement(),
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Box, Link as MaterialLink, Typography } from "@material-ui/core";
|
import { Box, Link as MaterialLink, Typography } from "@mui/material";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@ -13,17 +13,13 @@ function Index(): JSX.Element {
|
|||||||
<Typography variant="h4">Get started</Typography>
|
<Typography variant="h4">Get started</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography paragraph>
|
<Typography paragraph>
|
||||||
You’ve loaded a (new) project. What’s next? You can now use the MITM
|
You’ve loaded a (new) project. What’s next? You can now use the MITM proxy and review HTTP requests and
|
||||||
proxy and review HTTP requests and responses via the{" "}
|
responses via the{" "}
|
||||||
<Link href="/proxy/logs" passHref>
|
<Link href="/proxy/logs" passHref>
|
||||||
<MaterialLink color="secondary">Proxy logs</MaterialLink>
|
<MaterialLink color="primary">Proxy logs</MaterialLink>
|
||||||
</Link>
|
</Link>
|
||||||
. Stuck? Ask for help on the{" "}
|
. Stuck? Ask for help on the{" "}
|
||||||
<MaterialLink
|
<MaterialLink href="https://github.com/dstotijn/hetty/discussions" color="primary" target="_blank">
|
||||||
href="https://github.com/dstotijn/hetty/discussions"
|
|
||||||
color="secondary"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Discussions forum
|
Discussions forum
|
||||||
</MaterialLink>
|
</MaterialLink>
|
||||||
.
|
.
|
||||||
|
@ -1,61 +1,46 @@
|
|||||||
import {
|
import { Box, Button, Typography } from "@mui/material";
|
||||||
Box,
|
import FolderIcon from "@mui/icons-material/Folder";
|
||||||
Button,
|
|
||||||
createStyles,
|
|
||||||
makeStyles,
|
|
||||||
Theme,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import FolderIcon from "@material-ui/icons/Folder";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import Layout, { Page } from "../components/Layout";
|
import Layout, { Page } from "../components/Layout";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
|
||||||
createStyles({
|
|
||||||
titleHighlight: {
|
|
||||||
color: theme.palette.secondary.main,
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
fontSize: "1.6rem",
|
|
||||||
width: "60%",
|
|
||||||
lineHeight: 2,
|
|
||||||
marginBottom: theme.spacing(5),
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
function Index(): JSX.Element {
|
function Index(): JSX.Element {
|
||||||
const classes = useStyles();
|
const highlightSx = { color: "primary.main" };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout page={Page.Home} title="">
|
<Layout page={Page.Home} title="">
|
||||||
<Box p={4}>
|
<Box p={4}>
|
||||||
<Box mb={4} width="60%">
|
<Box mb={4} width="60%">
|
||||||
<Typography variant="h2">
|
<Typography variant="h2">
|
||||||
<span className={classes.titleHighlight}>Hetty://</span>
|
<Box component="span" sx={highlightSx}>
|
||||||
|
Hetty://
|
||||||
|
</Box>
|
||||||
<br />
|
<br />
|
||||||
The simple HTTP toolkit for security research.
|
The simple HTTP toolkit for security research.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography className={classes.subtitle} paragraph>
|
<Typography
|
||||||
What if security testing was intuitive, powerful, and good looking?
|
paragraph
|
||||||
What if it was <strong>free</strong>, instead of $400 per year?{" "}
|
sx={{
|
||||||
<span className={classes.titleHighlight}>Hetty</span> is listening on{" "}
|
fontSize: "1.6rem",
|
||||||
<code>:8080</code>…
|
width: "60%",
|
||||||
|
lineHeight: 2,
|
||||||
|
mb: 5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Welcome to{" "}
|
||||||
|
<Box component="span" sx={highlightSx}>
|
||||||
|
Hetty
|
||||||
|
</Box>
|
||||||
|
. Get started by creating a project.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Link href="/projects" passHref>
|
<Link href="/projects" passHref>
|
||||||
<Button
|
<Button
|
||||||
className={classes.button}
|
sx={{ mr: 2 }}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="primary"
|
||||||
component="a"
|
component="a"
|
||||||
size="large"
|
size="large"
|
||||||
startIcon={<FolderIcon />}
|
startIcon={<FolderIcon />}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Box, Divider, Grid, Typography } from "@material-ui/core";
|
import { Box, Divider, Grid, Typography } from "@mui/material";
|
||||||
import Layout, { Page } from "../../components/Layout";
|
import Layout, { Page } from "../../components/Layout";
|
||||||
import NewProject from "../../components/projects/NewProject";
|
import NewProject from "../../components/projects/NewProject";
|
||||||
import ProjectList from "../../components/projects/ProjectList";
|
import ProjectList from "../../components/projects/ProjectList";
|
||||||
@ -11,8 +11,7 @@ function Index(): JSX.Element {
|
|||||||
<Typography variant="h4">Projects</Typography>
|
<Typography variant="h4">Projects</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography paragraph>
|
<Typography paragraph>
|
||||||
Projects contain settings and data generated/processed by Hetty. They
|
Projects contain settings and data generated/processed by Hetty. They are stored in a single database on disk.
|
||||||
are stored in a single database on disk.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box my={4}>
|
<Box my={4}>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button, Typography } from "@material-ui/core";
|
import { Button, Typography } from "@mui/material";
|
||||||
import ListIcon from "@material-ui/icons/List";
|
import ListIcon from "@mui/icons-material/List";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import Layout, { Page } from "../../components/Layout";
|
import Layout, { Page } from "../../components/Layout";
|
||||||
@ -10,13 +10,7 @@ function Index(): JSX.Element {
|
|||||||
<Layout page={Page.ProxySetup} title="Proxy setup">
|
<Layout page={Page.ProxySetup} title="Proxy setup">
|
||||||
<Typography paragraph>Coming soon…</Typography>
|
<Typography paragraph>Coming soon…</Typography>
|
||||||
<Link href="/proxy/logs" passHref>
|
<Link href="/proxy/logs" passHref>
|
||||||
<Button
|
<Button variant="contained" color="primary" component="a" size="large" startIcon={<ListIcon />}>
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
component="a"
|
|
||||||
size="large"
|
|
||||||
startIcon={<ListIcon />}
|
|
||||||
>
|
|
||||||
View logs
|
View logs
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Box } from "@material-ui/core";
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
import LogsOverview from "../../../components/reqlog/LogsOverview";
|
import LogsOverview from "../../../components/reqlog/LogsOverview";
|
||||||
import Layout, { Page } from "../../../components/Layout";
|
import Layout, { Page } from "../../../components/Layout";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Box, Divider, Grid, Typography } from "@material-ui/core";
|
import { Box, Divider, Grid, Typography } from "@mui/material";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import Layout, { Page } from "../../components/Layout";
|
import Layout, { Page } from "../../components/Layout";
|
||||||
@ -13,11 +13,9 @@ function Index(): JSX.Element {
|
|||||||
<Typography variant="h4">Scope</Typography>
|
<Typography variant="h4">Scope</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography paragraph>
|
<Typography paragraph>
|
||||||
Scope rules are used by various modules in Hetty and can influence
|
Scope rules are used by various modules in Hetty and can influence their behavior. For example: the Proxy logs
|
||||||
their behavior. For example: the Proxy logs module can match incoming
|
module can match incoming requests against scope rules and decide its behavior (e.g. log or bypass) based on
|
||||||
requests against scope rules and decide its behavior (e.g. log or
|
the outcome of the match. All scope configuration is stored per project.
|
||||||
bypass) based on the outcome of the match. All scope configuration is
|
|
||||||
stored per project.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box my={4}>
|
<Box my={4}>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Box, Typography } from "@material-ui/core";
|
import { Box, Typography } from "@mui/material";
|
||||||
|
|
||||||
import Layout, { Page } from "../../components/Layout";
|
import Layout, { Page } from "../../components/Layout";
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
],
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": false,
|
"strict": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
@ -16,7 +16,8 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve"
|
"jsx": "preserve",
|
||||||
|
"incremental": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
|
7010
admin/yarn.lock
7010
admin/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user