Split CodePaths and FileCodeSnippet into multiple files

This commit is contained in:
Koen Vlaswinkel
2022-09-22 11:12:03 +02:00
parent 9c076152cb
commit 9ad28f36b4
9 changed files with 465 additions and 424 deletions

View File

@@ -0,0 +1,39 @@
import * as React from 'react';
import { ChangeEvent, SetStateAction, useCallback } from 'react';
import { VSCodeDropdown, VSCodeOption } from '@vscode/webview-ui-toolkit/react';
import { CodeFlow } from '../../../remote-queries/shared/analysis-result';
const getCodeFlowName = (codeFlow: CodeFlow) => {
const filePath = codeFlow.threadFlows[codeFlow.threadFlows.length - 1].fileLink.filePath;
return filePath.substring(filePath.lastIndexOf('/') + 1);
};
type CodeFlowsDropdownProps = {
codeFlows: CodeFlow[];
setSelectedCodeFlow: (value: SetStateAction<CodeFlow>) => void;
}
export const CodeFlowsDropdown = ({
codeFlows,
setSelectedCodeFlow
}: CodeFlowsDropdownProps) => {
const handleChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => {
const selectedOption = e.target;
const selectedIndex = selectedOption.value as unknown as number;
setSelectedCodeFlow(codeFlows[selectedIndex]);
}, [setSelectedCodeFlow, codeFlows]);
return (
<VSCodeDropdown onChange={handleChange}>
{codeFlows.map((codeFlow, index) =>
<VSCodeOption
key={index}
value={index}
>
{getCodeFlowName(codeFlow)}
</VSCodeOption>
)}
</VSCodeDropdown>
);
};

View File

@@ -0,0 +1,30 @@
import * as React from 'react';
import { AnalysisMessage, CodeFlow, ResultSeverity } from '../../../remote-queries/shared/analysis-result';
import { ThreadPath } from './ThreadPath';
type CodePathProps = {
codeFlow: CodeFlow;
message: AnalysisMessage;
severity: ResultSeverity;
}
export const CodePath = ({
codeFlow,
message,
severity
}: CodePathProps) => (
<>
{codeFlow.threadFlows.map((threadFlow, index) =>
<ThreadPath
key={index}
threadFlow={threadFlow}
step={index + 1}
message={message}
severity={severity}
isSource={index === 0}
isSink={index === codeFlow.threadFlows.length - 1}
/>
)}
</>
);

View File

@@ -1,239 +1,17 @@
import * as React from 'react'; import * as React from 'react';
import { ChangeEvent, SetStateAction, useCallback, useRef, useState } from 'react'; import { useRef, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTag } from '@vscode/webview-ui-toolkit/react'; import { VSCodeLink } from '@vscode/webview-ui-toolkit/react';
import { Overlay } from '@primer/react'; import { Overlay } from '@primer/react';
import { CodeFlow, AnalysisMessage, ResultSeverity, ThreadFlow } from '../../../remote-queries/shared/analysis-result'; import { AnalysisMessage, CodeFlow, ResultSeverity } from '../../../remote-queries/shared/analysis-result';
import { SectionTitle } from '../SectionTitle'; import { CodePathsOverlay } from './CodePathsOverlay';
import { VerticalSpace } from '../VerticalSpace';
import { FileCodeSnippet } from '../FileCodeSnippet';
const StyledCloseButton = styled.button`
position: absolute;
top: 1em;
right: 4em;
background-color: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
border: none;
cursor: pointer;
&:focus-visible {
outline: none
}
`;
const OverlayContainer = styled.div`
height: 100%;
width: 100%;
padding: 2em;
position: fixed;
top: 0;
left: 0;
background-color: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
overflow-y: scroll;
`;
const CloseButton = ({ onClick }: { onClick: () => void }) => (
<StyledCloseButton onClick={onClick} tabIndex={-1}>
<span className="codicon codicon-chrome-close" />
</StyledCloseButton>
);
const PathsContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
`;
const PathDetailsContainer = styled.div`
padding: 0;
border: 0;
`;
const PathDropdownContainer = styled.div`
flex-grow: 1;
padding: 0 0 0 0.2em;
border: none;
`;
const Container = styled.div`
max-width: 55em;
margin-bottom: 1.5em;
`;
const HeaderContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 1em;
`;
const TitleContainer = styled.div`
flex-grow: 1;
padding: 0;
border: none;
`;
const TagContainer = styled.div`
padding: 0;
border: none;
`;
const ShowPathsLink = styled(VSCodeLink)` const ShowPathsLink = styled(VSCodeLink)`
cursor: pointer; cursor: pointer;
`; `;
type ThreadPathProps = {
threadFlow: ThreadFlow;
step: number;
message: AnalysisMessage;
severity: ResultSeverity;
isSource?: boolean;
isSink?: boolean;
}
const ThreadPath = ({
threadFlow,
step,
message,
severity,
isSource,
isSink,
}: ThreadPathProps) => (
<Container>
<HeaderContainer>
<TitleContainer>
<SectionTitle>Step {step}</SectionTitle>
</TitleContainer>
{isSource &&
<TagContainer>
<VSCodeTag>Source</VSCodeTag>
</TagContainer>
}
{isSink &&
<TagContainer>
<VSCodeTag>Sink</VSCodeTag>
</TagContainer>
}
</HeaderContainer>
<FileCodeSnippet
fileLink={threadFlow.fileLink}
codeSnippet={threadFlow.codeSnippet}
highlightedRegion={threadFlow.highlightedRegion}
severity={severity}
message={isSink ? message : threadFlow.message}
/>
</Container>
);
type CodePathProps = {
codeFlow: CodeFlow;
message: AnalysisMessage;
severity: ResultSeverity;
}
const CodePath = ({
codeFlow,
message,
severity
}: CodePathProps) => (
<>
{codeFlow.threadFlows.map((threadFlow, index) =>
<ThreadPath
key={index}
threadFlow={threadFlow}
step={index + 1}
message={message}
severity={severity}
isSource={index === 0}
isSink={index === codeFlow.threadFlows.length - 1}
/>
)}
</>
);
const getCodeFlowName = (codeFlow: CodeFlow) => {
const filePath = codeFlow.threadFlows[codeFlow.threadFlows.length - 1].fileLink.filePath;
return filePath.substring(filePath.lastIndexOf('/') + 1);
};
type CodeFlowsDropdownProps = {
codeFlows: CodeFlow[];
setSelectedCodeFlow: (value: SetStateAction<CodeFlow>) => void;
}
const CodeFlowsDropdown = ({
codeFlows,
setSelectedCodeFlow
}: CodeFlowsDropdownProps) => {
const handleChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => {
const selectedOption = e.target;
const selectedIndex = selectedOption.value as unknown as number;
setSelectedCodeFlow(codeFlows[selectedIndex]);
}, [setSelectedCodeFlow, codeFlows]);
return (
<VSCodeDropdown onChange={handleChange}>
{codeFlows.map((codeFlow, index) =>
<VSCodeOption
key={index}
value={index}
>
{getCodeFlowName(codeFlow)}
</VSCodeOption>
)}
</VSCodeDropdown>
);
};
type CodePathsOverlayProps = {
codeFlows: CodeFlow[];
ruleDescription: string;
message: AnalysisMessage;
severity: ResultSeverity;
onClose: () => void;
}
const CodePathsOverlay = ({
codeFlows,
ruleDescription,
message,
severity,
onClose,
}: CodePathsOverlayProps) => {
const [selectedCodeFlow, setSelectedCodeFlow] = useState(codeFlows[0]);
return (
<OverlayContainer>
<CloseButton onClick={onClose} />
<SectionTitle>{ruleDescription}</SectionTitle>
<VerticalSpace size={2} />
<PathsContainer>
<PathDetailsContainer>
{codeFlows.length} paths available: {selectedCodeFlow.threadFlows.length} steps in
</PathDetailsContainer>
<PathDropdownContainer>
<CodeFlowsDropdown codeFlows={codeFlows} setSelectedCodeFlow={setSelectedCodeFlow} />
</PathDropdownContainer>
</PathsContainer>
<VerticalSpace size={2} />
<CodePath
codeFlow={selectedCodeFlow}
severity={severity}
message={message}
/>
<VerticalSpace size={3} />
</OverlayContainer>
);
};
type Props = { type Props = {
codeFlows: CodeFlow[], codeFlows: CodeFlow[],
ruleDescription: string, ruleDescription: string,

View File

@@ -0,0 +1,102 @@
import * as React from 'react';
import { useState } from 'react';
import styled from 'styled-components';
import { AnalysisMessage, CodeFlow, ResultSeverity } from '../../../remote-queries/shared/analysis-result';
import { SectionTitle } from '../SectionTitle';
import { VerticalSpace } from '../VerticalSpace';
import { CodeFlowsDropdown } from './CodeFlowsDropdown';
import { CodePath } from './CodePath';
const StyledCloseButton = styled.button`
position: absolute;
top: 1em;
right: 4em;
background-color: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
border: none;
cursor: pointer;
&:focus-visible {
outline: none
}
`;
const OverlayContainer = styled.div`
height: 100%;
width: 100%;
padding: 2em;
position: fixed;
top: 0;
left: 0;
background-color: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
overflow-y: scroll;
`;
const CloseButton = ({ onClick }: { onClick: () => void }) => (
<StyledCloseButton onClick={onClick} tabIndex={-1}>
<span className="codicon codicon-chrome-close" />
</StyledCloseButton>
);
const PathsContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
`;
const PathDetailsContainer = styled.div`
padding: 0;
border: 0;
`;
const PathDropdownContainer = styled.div`
flex-grow: 1;
padding: 0 0 0 0.2em;
border: none;
`;
type CodePathsOverlayProps = {
codeFlows: CodeFlow[];
ruleDescription: string;
message: AnalysisMessage;
severity: ResultSeverity;
onClose: () => void;
}
export const CodePathsOverlay = ({
codeFlows,
ruleDescription,
message,
severity,
onClose,
}: CodePathsOverlayProps) => {
const [selectedCodeFlow, setSelectedCodeFlow] = useState(codeFlows[0]);
return (
<OverlayContainer>
<CloseButton onClick={onClose} />
<SectionTitle>{ruleDescription}</SectionTitle>
<VerticalSpace size={2} />
<PathsContainer>
<PathDetailsContainer>
{codeFlows.length} paths available: {selectedCodeFlow.threadFlows.length} steps in
</PathDetailsContainer>
<PathDropdownContainer>
<CodeFlowsDropdown codeFlows={codeFlows} setSelectedCodeFlow={setSelectedCodeFlow} />
</PathDropdownContainer>
</PathsContainer>
<VerticalSpace size={2} />
<CodePath
codeFlow={selectedCodeFlow}
severity={severity}
message={message}
/>
<VerticalSpace size={3} />
</OverlayContainer>
);
};

View File

@@ -0,0 +1,74 @@
import * as React from 'react';
import styled from 'styled-components';
import { VSCodeTag } from '@vscode/webview-ui-toolkit/react';
import { AnalysisMessage, ResultSeverity, ThreadFlow } from '../../../remote-queries/shared/analysis-result';
import { SectionTitle } from '../SectionTitle';
import { FileCodeSnippet } from '../FileCodeSnippet';
const Container = styled.div`
max-width: 55em;
margin-bottom: 1.5em;
`;
const HeaderContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 1em;
`;
const TitleContainer = styled.div`
flex-grow: 1;
padding: 0;
border: none;
`;
const TagContainer = styled.div`
padding: 0;
border: none;
`;
type ThreadPathProps = {
threadFlow: ThreadFlow;
step: number;
message: AnalysisMessage;
severity: ResultSeverity;
isSource?: boolean;
isSink?: boolean;
}
export const ThreadPath = ({
threadFlow,
step,
message,
severity,
isSource,
isSink,
}: ThreadPathProps) => (
<Container>
<HeaderContainer>
<TitleContainer>
<SectionTitle>Step {step}</SectionTitle>
</TitleContainer>
{isSource &&
<TagContainer>
<VSCodeTag>Source</VSCodeTag>
</TagContainer>
}
{isSink &&
<TagContainer>
<VSCodeTag>Sink</VSCodeTag>
</TagContainer>
}
</HeaderContainer>
<FileCodeSnippet
fileLink={threadFlow.fileLink}
codeSnippet={threadFlow.codeSnippet}
highlightedRegion={threadFlow.highlightedRegion}
severity={severity}
message={isSink ? message : threadFlow.message}
/>
</Container>
);

View File

@@ -0,0 +1,43 @@
import * as React from 'react';
import styled from 'styled-components';
import { HighlightedRegion } from '../../../remote-queries/shared/analysis-result';
import { parseHighlightedLine, shouldHighlightLine } from '../../../pure/sarif-utils';
const replaceSpaceAndTabChar = (text: string) => text.replaceAll(' ', '\u00a0').replaceAll('\t', '\u00a0\u00a0\u00a0\u00a0');
const HighlightedSpan = styled.span`
background-color: var(--vscode-editor-findMatchHighlightBackground);
`;
const PlainCode = ({ text }: { text: string }) => {
return <span>{replaceSpaceAndTabChar(text)}</span>;
};
const HighlightedCode = ({ text }: { text: string }) => {
return <HighlightedSpan>{replaceSpaceAndTabChar(text)}</HighlightedSpan>;
};
export const CodeSnippetCode = ({
line,
lineNumber,
highlightedRegion
}: {
line: string,
lineNumber: number,
highlightedRegion?: HighlightedRegion
}) => {
if (!highlightedRegion || !shouldHighlightLine(lineNumber, highlightedRegion)) {
return <PlainCode text={line} />;
}
const partiallyHighlightedLine = parseHighlightedLine(line, lineNumber, highlightedRegion);
return (
<>
<PlainCode text={partiallyHighlightedLine.plainSection1} />
<HighlightedCode text={partiallyHighlightedLine.highlightedSection} />
<PlainCode text={partiallyHighlightedLine.plainSection2} />
</>
);
};

View File

@@ -0,0 +1,77 @@
import * as React from 'react';
import styled from 'styled-components';
import { AnalysisMessage, HighlightedRegion, ResultSeverity } from '../../../remote-queries/shared/analysis-result';
import { CodeSnippetCode } from './CodeSnippetCode';
import { CodeSnippetMessage } from './CodeSnippetMessage';
const MessageContainer = styled.div`
padding-top: 0.5em;
padding-bottom: 0.5em;
`;
const LineContainer = styled.div`
display: flex;
`;
const LineNumberContainer = styled.div`
border-style: none;
padding: 0.01em 0.5em 0.2em;
`;
const CodeSnippetLineCodeContainer = styled.div`
flex-grow: 1;
border-style: none;
padding: 0.01em 0.5em 0.2em 1.5em;
word-break: break-word;
`;
type CodeSnippetLineProps = {
line: string,
lineIndex: number,
startingLineIndex: number,
highlightedRegion?: HighlightedRegion,
severity?: ResultSeverity,
message?: AnalysisMessage,
messageChildren?: React.ReactNode,
};
export const CodeSnippetLine = ({
line,
lineIndex,
startingLineIndex,
highlightedRegion,
severity,
message,
messageChildren
}: CodeSnippetLineProps) => {
const shouldShowMessage = message &&
severity &&
highlightedRegion &&
highlightedRegion.endLine == startingLineIndex + lineIndex;
return (
<div>
<LineContainer>
<LineNumberContainer>{startingLineIndex + lineIndex}</LineNumberContainer>
<CodeSnippetLineCodeContainer>
<CodeSnippetCode
line={line}
lineNumber={startingLineIndex + lineIndex}
highlightedRegion={highlightedRegion}
/>
</CodeSnippetLineCodeContainer>
</LineContainer>
{shouldShowMessage &&
<MessageContainer>
<CodeSnippetMessage
message={message}
severity={severity}
>
{messageChildren}
</CodeSnippetMessage>
</MessageContainer>
}
</div>
);
};

View File

@@ -0,0 +1,93 @@
import * as React from 'react';
import styled from 'styled-components';
import { VSCodeLink } from '@vscode/webview-ui-toolkit/react';
import { AnalysisMessage, ResultSeverity } from '../../../remote-queries/shared/analysis-result';
import { createRemoteFileRef } from '../../../pure/location-link-utils';
import { VerticalSpace } from '../VerticalSpace';
const getSeverityColor = (severity: ResultSeverity) => {
switch (severity) {
case 'Recommendation':
return 'var(--vscode-editorInfo-foreground)';
case 'Warning':
return 'var(--vscode-editorWarning-foreground)';
case 'Error':
return 'var(--vscode-editorError-foreground)';
}
};
const MessageText = styled.div`
font-size: small;
padding-left: 0.5em;
`;
type CodeSnippetMessageContainerProps = {
severity: ResultSeverity;
};
const CodeSnippetMessageContainer = styled.div<CodeSnippetMessageContainerProps>`
border-color: var(--vscode-editor-snippetFinalTabstopHighlightBorder);
border-width: 0.1em;
border-style: solid;
border-left-color: ${props => getSeverityColor(props.severity)};
border-left-width: 0.3em;
padding-top: 1em;
padding-bottom: 1em;
`;
const LocationLink = styled(VSCodeLink)`
font-family: var(--vscode-editor-font-family)
`;
type CodeSnippetMessageProps = {
message: AnalysisMessage,
severity: ResultSeverity,
children: React.ReactNode
};
export const CodeSnippetMessage = ({
message,
severity,
children
}: CodeSnippetMessageProps) => {
return (
<CodeSnippetMessageContainer
severity={severity}
>
<MessageText>
{message.tokens.map((token, index) => {
switch (token.t) {
case 'text':
return <span key={index}>{token.text}</span>;
case 'location':
return (
<LocationLink
key={index}
href={
createRemoteFileRef(
token.location.fileLink,
token.location.highlightedRegion?.startLine,
token.location.highlightedRegion?.endLine
)
}
>
{token.text}
</LocationLink>
);
default:
return <></>;
}
})}
{
children && (
<>
<VerticalSpace size={2} />
{children}
</>
)
}
</MessageText>
</CodeSnippetMessageContainer>
);
};

View File

@@ -1,6 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { VSCodeLink } from '@vscode/webview-ui-toolkit/react'; import { VSCodeLink } from '@vscode/webview-ui-toolkit/react';
import { import {
AnalysisMessage, AnalysisMessage,
CodeSnippet, CodeSnippet,
@@ -9,24 +10,11 @@ import {
ResultSeverity ResultSeverity
} from '../../../remote-queries/shared/analysis-result'; } from '../../../remote-queries/shared/analysis-result';
import { createRemoteFileRef } from '../../../pure/location-link-utils'; import { createRemoteFileRef } from '../../../pure/location-link-utils';
import { parseHighlightedLine, shouldHighlightLine } from '../../../pure/sarif-utils'; import { CodeSnippetMessage } from './CodeSnippetMessage';
import { VerticalSpace } from '../VerticalSpace'; import { CodeSnippetLine } from './CodeSnippetLine';
const borderColor = 'var(--vscode-editor-snippetFinalTabstopHighlightBorder)'; const borderColor = 'var(--vscode-editor-snippetFinalTabstopHighlightBorder)';
const getSeverityColor = (severity: ResultSeverity) => {
switch (severity) {
case 'Recommendation':
return 'var(--vscode-editorInfo-foreground)';
case 'Warning':
return 'var(--vscode-editorWarning-foreground)';
case 'Error':
return 'var(--vscode-editorError-foreground)';
}
};
const replaceSpaceAndTabChar = (text: string) => text.replaceAll(' ', '\u00a0').replaceAll('\t', '\u00a0\u00a0\u00a0\u00a0');
const Container = styled.div` const Container = styled.div`
font-family: var(--vscode-editor-font-family); font-family: var(--vscode-editor-font-family);
font-size: small; font-size: small;
@@ -49,189 +37,6 @@ const CodeContainer = styled.div`
padding-bottom: 1em; padding-bottom: 1em;
`; `;
const MessageText = styled.div`
font-size: small;
padding-left: 0.5em;
`;
const MessageContainer = styled.div`
padding-top: 0.5em;
padding-bottom: 0.5em;
`;
const HighlightedSpan = styled.span`
background-color: var(--vscode-editor-findMatchHighlightBackground);
`;
const LineContainer = styled.div`
display: flex;
`;
const LineNumberContainer = styled.div`
border-style: none;
padding: 0.01em 0.5em 0.2em;
`;
const CodeSnippetLineCodeContainer = styled.div`
flex-grow: 1;
border-style: none;
padding: 0.01em 0.5em 0.2em 1.5em;
word-break: break-word;
`;
type CodeSnippetMessageContainerProps = {
severity: ResultSeverity;
};
const CodeSnippetMessageContainer = styled.div<CodeSnippetMessageContainerProps>`
border-color: var(--vscode-editor-snippetFinalTabstopHighlightBorder);
border-width: 0.1em;
border-style: solid;
border-left-color: ${props => getSeverityColor(props.severity)};
border-left-width: 0.3em;
padding-top: 1em;
padding-bottom: 1em;
`;
const LocationLink = styled(VSCodeLink)`
font-family: var(--vscode-editor-font-family)
`;
const PlainCode = ({ text }: { text: string }) => {
return <span>{replaceSpaceAndTabChar(text)}</span>;
};
const HighlightedCode = ({ text }: { text: string }) => {
return <HighlightedSpan>{replaceSpaceAndTabChar(text)}</HighlightedSpan>;
};
type CodeSnippetMessageProps = {
message: AnalysisMessage,
severity: ResultSeverity,
children: React.ReactNode
};
const CodeSnippetMessage = ({
message,
severity,
children
}: CodeSnippetMessageProps) => {
return (
<CodeSnippetMessageContainer
severity={severity}
>
<MessageText>
{message.tokens.map((token, index) => {
switch (token.t) {
case 'text':
return <span key={index}>{token.text}</span>;
case 'location':
return (
<LocationLink
key={index}
href={
createRemoteFileRef(
token.location.fileLink,
token.location.highlightedRegion?.startLine,
token.location.highlightedRegion?.endLine
)
}
>
{token.text}
</LocationLink>
);
default:
return <></>;
}
})}
{
children && (
<>
<VerticalSpace size={2} />
{children}
</>
)
}
</MessageText>
</CodeSnippetMessageContainer>
);
};
const CodeSnippetCode = ({
line,
lineNumber,
highlightedRegion
}: {
line: string,
lineNumber: number,
highlightedRegion?: HighlightedRegion
}) => {
if (!highlightedRegion || !shouldHighlightLine(lineNumber, highlightedRegion)) {
return <PlainCode text={line} />;
}
const partiallyHighlightedLine = parseHighlightedLine(line, lineNumber, highlightedRegion);
return (
<>
<PlainCode text={partiallyHighlightedLine.plainSection1} />
<HighlightedCode text={partiallyHighlightedLine.highlightedSection} />
<PlainCode text={partiallyHighlightedLine.plainSection2} />
</>
);
};
type CodeSnippetLineProps = {
line: string,
lineIndex: number,
startingLineIndex: number,
highlightedRegion?: HighlightedRegion,
severity?: ResultSeverity,
message?: AnalysisMessage,
messageChildren?: React.ReactNode,
};
const CodeSnippetLine = ({
line,
lineIndex,
startingLineIndex,
highlightedRegion,
severity,
message,
messageChildren
}: CodeSnippetLineProps) => {
const shouldShowMessage = message &&
severity &&
highlightedRegion &&
highlightedRegion.endLine == startingLineIndex + lineIndex;
return (
<div>
<LineContainer>
<LineNumberContainer>{startingLineIndex + lineIndex}</LineNumberContainer>
<CodeSnippetLineCodeContainer>
<CodeSnippetCode
line={line}
lineNumber={startingLineIndex + lineIndex}
highlightedRegion={highlightedRegion}
/>
</CodeSnippetLineCodeContainer>
</LineContainer>
{shouldShowMessage &&
<MessageContainer>
<CodeSnippetMessage
message={message}
severity={severity}
>
{messageChildren}
</CodeSnippetMessage>
</MessageContainer>
}
</div>
);
};
type Props = { type Props = {
fileLink: FileLink, fileLink: FileLink,
codeSnippet?: CodeSnippet, codeSnippet?: CodeSnippet,