diff --git a/admin/package.json b/admin/package.json index 59c313d..5257d45 100644 --- a/admin/package.json +++ b/admin/package.json @@ -19,6 +19,7 @@ "@mui/icons-material": "^5.3.1", "@mui/lab": "^5.0.0-alpha.66", "@mui/material": "^5.3.1", + "@mui/styles": "^5.4.2", "allotment": "^1.9.0", "deepmerge": "^4.2.2", "graphql": "^16.2.0", @@ -27,7 +28,8 @@ "next": "^12.0.8", "next-fonts": "^1.0.3", "react": "^17.0.2", - "react-dom": "^17.0.2" + "react-dom": "^17.0.2", + "react-split-pane": "^0.1.92" }, "devDependencies": { "@babel/core": "^7.0.0", diff --git a/admin/src/features/Layout.tsx b/admin/src/features/Layout.tsx index 5368793..d4fd924 100644 --- a/admin/src/features/Layout.tsx +++ b/admin/src/features/Layout.tsx @@ -27,6 +27,8 @@ import MuiListItemIcon, { ListItemIconProps } from "@mui/material/ListItemIcon"; import Link from "next/link"; import React, { useState } from "react"; +import { useActiveProject } from "lib/ActiveProjectContext"; + export enum Page { Home, GetStarted, @@ -132,6 +134,7 @@ interface Props { } export function Layout({ title, page, children }: Props): JSX.Element { + const activeProject = useActiveProject(); const theme = useTheme(); const [open, setOpen] = useState(false); @@ -200,7 +203,7 @@ export function Layout({ title, page, children }: Props): JSX.Element { - + @@ -210,7 +213,7 @@ export function Layout({ title, page, children }: Props): JSX.Element { - + @@ -220,7 +223,7 @@ export function Layout({ title, page, children }: Props): JSX.Element { - + diff --git a/admin/src/features/reqlog/components/HttpHeadersTable.tsx b/admin/src/features/reqlog/components/HttpHeadersTable.tsx deleted file mode 100644 index 717f92d..0000000 --- a/admin/src/features/reqlog/components/HttpHeadersTable.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { Alert } from "@mui/lab"; -import { Table, TableBody, TableCell, TableContainer, TableRow, Snackbar, SxProps, Theme } from "@mui/material"; -import React, { useState } from "react"; - -const baseCellStyle: SxProps = { - px: 0, - py: 0.33, - verticalAlign: "top", - border: "none", - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - "&:hover": { - color: "primary.main", - whiteSpace: "inherit", - overflow: "inherit", - textOverflow: "inherit", - cursor: "copy", - }, -}; - -const keyCellStyle = { - ...baseCellStyle, - pr: 1, - width: "40%", - fontWeight: "bold", - fontSize: ".75rem", -}; - -const valueCellStyle = { - ...baseCellStyle, - width: "60%", - border: "none", - fontSize: ".75rem", -}; - -interface Props { - headers: Array<{ key: string; value: string }>; -} - -function HttpHeadersTable({ headers }: Props): JSX.Element { - const [open, setOpen] = useState(false); - - const handleClick = (e: React.MouseEvent) => { - e.preventDefault(); - - const windowSel = window.getSelection(); - - if (!windowSel || !document) { - return; - } - - const r = document.createRange(); - r.selectNode(e.currentTarget); - windowSel.removeAllRanges(); - windowSel.addRange(r); - document.execCommand("copy"); - windowSel.removeAllRanges(); - - setOpen(true); - }; - - const handleClose = (event: Event | React.SyntheticEvent, reason?: string) => { - if (reason === "clickaway") { - return; - } - - setOpen(false); - }; - - return ( -
- - - Copied to clipboard. - - - - - - {headers.map(({ key, value }, index) => ( - - - {key}: - - - {value} - - - ))} - -
-
-
- ); -} - -export default HttpHeadersTable; diff --git a/admin/src/features/reqlog/components/LogDetail.tsx b/admin/src/features/reqlog/components/LogDetail.tsx index f672bb1..5416bd9 100644 --- a/admin/src/features/reqlog/components/LogDetail.tsx +++ b/admin/src/features/reqlog/components/LogDetail.tsx @@ -1,18 +1,20 @@ import Alert from "@mui/lab/Alert"; -import { Box, Grid, Paper, CircularProgress } from "@mui/material"; +import { Box, CircularProgress, Paper, Typography } from "@mui/material"; import RequestDetail from "./RequestDetail"; -import ResponseDetail from "./ResponseDetail"; +import Response from "lib/components/Response"; +import SplitPane from "lib/components/SplitPane"; import { useHttpRequestLogQuery } from "lib/graphql/generated"; interface Props { - requestId: string; + id?: string; } -function LogDetail({ requestId: id }: Props): JSX.Element { +function LogDetail({ id }: Props): JSX.Element { const { loading, error, data } = useHttpRequestLogQuery({ - variables: { id }, + variables: { id: id as string }, + skip: id === undefined, }); if (loading) { @@ -31,28 +33,24 @@ function LogDetail({ requestId: id }: Props): JSX.Element { } if (!data?.httpRequestLog) { - return
; + return ( + + Select a log entry… + + ); } - const httpRequestLog = data.httpRequestLog; + const reqLog = data.httpRequestLog; return ( -
- - - - - - - - {httpRequestLog.response && ( - - - - )} - - -
+ + + {reqLog.response && ( + + + + )} + ); } diff --git a/admin/src/features/reqlog/components/LogsOverview.tsx b/admin/src/features/reqlog/components/LogsOverview.tsx deleted file mode 100644 index 9bba0cd..0000000 --- a/admin/src/features/reqlog/components/LogsOverview.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import Alert from "@mui/lab/Alert"; -import { Box, CircularProgress, Link as MaterialLink, Typography } from "@mui/material"; -import Link from "next/link"; -import { useRouter } from "next/router"; - -import LogDetail from "./LogDetail"; -import RequestList from "./RequestList"; - -import CenteredPaper from "lib/components/CenteredPaper"; -import { useHttpRequestLogsQuery } from "lib/graphql/generated"; - -export default function LogsOverview(): JSX.Element { - const router = useRouter(); - const detailReqLogId = router.query.id as string | undefined; - const { loading, error, data } = useHttpRequestLogsQuery({ - pollInterval: 1000, - }); - - const handleLogClick = (reqId: string) => { - router.push("/proxy/logs?id=" + reqId, undefined, { - shallow: false, - }); - }; - - if (loading) { - return ; - } - if (error) { - if (error.graphQLErrors[0]?.extensions?.code === "no_active_project") { - return ( - - There is no project active.{" "} - - Create or open - {" "} - one first. - - ); - } - return Error fetching logs: {error.message}; - } - - const logs = data?.httpRequestLogs || []; - - return ( -
- - - - - {detailReqLogId && } - {logs.length !== 0 && !detailReqLogId && ( - - Select a log entry… - - )} - -
- ); -} diff --git a/admin/src/features/reqlog/components/RequestDetail.tsx b/admin/src/features/reqlog/components/RequestDetail.tsx index f7afedd..48c60e6 100644 --- a/admin/src/features/reqlog/components/RequestDetail.tsx +++ b/admin/src/features/reqlog/components/RequestDetail.tsx @@ -1,51 +1,46 @@ -import { Typography, Box, Divider } from "@mui/material"; +import { Typography, Box } from "@mui/material"; import React from "react"; -import HttpHeadersTable from "./HttpHeadersTable"; - -import Editor from "lib/components/Editor"; +import RequestTabs from "lib/components/RequestTabs"; import { HttpRequestLogQuery } from "lib/graphql/generated"; +import { queryParamsFromURL } from "lib/queryParamsFromURL"; interface Props { request: NonNullable; } function RequestDetail({ request }: Props): JSX.Element { - const { method, url, proto, headers, body } = request; + const { method, url, headers, body } = request; - const contentType = headers.find((header) => header.key === "Content-Type")?.value; const parsedUrl = new URL(url); return ( -
- + + Request - {method} {decodeURIComponent(parsedUrl.pathname + parsedUrl.search)}{" "} - - {proto} - + {method} {decodeURIComponent(parsedUrl.pathname + parsedUrl.search)} - - - - + + - - {body && } -
+ ); } diff --git a/admin/src/features/reqlog/components/RequestList.tsx b/admin/src/features/reqlog/components/RequestList.tsx deleted file mode 100644 index 5079ada..0000000 --- a/admin/src/features/reqlog/components/RequestList.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { - TableContainer, - Paper, - Table, - TableHead, - TableRow, - TableCell, - TableBody, - Typography, - Box, - useTheme, - MenuItem, - Snackbar, - Alert, - Link, -} from "@mui/material"; -import React, { useState } from "react"; - -import CenteredPaper from "lib/components/CenteredPaper"; -import HttpStatusIcon from "lib/components/HttpStatusIcon"; -import useContextMenu from "lib/components/useContextMenu"; -import { HttpRequestLogsQuery, useCreateSenderRequestFromHttpRequestLogMutation } from "lib/graphql/generated"; - -interface Props { - logs: NonNullable; - selectedReqLogId?: string; - onLogClick(requestId: string): void; -} - -export default function RequestList({ logs, onLogClick, selectedReqLogId }: Props): JSX.Element { - return ( -
- - {logs.length === 0 && ( - - - No logs found. - - - )} -
- ); -} - -interface RequestListTableProps { - logs: HttpRequestLogsQuery["httpRequestLogs"]; - selectedReqLogId?: string; - onLogClick(requestId: string): void; -} - -function RequestListTable({ logs, selectedReqLogId, onLogClick }: RequestListTableProps): JSX.Element { - const theme = useTheme(); - - const [createSenderReqFromLog] = useCreateSenderRequestFromHttpRequestLogMutation({}); - - const [copyToSenderId, setCopyToSenderId] = useState(""); - const [Menu, handleContextMenu, handleContextMenuClose] = useContextMenu(); - - const handleCopyToSenderClick = () => { - createSenderReqFromLog({ - variables: { - id: copyToSenderId, - }, - onCompleted({ createSenderRequestFromHttpRequestLog }) { - const { id } = createSenderRequestFromHttpRequestLog; - setNewSenderReqId(id); - setCopiedReqNotifOpen(true); - }, - }); - handleContextMenuClose(); - }; - - const [newSenderReqId, setNewSenderReqId] = useState(""); - const [copiedReqNotifOpen, setCopiedReqNotifOpen] = useState(false); - const handleCloseCopiedNotif = (_: Event | React.SyntheticEvent, reason?: string) => { - if (reason === "clickaway") { - return; - } - setCopiedReqNotifOpen(false); - }; - - return ( -
- - Copy request to Sender - - - - Request was copied. Edit in Sender. - - - - - - - - Method - Origin - Path - Status - - - - {logs.map(({ id, method, url, response }) => { - const { origin, pathname, search, hash } = new URL(url); - - const cellStyle = { - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - }; - - return ( - onLogClick(id)} - onContextMenu={(e) => { - setCopyToSenderId(id); - handleContextMenu(e); - }} - > - - {method} - - {origin} - - {decodeURIComponent(pathname + search + hash)} - - - {response && ( -
- {" "} - - {response.statusCode} {response.statusReason} - -
- )} -
-
- ); - })} -
-
-
-
- ); -} diff --git a/admin/src/features/reqlog/components/RequestLogs.tsx b/admin/src/features/reqlog/components/RequestLogs.tsx new file mode 100644 index 0000000..df505b0 --- /dev/null +++ b/admin/src/features/reqlog/components/RequestLogs.tsx @@ -0,0 +1,90 @@ +import { Alert, Box, Link, MenuItem, Snackbar } from "@mui/material"; +import { useRouter } from "next/router"; +import { useState } from "react"; + +import LogDetail from "./LogDetail"; +import Search from "./Search"; + +import RequestsTable from "lib/components/RequestsTable"; +import SplitPane from "lib/components/SplitPane"; +import useContextMenu from "lib/components/useContextMenu"; +import { useCreateSenderRequestFromHttpRequestLogMutation, useHttpRequestLogsQuery } from "lib/graphql/generated"; + +export function RequestLogs(): JSX.Element { + const router = useRouter(); + const id = router.query.id as string | undefined; + const { data } = useHttpRequestLogsQuery({ + pollInterval: 1000, + }); + + const [createSenderReqFromLog] = useCreateSenderRequestFromHttpRequestLogMutation({}); + + const [copyToSenderId, setCopyToSenderId] = useState(""); + const [Menu, handleContextMenu, handleContextMenuClose] = useContextMenu(); + + const handleCopyToSenderClick = () => { + createSenderReqFromLog({ + variables: { + id: copyToSenderId, + }, + onCompleted({ createSenderRequestFromHttpRequestLog }) { + const { id } = createSenderRequestFromHttpRequestLog; + setNewSenderReqId(id); + setCopiedReqNotifOpen(true); + }, + }); + handleContextMenuClose(); + }; + + const [newSenderReqId, setNewSenderReqId] = useState(""); + const [copiedReqNotifOpen, setCopiedReqNotifOpen] = useState(false); + const handleCloseCopiedNotif = (_: Event | React.SyntheticEvent, reason?: string) => { + if (reason === "clickaway") { + return; + } + setCopiedReqNotifOpen(false); + }; + + const handleRowClick = (id: string) => { + router.push(`/proxy/logs?id=${id}`); + }; + + const handleRowContextClick = (e: React.MouseEvent, id: string) => { + setCopyToSenderId(id); + handleContextMenu(e); + }; + + return ( + + + + + + + + Copy request to Sender + + + + Request was copied. Edit in Sender. + + + + + + + + + + ); +} diff --git a/admin/src/features/reqlog/components/ResponseDetail.tsx b/admin/src/features/reqlog/components/ResponseDetail.tsx deleted file mode 100644 index 7b839f3..0000000 --- a/admin/src/features/reqlog/components/ResponseDetail.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Typography, Box, Divider } from "@mui/material"; - -import HttpHeadersTable from "./HttpHeadersTable"; - -import Editor from "lib/components/Editor"; -import HttpStatusIcon from "lib/components/HttpStatusIcon"; -import { HttpRequestLogQuery } from "lib/graphql/generated"; - -interface Props { - response: NonNullable["response"]>; -} - -function ResponseDetail({ response }: Props): JSX.Element { - const contentType = response.headers.find((header) => header.key.toLowerCase() === "content-type")?.value; - - return ( -
- - - Response - - - {" "} - - - {response.proto} - - {" "} - {response.statusCode} {response.statusReason} - - - - - - - - - - {response.body && } -
- ); -} - -export default ResponseDetail; diff --git a/admin/src/features/reqlog/components/Search.tsx b/admin/src/features/reqlog/components/Search.tsx index 842771c..93406d4 100644 --- a/admin/src/features/reqlog/components/Search.tsx +++ b/admin/src/features/reqlog/components/Search.tsx @@ -17,8 +17,7 @@ import { import IconButton from "@mui/material/IconButton"; import React, { useRef, useState } from "react"; -import { ConfirmationDialog, useConfirmationDialog } from "./ConfirmationDialog"; - +import { ConfirmationDialog, useConfirmationDialog } from "lib/components/ConfirmationDialog"; import { HttpRequestLogFilterDocument, HttpRequestLogsDocument, diff --git a/admin/src/features/reqlog/graphql/httpRequestLog.graphql b/admin/src/features/reqlog/graphql/httpRequestLog.graphql index 1facdb8..d96aa63 100644 --- a/admin/src/features/reqlog/graphql/httpRequestLog.graphql +++ b/admin/src/features/reqlog/graphql/httpRequestLog.graphql @@ -10,6 +10,7 @@ query HttpRequestLog($id: ID!) { } body response { + id proto headers { key diff --git a/admin/src/features/reqlog/index.ts b/admin/src/features/reqlog/index.ts new file mode 100644 index 0000000..4e1de72 --- /dev/null +++ b/admin/src/features/reqlog/index.ts @@ -0,0 +1,3 @@ +import { RequestLogs } from "./components/RequestLogs"; + +export default RequestLogs; diff --git a/admin/src/features/sender/components/EditRequest.tsx b/admin/src/features/sender/components/EditRequest.tsx index e903419..b2706c9 100644 --- a/admin/src/features/sender/components/EditRequest.tsx +++ b/admin/src/features/sender/components/EditRequest.tsx @@ -10,14 +10,13 @@ import { TextField, Typography, } from "@mui/material"; -import { AllotmentProps, PaneProps } from "allotment/dist/types/src/allotment"; import { useRouter } from "next/router"; -import React, { useEffect, useRef, useState } from "react"; - -import EditRequestTabs from "./EditRequestTabs"; -import { KeyValuePair, sortKeyValuePairs } from "./KeyValuePair"; -import Response from "./Response"; +import React, { useState } from "react"; +import { KeyValuePair, sortKeyValuePairs } from "lib/components/KeyValuePair"; +import RequestTabs from "lib/components/RequestTabs"; +import Response from "lib/components/Response"; +import SplitPane from "lib/components/SplitPane"; import { GetSenderRequestQuery, useCreateOrUpdateSenderRequestMutation, @@ -25,8 +24,7 @@ import { useGetSenderRequestQuery, useSendRequestMutation, } from "lib/graphql/generated"; - -import "allotment/dist/style.css"; +import { queryParamsFromURL } from "lib/queryParamsFromURL"; enum HttpMethod { Get = "GET", @@ -88,22 +86,6 @@ function updateURLQueryParams(url: string, queryParams: KeyValuePair[]) { return newURL + "?" + rawQueryParams; } -function queryParamsFromURL(url: string): KeyValuePair[] { - const questionMarkIndex = url.indexOf("?"); - if (questionMarkIndex === -1) { - return []; - } - - const queryParams: KeyValuePair[] = []; - - const searchParams = new URLSearchParams(url.slice(questionMarkIndex + 1)); - for (const [key, value] of searchParams) { - queryParams.push({ key, value }); - } - - return queryParams; -} - function EditRequest(): JSX.Element { const router = useRouter(); const reqId = router.query.id as string | undefined; @@ -219,28 +201,6 @@ function EditRequest(): JSX.Element { createOrUpdateRequestAndSend(); }; - const isMountedRef = useRef(false); - const [Allotment, setAllotment] = useState< - (React.ComponentType & { Pane: React.ComponentType }) | null - >(null); - useEffect(() => { - isMountedRef.current = true; - import("allotment") - .then((mod) => { - if (!isMountedRef.current) { - return; - } - setAllotment(mod.Allotment); - }) - .catch((err) => console.error(err, `could not import allotment ${err.message}`)); - return () => { - isMountedRef.current = false; - }; - }, []); - if (!Allotment) { - return
Loading...
; - } - return ( @@ -276,31 +236,27 @@ function EditRequest(): JSX.Element { )} - - - - - - Request - - - + + + + + Request + + - - - - + + - + ); diff --git a/admin/src/features/sender/components/History.tsx b/admin/src/features/sender/components/History.tsx index 77fb610..b32d2d9 100644 --- a/admin/src/features/sender/components/History.tsx +++ b/admin/src/features/sender/components/History.tsx @@ -1,8 +1,7 @@ -import { TableContainer, Table, TableHead, TableRow, TableCell, Typography, Box, TableBody } from "@mui/material"; +import { Box, Paper, Typography } from "@mui/material"; import { useRouter } from "next/router"; -import CenteredPaper from "lib/components/CenteredPaper"; -import HttpStatusIcon from "lib/components/HttpStatusIcon"; +import RequestsTable from "lib/components/RequestsTable"; import { useGetSenderRequestsQuery } from "lib/graphql/generated"; function History(): JSX.Element { @@ -20,71 +19,13 @@ function History(): JSX.Element { return ( {!loading && data?.senderRequests && data?.senderRequests.length > 0 && ( - - - - - Method - Origin - Path - Status - - - - {data?.senderRequests && - data.senderRequests.map(({ id, method, url, response }) => { - const { origin, pathname, search, hash } = new URL(url); - - const cellStyle = { - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - }; - - return ( - handleRowClick(id)} - > - - {method} - - {origin} - - {decodeURIComponent(pathname + search + hash)} - - - {response && ( -
- {" "} - - {response.statusCode} {response.statusReason} - -
- )} -
-
- ); - })} -
-
-
+ )} {!loading && data?.senderRequests.length === 0 && ( - + No requests created yet. - + )}
diff --git a/admin/src/features/sender/components/KeyValuePair.tsx b/admin/src/features/sender/components/KeyValuePair.tsx deleted file mode 100644 index f4a57df..0000000 --- a/admin/src/features/sender/components/KeyValuePair.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import ClearIcon from "@mui/icons-material/Clear"; -import { IconButton, InputBase, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material"; - -export interface KeyValuePair { - key: string; - value: string; -} - -export interface KeyValuePairTableProps { - items: KeyValuePair[]; - onChange?: (key: string, value: string, index: number) => void; - onDelete?: (index: number) => void; -} - -export function KeyValuePairTable({ items, onChange, onDelete }: KeyValuePairTableProps): JSX.Element { - const inputSx = { - fontSize: "0.875rem", - "&.MuiInputBase-root input": { - p: 0, - }, - }; - - return ( - - - - - Key - Value - {onDelete && } - - - - {items.map(({ key, value }, idx) => ( - - - {!onChange && {key}} - {onChange && ( - { - onChange && onChange(e.target.value, value, idx); - }} - sx={inputSx} - /> - )} - - - {!onChange && value} - {onChange && ( - { - onChange && onChange(key, e.target.value, idx); - }} - sx={inputSx} - /> - )} - - {onDelete && ( - -
- { - onDelete && onDelete(idx); - }} - sx={{ - visibility: onDelete === undefined || items.length === idx + 1 ? "hidden" : "inherit", - }} - > - - -
-
- )} -
- ))} -
-
-
- ); -} - -export function sortKeyValuePairs(items: KeyValuePair[]): KeyValuePair[] { - const sorted = [...items]; - - sorted.sort((a, b) => { - if (a.key < b.key) { - return -1; - } - if (a.key > b.key) { - return 1; - } - return 0; - }); - - return sorted; -} - -export default KeyValuePairTable; diff --git a/admin/src/features/sender/components/Sender.tsx b/admin/src/features/sender/components/Sender.tsx new file mode 100644 index 0000000..8bc315e --- /dev/null +++ b/admin/src/features/sender/components/Sender.tsx @@ -0,0 +1,21 @@ +import { Box } from "@mui/material"; + +import EditRequest from "./EditRequest"; +import History from "./History"; + +import SplitPane from "lib/components/SplitPane"; + +export default function Sender(): JSX.Element { + return ( + + + + + + + + + + + ); +} diff --git a/admin/src/features/sender/index.ts b/admin/src/features/sender/index.ts new file mode 100644 index 0000000..d6ed6bf --- /dev/null +++ b/admin/src/features/sender/index.ts @@ -0,0 +1,3 @@ +import Sender from "./components/Sender"; + +export default Sender; diff --git a/admin/src/lib/ActiveProjectContext.tsx b/admin/src/lib/ActiveProjectContext.tsx new file mode 100644 index 0000000..3bc35bc --- /dev/null +++ b/admin/src/lib/ActiveProjectContext.tsx @@ -0,0 +1,20 @@ +import React, { createContext, useContext } from "react"; + +import { Project, useProjectsQuery } from "./graphql/generated"; + +const ActiveProjectContext = createContext(null); + +interface Props { + children?: React.ReactNode | undefined; +} + +export function ActiveProjectProvider({ children }: Props): JSX.Element { + const { data } = useProjectsQuery(); + const project = data?.projects.find((project) => project.isActive) || null; + + return {children}; +} + +export function useActiveProject() { + return useContext(ActiveProjectContext); +} diff --git a/admin/src/lib/components/CenteredPaper.tsx b/admin/src/lib/components/CenteredPaper.tsx deleted file mode 100644 index 2fa73b7..0000000 --- a/admin/src/lib/components/CenteredPaper.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Paper } from "@mui/material"; - -function CenteredPaper({ children }: { children: React.ReactNode }): JSX.Element { - return ( -
- - {children} - -
- ); -} - -export default CenteredPaper; diff --git a/admin/src/features/reqlog/components/ConfirmationDialog.tsx b/admin/src/lib/components/ConfirmationDialog.tsx similarity index 100% rename from admin/src/features/reqlog/components/ConfirmationDialog.tsx rename to admin/src/lib/components/ConfirmationDialog.tsx diff --git a/admin/src/lib/components/KeyValuePair.tsx b/admin/src/lib/components/KeyValuePair.tsx new file mode 100644 index 0000000..ce8167e --- /dev/null +++ b/admin/src/lib/components/KeyValuePair.tsx @@ -0,0 +1,201 @@ +import ClearIcon from "@mui/icons-material/Clear"; +import { + Alert, + IconButton, + InputBase, + InputBaseProps, + Snackbar, + styled, + Table, + TableBody, + TableCell, + TableCellProps, + TableContainer, + TableHead, + TableRow, + TableRowProps, +} from "@mui/material"; +import { useState } from "react"; + +const StyledInputBase = styled(InputBase)(() => ({ + fontSize: "0.875rem", + "&.MuiInputBase-root input": { + p: 0, + }, +})); + +const StyledTableRow = styled(TableRow)(() => ({ + "& .delete-button": { + visibility: "hidden", + }, + "&:hover .delete-button": { + visibility: "inherit", + }, +})); + +export interface KeyValuePair { + key: string; + value: string; +} + +export interface KeyValuePairTableProps { + items: KeyValuePair[]; + onChange?: (key: string, value: string, index: number) => void; + onDelete?: (index: number) => void; +} + +export function KeyValuePairTable({ items, onChange, onDelete }: KeyValuePairTableProps): JSX.Element { + const [copyConfOpen, setCopyConfOpen] = useState(false); + + const handleCellClick = (e: React.MouseEvent) => { + e.preventDefault(); + + const windowSel = window.getSelection(); + + if (!windowSel || !document) { + return; + } + + const r = document.createRange(); + r.selectNode(e.currentTarget); + windowSel.removeAllRanges(); + windowSel.addRange(r); + document.execCommand("copy"); + windowSel.removeAllRanges(); + + setCopyConfOpen(true); + }; + + const handleCopyConfClose = (_: Event | React.SyntheticEvent, reason?: string) => { + if (reason === "clickaway") { + return; + } + + setCopyConfOpen(false); + }; + + const baseCellStyle = { + "&:hover": { + cursor: "copy", + }, + }; + + const KeyTableCell = styled(TableCell)(() => (!onChange ? baseCellStyle : {})); + const ValueTableCell = styled(TableCell)(() => ({ + ...(!onChange && baseCellStyle), + width: "60%", + wordBreak: "break-all", + })); + + return ( +
+ + + Copied to clipboard. + + + + + + + Key + Value + {onDelete && } + + + + {items.map(({ key, value }, idx) => ( + + { + !onChange && handleCellClick(e); + }} + > + {!onChange && {key}} + {onChange && ( + { + onChange && onChange(e.target.value, value, idx); + }} + /> + )} + + { + !onChange && handleCellClick(e); + }} + > + {!onChange && value} + {onChange && ( + { + onChange && onChange(key, e.target.value, idx); + }} + /> + )} + + {onDelete && ( + +
+ { + onDelete && onDelete(idx); + }} + sx={{ + visibility: onDelete === undefined || items.length === idx + 1 ? "hidden" : "inherit", + }} + > + + +
+
+ )} +
+ ))} +
+
+
+
+ ); +} + +export function sortKeyValuePairs(items: KeyValuePair[]): KeyValuePair[] { + const sorted = [...items]; + + sorted.sort((a, b) => { + if (a.key < b.key) { + return -1; + } + if (a.key > b.key) { + return 1; + } + return 0; + }); + + return sorted; +} + +export default KeyValuePairTable; diff --git a/admin/src/features/sender/components/EditRequestTabs.tsx b/admin/src/lib/components/RequestTabs.tsx similarity index 60% rename from admin/src/features/sender/components/EditRequestTabs.tsx rename to admin/src/lib/components/RequestTabs.tsx index 2742ff7..8aa9876 100644 --- a/admin/src/features/sender/components/EditRequestTabs.tsx +++ b/admin/src/lib/components/RequestTabs.tsx @@ -12,18 +12,18 @@ enum TabValue { Body = "body", } -interface EditRequestTabsProps { +interface RequestTabsProps { queryParams: KeyValuePair[]; headers: KeyValuePair[]; - onQueryParamChange: KeyValuePairTableProps["onChange"]; - onQueryParamDelete: KeyValuePairTableProps["onDelete"]; - onHeaderChange: KeyValuePairTableProps["onChange"]; - onHeaderDelete: KeyValuePairTableProps["onDelete"]; - body: string; - onBodyChange: (value: string) => void; + onQueryParamChange?: KeyValuePairTableProps["onChange"]; + onQueryParamDelete?: KeyValuePairTableProps["onDelete"]; + onHeaderChange?: KeyValuePairTableProps["onChange"]; + onHeaderDelete?: KeyValuePairTableProps["onDelete"]; + body?: string | null; + onBodyChange?: (value: string) => void; } -function EditRequestTabs(props: EditRequestTabsProps): JSX.Element { +function RequestTabs(props: RequestTabsProps): JSX.Element { const { queryParams, onQueryParamChange, @@ -40,46 +40,45 @@ function EditRequestTabs(props: EditRequestTabsProps): JSX.Element { textTransform: "none", }; + const queryParamsLength = onQueryParamChange ? queryParams.length - 1 : queryParams.length; + const headersLength = onHeaderChange ? headers.length - 1 : headers.length; + return ( - + - + setTabValue(value)}> - + 1 ? "s" : "") + ")" : "")} + label={"Body" + (body?.length ? ` (${body.length} byte` + (body.length > 1 ? "s" : "") + ")" : "")} sx={tabSx} /> - - + + - + { - onBodyChange(value || ""); + onBodyChange && onBodyChange(value || ""); }} - monacoOptions={{ readOnly: false }} + monacoOptions={{ readOnly: onBodyChange === undefined }} contentType={headers.find(({ key }) => key.toLowerCase() === "content-type")?.value} /> @@ -89,4 +88,4 @@ function EditRequestTabs(props: EditRequestTabsProps): JSX.Element { ); } -export default EditRequestTabs; +export default RequestTabs; diff --git a/admin/src/lib/components/RequestsTable.tsx b/admin/src/lib/components/RequestsTable.tsx new file mode 100644 index 0000000..baa9b17 --- /dev/null +++ b/admin/src/lib/components/RequestsTable.tsx @@ -0,0 +1,125 @@ +import { + TableContainer, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + styled, + TableCellProps, + TableRowProps, +} from "@mui/material"; + +import HttpStatusIcon from "./HttpStatusIcon"; + +import { HttpMethod } from "lib/graphql/generated"; + +const baseCellStyle = { + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", +} as const; + +const MethodTableCell = styled(TableCell)(() => ({ + ...baseCellStyle, + width: "100px", +})); + +const OriginTableCell = styled(TableCell)(() => ({ + ...baseCellStyle, + maxWidth: "100px", +})); + +const PathTableCell = styled(TableCell)(() => ({ + ...baseCellStyle, + maxWidth: "200px", +})); + +const StatusTableCell = styled(TableCell)(() => ({ + ...baseCellStyle, + width: "100px", +})); + +const RequestTableRow = styled(TableRow)(() => ({ + "&:hover": { + cursor: "pointer", + }, +})); + +interface HttpRequest { + id: string; + url: string; + method: HttpMethod; + response?: HttpResponse | null; +} + +interface HttpResponse { + statusCode: number; + statusReason: string; + body?: string; +} + +interface Props { + requests: HttpRequest[]; + activeRowId?: string; + onRowClick?: (id: string) => void; + onContextMenu?: (e: React.MouseEvent, id: string) => void; +} + +export default function RequestsTable(props: Props): JSX.Element { + const { requests, activeRowId, onRowClick, onContextMenu } = props; + + return ( + + + + + Method + Origin + Path + Status + + + + {requests.map(({ id, method, url, response }) => { + const { origin, pathname, search, hash } = new URL(url); + + return ( + { + onRowClick && onRowClick(id); + }} + onContextMenu={(e) => { + onContextMenu && onContextMenu(e, id); + }} + > + + {method} + + {origin} + {decodeURIComponent(pathname + search + hash)} + + {response && } + + + ); + })} + +
+
+ ); +} + +function Status({ code, reason }: { code: number; reason: string }): JSX.Element { + return ( +
+ {" "} + + {code} {reason} + +
+ ); +} diff --git a/admin/src/features/sender/components/Response.tsx b/admin/src/lib/components/Response.tsx similarity index 54% rename from admin/src/features/sender/components/Response.tsx rename to admin/src/lib/components/Response.tsx index 22cec12..2f30f26 100644 --- a/admin/src/features/sender/components/Response.tsx +++ b/admin/src/lib/components/Response.tsx @@ -13,22 +13,20 @@ interface ResponseProps { function Response({ response }: ResponseProps): JSX.Element { return ( -
- - - Response - - {response && ( - - - - )} - -
+ + + Response + + {response && ( + + + + )} + + Response not received yet. - + ); function ResponseTabs(props: ResponseTabsProps): JSX.Element { @@ -38,7 +36,7 @@ function ResponseTabs(props: ResponseTabsProps): JSX.Element { return ( - + setTabValue(value)}> (({ theme }) => ({ + ".Resizer": { + zIndex: theme.zIndex.mobileStepper, + boxSizing: "border-box", + backgroundClip: "padding-box", + backgroundColor: alpha(theme.palette.grey[400], 0.05), + }, + ".Resizer:hover": { + transition: "all 0.5s ease", + backgroundColor: alpha(theme.palette.primary.main, 1), + }, + + ".Resizer.horizontal": { + height: theme.spacing(SIZE_FACTOR), + marginTop: theme.spacing(MARGIN_FACTOR), + marginBottom: theme.spacing(MARGIN_FACTOR), + borderTop: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`, + borderBottom: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`, + borderBottomColor: "rgba(255, 255, 255, 0)", + cursor: "row-resize", + width: "100%", + }, + + ".Resizer.vertical": { + width: theme.spacing(SIZE_FACTOR), + marginLeft: theme.spacing(MARGIN_FACTOR), + marginRight: theme.spacing(MARGIN_FACTOR), + borderLeft: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`, + borderRight: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`, + cursor: "col-resize", + }, + + ".Resizer.disabled": { + cursor: "not-allowed", + }, + + ".Resizer.disabled:hover": { + borderColor: "transparent", + }, + + ".Pane": { + overflow: "hidden", + }, +})); + +export default SplitPane; diff --git a/admin/src/lib/graphql/generated.tsx b/admin/src/lib/graphql/generated.tsx index a08e322..ab0cfba 100644 --- a/admin/src/lib/graphql/generated.tsx +++ b/admin/src/lib/graphql/generated.tsx @@ -288,7 +288,7 @@ export type HttpRequestLogQueryVariables = Exact<{ }>; -export type HttpRequestLogQuery = { __typename?: 'Query', httpRequestLog?: { __typename?: 'HttpRequestLog', id: string, method: HttpMethod, url: string, proto: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }>, response?: { __typename?: 'HttpResponseLog', proto: HttpProtocol, statusCode: number, statusReason: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null } | null }; +export type HttpRequestLogQuery = { __typename?: 'Query', httpRequestLog?: { __typename?: 'HttpRequestLog', id: string, method: HttpMethod, url: string, proto: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }>, response?: { __typename?: 'HttpResponseLog', id: string, proto: HttpProtocol, statusCode: number, statusReason: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null } | null }; export type HttpRequestLogFilterQueryVariables = Exact<{ [key: string]: never; }>; @@ -568,6 +568,7 @@ export const HttpRequestLogDocument = gql` } body response { + id proto headers { key diff --git a/admin/src/lib/mui/theme.ts b/admin/src/lib/mui/theme.ts index ee3b81a..7920b86 100644 --- a/admin/src/lib/mui/theme.ts +++ b/admin/src/lib/mui/theme.ts @@ -1,6 +1,12 @@ import * as colors from "@mui/material/colors"; import { createTheme } from "@mui/material/styles"; +declare module "@mui/material/Paper" { + interface PaperPropsVariantOverrides { + centered: true; + } +} + const heading = { fontFamily: "'JetBrains Mono', monospace", fontWeight: 600, @@ -41,13 +47,28 @@ theme = createTheme(theme, { }, }, components: { - MuiTableCell: { + MuiTableRow: { styleOverrides: { - stickyHeader: { - backgroundColor: theme.palette.secondary.dark, + root: { + "&.Mui-selected, &.Mui-selected:hover": { + backgroundColor: theme.palette.grey[700], + }, }, }, }, + MuiPaper: { + variants: [ + { + props: { variant: "centered" }, + style: { + display: "flex", + justifyContent: "center", + alignItems: "center", + padding: theme.spacing(4), + }, + }, + ], + }, }, }); diff --git a/admin/src/lib/queryParamsFromURL.tsx b/admin/src/lib/queryParamsFromURL.tsx new file mode 100644 index 0000000..6ce89a6 --- /dev/null +++ b/admin/src/lib/queryParamsFromURL.tsx @@ -0,0 +1,17 @@ +import { KeyValuePair } from "./components/KeyValuePair"; + +export function queryParamsFromURL(url: string): KeyValuePair[] { + const questionMarkIndex = url.indexOf("?"); + if (questionMarkIndex === -1) { + return []; + } + + const queryParams: KeyValuePair[] = []; + + const searchParams = new URLSearchParams(url.slice(questionMarkIndex + 1)); + for (const [key, value] of searchParams) { + queryParams.push({ key, value }); + } + + return queryParams; +} diff --git a/admin/src/pages/_app.tsx b/admin/src/pages/_app.tsx index 9cb4275..e7eda29 100644 --- a/admin/src/pages/_app.tsx +++ b/admin/src/pages/_app.tsx @@ -1,11 +1,12 @@ import { ApolloProvider } from "@apollo/client"; import { CacheProvider, EmotionCache } from "@emotion/react"; +import { ThemeProvider } from "@mui/material"; import CssBaseline from "@mui/material/CssBaseline"; -import { ThemeProvider } from "@mui/material/styles"; import { AppProps } from "next/app"; import Head from "next/head"; import React from "react"; +import { ActiveProjectProvider } from "lib/ActiveProjectContext"; import { useApollo } from "lib/graphql/useApollo"; import createEmotionCache from "lib/mui/createEmotionCache"; import theme from "lib/mui/theme"; @@ -30,10 +31,12 @@ export default function MyApp(props: MyAppProps) { - - - - + + + + + + ); diff --git a/admin/src/pages/proxy/logs/index.tsx b/admin/src/pages/proxy/logs/index.tsx index 447859e..97000d6 100644 --- a/admin/src/pages/proxy/logs/index.tsx +++ b/admin/src/pages/proxy/logs/index.tsx @@ -1,16 +1,10 @@ -import { Box } from "@mui/material"; - import { Layout, Page } from "features/Layout"; -import LogsOverview from "features/reqlog/components/LogsOverview"; -import Search from "features/reqlog/components/Search"; +import RequestLogs from "features/reqlog"; function ProxyLogs(): JSX.Element { return ( - - - - + ); } diff --git a/admin/src/pages/sender/index.tsx b/admin/src/pages/sender/index.tsx index d141378..0a9373e 100644 --- a/admin/src/pages/sender/index.tsx +++ b/admin/src/pages/sender/index.tsx @@ -1,47 +1,10 @@ -import { Box } from "@mui/system"; -import { AllotmentProps } from "allotment"; -import { PaneProps } from "allotment/dist/types/src/allotment"; -import { ComponentType, useEffect, useRef, useState } from "react"; - import { Layout, Page } from "features/Layout"; -import EditRequest from "features/sender/components/EditRequest"; -import History from "features/sender/components/History"; +import Sender from "features/sender"; function Index(): JSX.Element { - const isMountedRef = useRef(false); - const [Allotment, setAllotment] = useState< - (ComponentType & { Pane: ComponentType }) | null - >(null); - useEffect(() => { - isMountedRef.current = true; - import("allotment") - .then((mod) => { - if (!isMountedRef.current) { - return; - } - setAllotment(mod.Allotment); - }) - .catch((err) => console.error(err, `could not import allotment ${err.message}`)); - return () => { - isMountedRef.current = false; - }; - }, []); - if (!Allotment) { - return
Loading...
; - } - return ( - - - - - - - - - - + ); } diff --git a/admin/yarn.lock b/admin/yarn.lock index daa2a3e..f527df5 100644 --- a/admin/yarn.lock +++ b/admin/yarn.lock @@ -499,7 +499,7 @@ core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.17.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.8.3": version "7.17.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== @@ -1229,6 +1229,15 @@ "@mui/utils" "^5.3.0" prop-types "^15.7.2" +"@mui/private-theming@^5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.4.2.tgz#f0a05f908456a2f7b87ccb6fc3b6e1faae9d89e6" + integrity sha512-mlPDYYko4wIcwXjCPEmOWbNTT4DZ6h9YHdnRtQPnWM28+TRUHEo7SbydnnmVDQLRXUfaH4Y6XtEHIfBNPE/SLg== + dependencies: + "@babel/runtime" "^7.17.0" + "@mui/utils" "^5.4.2" + prop-types "^15.7.2" + "@mui/styled-engine@^5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.3.0.tgz#b260a06398fc7335a62fd65ebbb9fc3c4071027b" @@ -1238,6 +1247,29 @@ "@emotion/cache" "^11.7.1" prop-types "^15.7.2" +"@mui/styles@^5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@mui/styles/-/styles-5.4.2.tgz#e0dadfc5de8255605f23c2f909f3669f0911bb88" + integrity sha512-BX75fNHmRF51yove9dBkH28gpSFjClOPDEnUwLTghPYN913OsqViS/iuCd61dxzygtEEmmeYuWfQjxu/F6vF5g== + dependencies: + "@babel/runtime" "^7.17.0" + "@emotion/hash" "^0.8.0" + "@mui/private-theming" "^5.4.2" + "@mui/types" "^7.1.2" + "@mui/utils" "^5.4.2" + clsx "^1.1.1" + csstype "^3.0.10" + hoist-non-react-statics "^3.3.2" + jss "^10.8.2" + jss-plugin-camel-case "^10.8.2" + jss-plugin-default-unit "^10.8.2" + jss-plugin-global "^10.8.2" + jss-plugin-nested "^10.8.2" + jss-plugin-props-sort "^10.8.2" + jss-plugin-rule-value-function "^10.8.2" + jss-plugin-vendor-prefixer "^10.8.2" + prop-types "^15.7.2" + "@mui/system@^5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.3.0.tgz#cd2c5fd7631f2c90f0072c866015bb24e319b66e" @@ -1257,6 +1289,11 @@ resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.0.tgz#5ed928c5a41cfbf9a4be82ea3bbdc47bcc9610d5" integrity sha512-Hh7ALdq/GjfIwLvqH3XftuY3bcKhupktTm+S6qRIDGOtPtRuq2L21VWzOK4p7kblirK0XgGVH5BLwa6u8z/6QQ== +"@mui/types@^7.1.2": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.2.tgz#4f3678ae77a7a3efab73b6e040469cc6df2144ac" + integrity sha512-SD7O1nVzqG+ckQpFjDhXPZjRceB8HQFHEvdLLrPhlJy4lLbwEBbxK74Tj4t6Jgk0fTvLJisuwOutrtYe9P/xBQ== + "@mui/utils@^5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.3.0.tgz#5f31915063d25c56f1d3ba9e289bf447472a868c" @@ -1268,6 +1305,17 @@ prop-types "^15.7.2" react-is "^17.0.2" +"@mui/utils@^5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.4.2.tgz#3edda8f80de235418fff0424ee66e2a49793ec01" + integrity sha512-646dBCC57MXTo/Gf3AnZSHRHznaTETQq5x7AWp5FRQ4jPeyT4WSs18cpJVwkV01cAHKh06pNQTIufIALIWCL5g== + dependencies: + "@babel/runtime" "^7.17.0" + "@types/prop-types" "^15.7.4" + "@types/react-is" "^16.7.1 || ^17.0.0" + prop-types "^15.7.2" + react-is "^17.0.2" + "@n1ru4l/graphql-live-query@^0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@n1ru4l/graphql-live-query/-/graphql-live-query-0.9.0.tgz#defaebdd31f625bee49e6745934f36312532b2bc" @@ -2489,6 +2537,14 @@ cross-undici-fetch@^0.1.19: undici "^4.9.3" web-streams-polyfill "^3.2.0" +css-vendor@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d" + integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ== + dependencies: + "@babel/runtime" "^7.8.3" + is-in-browser "^1.0.2" + csstype@^3.0.10, csstype@^3.0.2: version "3.0.10" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" @@ -3490,6 +3546,11 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +hyphenate-style-name@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" + integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== + iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -3677,6 +3738,11 @@ is-glob@4.0.3, is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-in-browser@^1.0.2, is-in-browser@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" + integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= + is-interactive@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" @@ -3931,6 +3997,76 @@ jsonwebtoken@^8.5.1: ms "^2.1.1" semver "^5.6.0" +jss-plugin-camel-case@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.9.0.tgz#4921b568b38d893f39736ee8c4c5f1c64670aaf7" + integrity sha512-UH6uPpnDk413/r/2Olmw4+y54yEF2lRIV8XIZyuYpgPYTITLlPOsq6XB9qeqv+75SQSg3KLocq5jUBXW8qWWww== + dependencies: + "@babel/runtime" "^7.3.1" + hyphenate-style-name "^1.0.3" + jss "10.9.0" + +jss-plugin-default-unit@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.9.0.tgz#bb23a48f075bc0ce852b4b4d3f7582bc002df991" + integrity sha512-7Ju4Q9wJ/MZPsxfu4T84mzdn7pLHWeqoGd/D8O3eDNNJ93Xc8PxnLmV8s8ZPNRYkLdxZqKtm1nPQ0BM4JRlq2w== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.0" + +jss-plugin-global@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.9.0.tgz#fc07a0086ac97aca174e37edb480b69277f3931f" + integrity sha512-4G8PHNJ0x6nwAFsEzcuVDiBlyMsj2y3VjmFAx/uHk/R/gzJV+yRHICjT4MKGGu1cJq2hfowFWCyrr/Gg37FbgQ== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.0" + +jss-plugin-nested@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.9.0.tgz#cc1c7d63ad542c3ccc6e2c66c8328c6b6b00f4b3" + integrity sha512-2UJnDrfCZpMYcpPYR16oZB7VAC6b/1QLsRiAutOt7wJaaqwCBvNsosLEu/fUyKNQNGdvg2PPJFDO5AX7dwxtoA== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.0" + tiny-warning "^1.0.2" + +jss-plugin-props-sort@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.9.0.tgz#30e9567ef9479043feb6e5e59db09b4de687c47d" + integrity sha512-7A76HI8bzwqrsMOJTWKx/uD5v+U8piLnp5bvru7g/3ZEQOu1+PjHvv7bFdNO3DwNPC9oM0a//KwIJsIcDCjDzw== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.0" + +jss-plugin-rule-value-function@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.9.0.tgz#379fd2732c0746fe45168011fe25544c1a295d67" + integrity sha512-IHJv6YrEf8pRzkY207cPmdbBstBaE+z8pazhPShfz0tZSDtRdQua5jjg6NMz3IbTasVx9FdnmptxPqSWL5tyJg== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.0" + tiny-warning "^1.0.2" + +jss-plugin-vendor-prefixer@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.9.0.tgz#aa9df98abfb3f75f7ed59a3ec50a5452461a206a" + integrity sha512-MbvsaXP7iiVdYVSEoi+blrW+AYnTDvHTW6I6zqi7JcwXdc6I9Kbm234nEblayhF38EftoenbM+5218pidmC5gA== + dependencies: + "@babel/runtime" "^7.3.1" + css-vendor "^2.0.8" + jss "10.9.0" + +jss@10.9.0, jss@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss/-/jss-10.9.0.tgz#7583ee2cdc904a83c872ba695d1baab4b59c141b" + integrity sha512-YpzpreB6kUunQBbrlArlsMpXYyndt9JATbt95tajx0t4MTJJcCJdd4hdNpHmOIDiUJrF/oX5wtVFrS3uofWfGw== + dependencies: + "@babel/runtime" "^7.3.1" + csstype "^3.0.2" + is-in-browser "^1.1.3" + tiny-warning "^1.0.2" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b" @@ -4759,7 +4895,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.4, prop-types@^15.6.2, prop-types@^15.7.2: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -4822,11 +4958,32 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + react-refresh@0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg== +react-split-pane@^0.1.92: + version "0.1.92" + resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.92.tgz#68242f72138aed95dd5910eeb9d99822c4fc3a41" + integrity sha512-GfXP1xSzLMcLJI5BM36Vh7GgZBpy+U/X0no+VM3fxayv+p1Jly5HpMofZJraeaMl73b3hvlr+N9zJKvLB/uz9w== + dependencies: + prop-types "^15.7.2" + react-lifecycles-compat "^3.0.4" + react-style-proptype "^3.2.2" + +react-style-proptype@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/react-style-proptype/-/react-style-proptype-3.2.2.tgz#d8e998e62ce79ec35b087252b90f19f1c33968a0" + integrity sha512-ywYLSjNkxKHiZOqNlso9PZByNEY+FTyh3C+7uuziK0xFXu9xzdyfHwg4S9iyiRRoPCR4k2LqaBBsWVmSBwCWYQ== + dependencies: + prop-types "^15.5.4" + react-transition-group@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" @@ -5503,6 +5660,11 @@ through@^2.3.6, through@^2.3.8: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tiny-warning@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + title-case@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/title-case/-/title-case-3.0.3.tgz#bc689b46f02e411f1d1e1d081f7c3deca0489982"