Skip to content

Commit

Permalink
FI-2433: Collapse request details (#448)
Browse files Browse the repository at this point in the history
* convert codeblock to collapsible panel

* make prop optional

* remove dead code

---------

Co-authored-by: Alyssa Wang <awang@mitre.org>
  • Loading branch information
AlyssaWang and AlyssaWang authored Feb 23, 2024
1 parent 022aeaf commit 21b7aef
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 77 deletions.
48 changes: 33 additions & 15 deletions client/src/components/RequestDetailModal/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,50 @@
import { Container, Paper } from '@mui/material';
import React, { FC } from 'react';
import useStyles from './styles';
import { Box, Card, CardContent, CardHeader, Collapse, Divider } from '@mui/material';
import { RequestHeader } from '~/models/testSuiteModels';
import CollapseButton from '~/components/_common/CollapseButton';
import CopyButton from '~/components/_common/CopyButton';

import { formatBodyIfJSON } from './helpers';
import useStyles from './styles';

export interface CodeBlockProps {
body?: string | null;
collapsedState?: boolean;
headers?: RequestHeader[] | null | undefined;
title?: string;
}

const CodeBlock: FC<CodeBlockProps> = ({ body, headers }) => {
const CodeBlock: FC<CodeBlockProps> = ({ body, collapsedState = false, headers, title }) => {
const { classes } = useStyles();
const [collapsed, setCollapsed] = React.useState(collapsedState);

if (body && body.length > 0) {
return (
<Container
component={Paper}
variant="outlined"
className={classes.codeblock}
data-testid="code-block"
>
<pre data-testid="pre">
<code data-testid="code" className={classes.code}>
{formatBodyIfJSON(body, headers)}
</code>
</pre>
</Container>
<Card variant="outlined" className={classes.codeblock} data-testid="code-block">
<CardHeader
subheader={title || 'Code'}
action={
<Box display="flex">
<CopyButton copyText={formatBodyIfJSON(body, headers)} size="small" />
<CollapseButton
setCollapsed={setCollapsed}
startState={collapsedState}
size="small"
/>
</Box>
}
/>
<Collapse in={!collapsed}>
<Divider />
<CardContent sx={{ pt: 0 }}>
<pre data-testid="pre">
<code data-testid="code" className={classes.code}>
{formatBodyIfJSON(body, headers)}
</code>
</pre>
</CardContent>
</Collapse>
</Card>
);
} else {
return null;
Expand Down
38 changes: 15 additions & 23 deletions client/src/components/RequestDetailModal/RequestDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import {
DialogContent,
DialogTitle,
Divider,
IconButton,
Typography,
} from '@mui/material';
import { Request } from '~/models/testSuiteModels';
import { ContentCopy, Input } from '@mui/icons-material';
import { Input } from '@mui/icons-material';
import CustomTooltip from '~/components/_common/CustomTooltip';
import CodeBlock from './CodeBlock';
import HeaderTable from './HeaderTable';
import useStyles from './styles';
import CopyButton from '../_common/CopyButton';

export interface RequestDetailModalProps {
request?: Request;
Expand All @@ -30,19 +30,9 @@ const RequestDetailModal: FC<RequestDetailModalProps> = ({
modalVisible,
usedRequest,
}) => {
const [copySuccess, setCopySuccess] = React.useState(false);
const { classes } = useStyles();
const timestamp = request?.timestamp ? new Date(request?.timestamp) : null;

const copyTextClick = async (text: string) => {
await navigator.clipboard.writeText(text).then(() => {
setCopySuccess(true);
setTimeout(() => {
setCopySuccess(false);
}, 2000); // 2 second delay
});
};

const usedRequestIcon = (
<CustomTooltip title="This request was performed in another test and the result is used by this test">
<Input className={classes.inputIcon} />
Expand All @@ -64,15 +54,7 @@ const RequestDetailModal: FC<RequestDetailModalProps> = ({
<Box pr={1} className={classes.modalTitleURL}>
{request?.url}
</Box>
{request?.url && (
<CustomTooltip title={copySuccess ? 'Text copied!' : 'Copy text'}>
<Box pr={1}>
<IconButton color="secondary" onClick={() => void copyTextClick(request.url)}>
<ContentCopy fontSize="inherit" />
</IconButton>
</Box>
</CustomTooltip>
)}
{request?.url && <CopyButton copyText={request.url} />}
<Box display="flex" flexShrink={0}>
&#8594; {request?.status}
</Box>
Expand Down Expand Up @@ -105,14 +87,24 @@ const RequestDetailModal: FC<RequestDetailModalProps> = ({
</Typography>
{timestamp && <Typography variant="overline">{timestamp.toLocaleString()}</Typography>}
<HeaderTable headers={request.request_headers || []} />
<CodeBlock body={request.request_body} headers={request.request_headers} />
<CodeBlock
body={request.request_body}
collapsedState={true}
headers={request.request_headers}
title="Request Body"
/>
</Box>
<Box pb={3}>
<Typography variant="h5" component="h3" pb={2}>
Response
</Typography>
<HeaderTable headers={request.response_headers || []} />
<CodeBlock body={request.response_body} headers={request.response_headers} />
<CodeBlock
body={request.response_body}
collapsedState={true}
headers={request.response_headers}
title="Response Body"
/>
</Box>
</DialogContent>
<DialogActions>
Expand Down
3 changes: 1 addition & 2 deletions client/src/components/RequestDetailModal/styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ export default makeStyles()((_theme: Theme) => ({
fontWeight: 600,
},
codeblock: {
width: '100%',
overflow: 'auto',
fontSize: 'small',
marginTop: '10px',
marginTop: '16px',
},
code: {
textWrap: 'wrap',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { FC } from 'react';
import {
Box,
Button,
IconButton,
Typography,
TableRow,
TableCell,
Expand All @@ -11,13 +10,14 @@ import {
TableBody,
TableHead,
} from '@mui/material';
import { ContentCopy, Input, SaveAlt } from '@mui/icons-material';
import { Input, SaveAlt } from '@mui/icons-material';
import { Request } from '~/models/testSuiteModels';
import { getRequestDetails } from '~/api/RequestsApi';
import RequestDetailModal from '~/components/RequestDetailModal/RequestDetailModal';
import CustomTooltip from '~/components/_common/CustomTooltip';
import { useSnackbar } from 'notistack';
import useStyles from './styles';
import CopyButton from '~/components/_common/CopyButton';

interface RequestListProps {
resultId: string;
Expand All @@ -30,7 +30,6 @@ const RequestList: FC<RequestListProps> = ({ requests, resultId, updateRequest,
const { classes } = useStyles();
const { enqueueSnackbar } = useSnackbar();
const [showDetails, setShowDetails] = React.useState(false);
const [copySuccess, setCopySuccess] = React.useState({});
const [detailedRequest, setDetailedRequest] = React.useState<Request>();
const headerTitles = ['Type', 'URL', 'Status'];

Expand All @@ -55,16 +54,6 @@ const RequestList: FC<RequestListProps> = ({ requests, resultId, updateRequest,
}
};

const copyTextClick = async (text: string) => {
await navigator.clipboard.writeText(text).then(() => {
setCopySuccess({ ...copySuccess, [text]: true });
setTimeout(() => {
// Reset map instead of setting false to avoid async bug
setCopySuccess({});
}, 2000); // 2 second delay
});
};

const renderReferenceIcon = (request: Request) => {
if (request.result_id !== resultId) {
return (
Expand Down Expand Up @@ -109,7 +98,7 @@ const RequestList: FC<RequestListProps> = ({ requests, resultId, updateRequest,
role="none"
aria-hidden="true"
aria-label="header-request-direction"
></TableCell>
/>
{view === 'run' && (
<TableCell
key={'Details'}
Expand Down Expand Up @@ -155,29 +144,7 @@ const RequestList: FC<RequestListProps> = ({ requests, resultId, updateRequest,
{request.url}
</Typography>
</CustomTooltip>
<CustomTooltip
title={
copySuccess[request.url as keyof typeof copySuccess] ? 'Text copied!' : 'Copy text'
}
sx={
view === 'report'
? {
display: 'none',
'@media print': {
display: 'none',
},
}
: {}
}
>
<IconButton
size="small"
color="secondary"
onClick={() => void copyTextClick(request.url)}
>
<ContentCopy fontSize="inherit" />
</IconButton>
</CustomTooltip>
<CopyButton copyText={request.url} size="small" view={view} />
</Box>
</TableCell>
<TableCell>
Expand Down
38 changes: 38 additions & 0 deletions client/src/components/_common/CollapseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { FC, useEffect } from 'react';
import { IconButton, IconButtonPropsSizeOverrides } from '@mui/material';
import { OverridableStringUnion } from '@mui/types';
import { ExpandLess, ExpandMore } from '@mui/icons-material';
import CustomTooltip from '~/components/_common/CustomTooltip';

export interface CollapseButtonProps {
setCollapsed: (collapsed: boolean) => void;
size?: OverridableStringUnion<'small' | 'large' | 'medium', IconButtonPropsSizeOverrides>;
startState?: boolean;
}

const CollapseButton: FC<CollapseButtonProps> = ({
setCollapsed: setParentCollapsed,
size,
startState = false,
}) => {
const [collapsed, setCollapsed] = React.useState(startState);

useEffect(() => {
setParentCollapsed(collapsed);
}, [collapsed]);

return (
<CustomTooltip title={collapsed ? 'Expand panel' : 'Collapse panel'}>
<IconButton
size={size}
color="secondary"
aria-label={collapsed ? 'expand button' : 'collapse button'}
onClick={() => setCollapsed(!collapsed)}
>
{collapsed ? <ExpandMore fontSize="inherit" /> : <ExpandLess fontSize="inherit" />}
</IconButton>
</CustomTooltip>
);
};

export default CollapseButton;
53 changes: 53 additions & 0 deletions client/src/components/_common/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { FC } from 'react';
import { Box, IconButton, IconButtonPropsSizeOverrides } from '@mui/material';
import { OverridableStringUnion } from '@mui/types';
import { ContentCopy } from '@mui/icons-material';
import CustomTooltip from '~/components/_common/CustomTooltip';

export interface CopyButtonProps {
copyText: string;
size?: OverridableStringUnion<'small' | 'large' | 'medium', IconButtonPropsSizeOverrides>;
view?: string;
}

const CopyButton: FC<CopyButtonProps> = ({ copyText, size, view }) => {
const [copySuccess, setCopySuccess] = React.useState({}); // Use map for lists of copiable items

const copyTextClick = async (text: string) => {
await navigator.clipboard.writeText(text).then(() => {
setCopySuccess({ ...copySuccess, [text]: true });
setTimeout(() => {
// Reset map instead of setting false to avoid async bug
setCopySuccess({});
}, 2000); // 2 second delay
});
};
return (
<CustomTooltip
title={copySuccess[copyText as keyof typeof copySuccess] ? 'Text copied!' : 'Copy text'}
sx={
view === 'report'
? {
display: 'none',
'@media print': {
display: 'none',
},
}
: {}
}
>
<Box pr={1}>
<IconButton
size={size}
color="secondary"
aria-label="copy button"
onClick={() => void copyTextClick(copyText)}
>
<ContentCopy fontSize="inherit" />
</IconButton>
</Box>
</CustomTooltip>
);
};

export default CopyButton;

0 comments on commit 21b7aef

Please sign in to comment.