Add support for showing code flows (#1187)

This commit is contained in:
Charis Kyriakou
2022-03-09 09:15:45 +00:00
committed by GitHub
parent 3fc3b259ba
commit 2c5004387d
7 changed files with 597 additions and 84 deletions

View File

@@ -11,7 +11,7 @@
"dependencies": {
"@octokit/rest": "^18.5.6",
"@primer/octicons-react": "^16.3.0",
"@primer/react": "^34.3.0",
"@primer/react": "^35.0.0-rc.c106d292",
"child-process-promise": "^2.2.1",
"classnames": "~2.2.6",
"d3": "^6.3.1",
@@ -647,9 +647,9 @@
}
},
"node_modules/@primer/behaviors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.0.3.tgz",
"integrity": "sha512-zh1FKvAXLjKs0rr9Ik9E5M3Q9/npa9hmpuHKmYZn7u9QnSl+X13jFPme3AmtokOlfduFYeHfQyzSIJEhSEVl3w=="
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.0.tgz",
"integrity": "sha512-Ej2OUc3ZIFaR7WwIUqESO1DTzmpb7wc8xbTVRT9s52jZQDjN7g5iljoK3ocYZm+BIAcKn3MvcwB42hEk4Ga4xQ=="
},
"node_modules/@primer/octicons-react": {
"version": "16.3.0",
@@ -668,11 +668,11 @@
"integrity": "sha512-+Gwo89YK1OFi6oubTlah/zPxxzMNaMLy+inECAYI646KIFdzzhAsKWb3z5tSOu5Ff7no4isRV64rWfMSKLZclw=="
},
"node_modules/@primer/react": {
"version": "34.3.0",
"resolved": "https://registry.npmjs.org/@primer/react/-/react-34.3.0.tgz",
"integrity": "sha512-C0qrULg4pcfcPaHwZVyU86HACPP/xVjhNqQaEgvdPV8tFA86ql7EVMuoA973Ke42rUvrhmeO4GBILwQD+3im5A==",
"version": "35.0.0-rc.c106d292",
"resolved": "https://registry.npmjs.org/@primer/react/-/react-35.0.0-rc.c106d292.tgz",
"integrity": "sha512-4CyB2OvwMOt1ZULbOUD6Nz7LnE433OT/h6hJDld/IE4klGAtF1HxDVUG7PfbGmlpw87I79WGjvH2+qx6EIhtZA==",
"dependencies": {
"@primer/behaviors": "1.0.3",
"@primer/behaviors": "1.1.0",
"@primer/octicons-react": "16.1.1",
"@primer/primitives": "7.1.1",
"@radix-ui/react-polymorphic": "0.0.14",
@@ -688,8 +688,13 @@
"color2k": "1.2.4",
"deepmerge": "4.2.2",
"focus-visible": "5.2.0",
"history": "5.0.0",
"styled-system": "5.1.5"
},
"engines": {
"node": ">=12",
"npm": ">=7"
},
"peerDependencies": {
"react": "^17.0.0",
"react-dom": "^17.0.0",
@@ -6568,6 +6573,14 @@
"he": "bin/he"
}
},
"node_modules/history": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.0.0.tgz",
"integrity": "sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==",
"dependencies": {
"@babel/runtime": "^7.7.6"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -13761,9 +13774,9 @@
}
},
"@primer/behaviors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.0.3.tgz",
"integrity": "sha512-zh1FKvAXLjKs0rr9Ik9E5M3Q9/npa9hmpuHKmYZn7u9QnSl+X13jFPme3AmtokOlfduFYeHfQyzSIJEhSEVl3w=="
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.0.tgz",
"integrity": "sha512-Ej2OUc3ZIFaR7WwIUqESO1DTzmpb7wc8xbTVRT9s52jZQDjN7g5iljoK3ocYZm+BIAcKn3MvcwB42hEk4Ga4xQ=="
},
"@primer/octicons-react": {
"version": "16.3.0",
@@ -13777,11 +13790,11 @@
"integrity": "sha512-+Gwo89YK1OFi6oubTlah/zPxxzMNaMLy+inECAYI646KIFdzzhAsKWb3z5tSOu5Ff7no4isRV64rWfMSKLZclw=="
},
"@primer/react": {
"version": "34.3.0",
"resolved": "https://registry.npmjs.org/@primer/react/-/react-34.3.0.tgz",
"integrity": "sha512-C0qrULg4pcfcPaHwZVyU86HACPP/xVjhNqQaEgvdPV8tFA86ql7EVMuoA973Ke42rUvrhmeO4GBILwQD+3im5A==",
"version": "35.0.0-rc.c106d292",
"resolved": "https://registry.npmjs.org/@primer/react/-/react-35.0.0-rc.c106d292.tgz",
"integrity": "sha512-4CyB2OvwMOt1ZULbOUD6Nz7LnE433OT/h6hJDld/IE4klGAtF1HxDVUG7PfbGmlpw87I79WGjvH2+qx6EIhtZA==",
"requires": {
"@primer/behaviors": "1.0.3",
"@primer/behaviors": "1.1.0",
"@primer/octicons-react": "16.1.1",
"@primer/primitives": "7.1.1",
"@radix-ui/react-polymorphic": "0.0.14",
@@ -13797,6 +13810,7 @@
"color2k": "1.2.4",
"deepmerge": "4.2.2",
"focus-visible": "5.2.0",
"history": "5.0.0",
"styled-system": "5.1.5"
},
"dependencies": {
@@ -18799,6 +18813,14 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"history": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.0.0.tgz",
"integrity": "sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==",
"requires": {
"@babel/runtime": "^7.7.6"
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",

View File

@@ -1065,7 +1065,7 @@
"dependencies": {
"@octokit/rest": "^18.5.6",
"@primer/octicons-react": "^16.3.0",
"@primer/react": "^34.3.0",
"@primer/react": "^35.0.0-rc.c106d292",
"child-process-promise": "^2.2.1",
"classnames": "~2.2.6",
"d3": "^6.3.1",

View File

@@ -102,6 +102,7 @@ export const sampleRemoteQueryResult: RemoteQueryResult = {
const createAnalysisResults = (n: number) => Array(n).fill(
{
message: 'This shell command depends on an uncontrolled [absolute path](1).',
shortDescription: 'Shell command built from environment values',
severity: 'Error',
filePath: 'npm-packages/meteor-installer/config.js',
codeSnippet: {
@@ -113,7 +114,202 @@ const createAnalysisResults = (n: number) => Array(n).fill(
startLine: 255,
startColumn: 28,
endColumn: 62
}
},
codeFlows: [
{
threadFlows: [
{
filePath: 'npm-packages/meteor-installer/config.js',
highlightedRegion: {
startLine: 35,
startColumn: 20,
endColumn: 61
},
codeSnippet: {
startLine: 33,
endLine: 37,
text: '\nconst meteorLocalFolder = \'.meteor\';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n'
}
},
{
filePath: 'npm-packages/meteor-installer/config.js',
highlightedRegion: {
startLine: 35,
startColumn: 7,
endColumn: 61
},
codeSnippet: {
startLine: 33,
endLine: 37,
text: '\nconst meteorLocalFolder = \'.meteor\';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n'
}
},
{
filePath: 'npm-packages/meteor-installer/config.js',
highlightedRegion: {
startLine: 40,
startColumn: 3,
endColumn: 13
},
codeSnippet: {
startLine: 38,
endLine: 42,
text: ' METEOR_LATEST_VERSION,\n extractPath: rootPath,\n meteorPath,\n release: process.env.INSTALL_METEOR_VERSION || METEOR_LATEST_VERSION,\n rootPath,\n'
}
},
{
filePath: 'npm-packages/meteor-installer/install.js',
highlightedRegion: {
startLine: 12,
startColumn: 3,
endColumn: 13
},
codeSnippet: {
startLine: 10,
endLine: 14,
text: 'const os = require(\'os\');\nconst {\n meteorPath,\n release,\n startedPath,\n'
}
},
{
filePath: 'npm-packages/meteor-installer/install.js',
highlightedRegion: {
startLine: 11,
startColumn: 7,
endLine: 22,
endColumn: 27
},
codeSnippet: {
startLine: 9,
endLine: 24,
text: 'const tmp = require(\'tmp\');\nconst os = require(\'os\');\nconst {\n meteorPath,\n release,\n startedPath,\n extractPath,\n isWindows,\n rootPath,\n sudoUser,\n isSudo,\n isMac,\n METEOR_LATEST_VERSION,\n} = require(\'./config.js\');\nconst { uninstall } = require(\'./uninstall\');\nconst {\n'
}
},
{
filePath: 'npm-packages/meteor-installer/install.js',
highlightedRegion: {
startLine: 255,
startColumn: 42,
endColumn: 52
},
codeSnippet: {
startLine: 253,
endLine: 257,
text: ' if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path "${meteorPath}/;%path%`);\n return;\n }\n'
}
},
{
filePath: 'npm-packages/meteor-installer/install.js',
highlightedRegion: {
startLine: 255,
startColumn: 28,
endColumn: 62
},
codeSnippet: {
startLine: 253,
endLine: 257,
text: ' if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path "${meteorPath}/;%path%`);\n return;\n }\n'
}
}
]
},
{
threadFlows: [
{
filePath: 'npm-packages/meteor-installer/config2.js',
highlightedRegion: {
startLine: 35,
startColumn: 20,
endColumn: 61
},
codeSnippet: {
startLine: 33,
endLine: 37,
text: '\nconst meteorLocalFolder = \'.meteor\';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n'
}
},
{
filePath: 'npm-packages/meteor-installer/config2.js',
highlightedRegion: {
startLine: 35,
startColumn: 7,
endColumn: 61
},
codeSnippet: {
startLine: 33,
endLine: 37,
text: '\nconst meteorLocalFolder = \'.meteor\';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n'
}
},
{
filePath: 'npm-packages/meteor-installer/config2.js',
highlightedRegion: {
startLine: 40,
startColumn: 3,
endColumn: 13
},
codeSnippet: {
startLine: 38,
endLine: 42,
text: ' METEOR_LATEST_VERSION,\n extractPath: rootPath,\n meteorPath,\n release: process.env.INSTALL_METEOR_VERSION || METEOR_LATEST_VERSION,\n rootPath,\n'
}
},
{
filePath: 'npm-packages/meteor-installer/install2.js',
highlightedRegion: {
startLine: 12,
startColumn: 3,
endColumn: 13
},
codeSnippet: {
startLine: 10,
endLine: 14,
text: 'const os = require(\'os\');\nconst {\n meteorPath,\n release,\n startedPath,\n'
}
},
{
filePath: 'npm-packages/meteor-installer/install2.js',
highlightedRegion: {
startLine: 11,
startColumn: 7,
endLine: 22,
endColumn: 27
},
codeSnippet: {
startLine: 9,
endLine: 24,
text: 'const tmp = require(\'tmp\');\nconst os = require(\'os\');\nconst {\n meteorPath,\n release,\n startedPath,\n extractPath,\n isWindows,\n rootPath,\n sudoUser,\n isSudo,\n isMac,\n METEOR_LATEST_VERSION,\n} = require(\'./config.js\');\nconst { uninstall } = require(\'./uninstall\');\nconst {\n'
}
},
{
filePath: 'npm-packages/meteor-installer/install2.js',
highlightedRegion: {
startLine: 255,
startColumn: 42,
endColumn: 52
},
codeSnippet: {
startLine: 253,
endLine: 257,
text: ' if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path "${meteorPath}/;%path%`);\n return;\n }\n'
}
},
{
filePath: 'npm-packages/meteor-installer/install2.js',
highlightedRegion: {
startLine: 255,
startColumn: 28,
endColumn: 62
},
codeSnippet: {
startLine: 253,
endLine: 257,
text: ' if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path "${meteorPath}/;%path%`);\n return;\n }\n'
}
}
]
}
]
}
);

View File

@@ -1,6 +1,6 @@
import * as sarif from 'sarif';
import { AnalysisAlert, ResultSeverity } from './shared/analysis-result';
import { AnalysisAlert, CodeFlow, CodeSnippet, HighlightedRegion, ResultSeverity, ThreadFlow } from './shared/analysis-result';
const defaultSeverity = 'Warning';
@@ -36,77 +36,38 @@ export function extractAnalysisAlerts(
const severity = tryGetSeverity(run, result) || defaultSeverity;
const { codeFlows, errors: codeFlowsErrors } = extractCodeFlows(result);
if (codeFlowsErrors.length > 0) {
errors.push(...codeFlowsErrors);
continue;
}
if (!result.locations) {
errors.push('No locations found in the SARIF result');
continue;
}
const rule = tryGetRule(run, result);
const shortDescription = rule?.shortDescription?.text || message;
for (const location of result.locations) {
const contextRegion = location.physicalLocation?.contextRegion;
if (!contextRegion) {
errors.push('No context region found in the SARIF result location');
continue;
}
if (contextRegion.startLine === undefined) {
errors.push('No start line set for a result context region');
continue;
}
if (contextRegion.endLine === undefined) {
errors.push('No end line set for a result context region');
continue;
}
if (!contextRegion.snippet?.text) {
errors.push('No text set for a result context region');
continue;
}
const { processedLocation, errors: locationErrors } = extractLocation(location);
const region = location.physicalLocation?.region;
if (!region) {
errors.push('No region found in the SARIF result location');
continue;
}
if (region.startLine === undefined) {
errors.push('No start line set for a result region');
continue;
}
if (region.startColumn === undefined) {
errors.push('No start column set for a result region');
continue;
}
if (region.endColumn === undefined) {
errors.push('No end column set for a result region');
continue;
}
const filePath = location.physicalLocation?.artifactLocation?.uri;
if (!filePath) {
errors.push('No file path found in the SARIF result location');
if (locationErrors.length > 0) {
errors.push(...locationErrors);
continue;
}
const analysisAlert = {
message,
filePath,
shortDescription: shortDescription,
filePath: processedLocation!.filePath,
severity,
codeSnippet: {
startLine: contextRegion.startLine,
endLine: contextRegion.endLine,
text: contextRegion.snippet.text
},
highlightedRegion: {
startLine: region.startLine,
startColumn: region.startColumn,
endLine: region.endLine,
endColumn: region.endColumn
}
codeSnippet: processedLocation!.codeSnippet,
highlightedRegion: processedLocation!.highlightedRegion,
codeFlows: codeFlows
};
const validationErrors = getAlertValidationErrors(analysisAlert);
if (validationErrors.length > 0) {
errors.push(...validationErrors);
continue;
}
alerts.push(analysisAlert);
}
}
@@ -186,18 +147,149 @@ export function tryGetRule(
return undefined;
}
function getAlertValidationErrors(alert: AnalysisAlert): string[] {
const errors = [];
interface Location {
message?: string;
filePath: string;
codeSnippet: CodeSnippet,
highlightedRegion: HighlightedRegion
}
if (alert.codeSnippet.startLine > alert.codeSnippet.endLine) {
errors.push('The code snippet start line is greater than the end line');
function validateContextRegion(contextRegion: sarif.Region | undefined): string[] {
const errors: string[] = [];
if (!contextRegion) {
errors.push('No context region found in the SARIF result location');
return errors;
}
if (contextRegion.startLine === undefined) {
errors.push('No start line set for a result context region');
}
if (contextRegion.endLine === undefined) {
errors.push('No end line set for a result context region');
}
if (!contextRegion.snippet?.text) {
errors.push('No text set for a result context region');
}
const highlightedRegion = alert.highlightedRegion;
if (highlightedRegion.endLine === highlightedRegion.startLine &&
highlightedRegion.endColumn < highlightedRegion.startColumn) {
errors.push('The highlighted region end column is greater than the start column');
if (errors.length > 0) {
return errors;
}
if (contextRegion.startLine! > contextRegion.endLine!) {
errors.push('Start line is greater than the end line in result context region');
}
return errors;
}
function validateRegion(region: sarif.Region | undefined): string[] {
const errors: string[] = [];
if (!region) {
errors.push('No region found in the SARIF result location');
return errors;
}
if (region.startLine === undefined) {
errors.push('No start line set for a result region');
}
if (region.startColumn === undefined) {
errors.push('No start column set for a result region');
}
if (region.endColumn === undefined) {
errors.push('No end column set for a result region');
}
if (errors.length > 0) {
return errors;
}
if (region.endLine! === region.startLine! &&
region.endColumn! < region.startColumn!) {
errors.push('End column is greater than the start column in a result region');
}
return errors;
}
function extractLocation(
location: sarif.Location
): {
processedLocation: Location | undefined,
errors: string[]
} {
const message = location.message?.text;
const errors = [];
const contextRegion = location.physicalLocation?.contextRegion;
const contextRegionErrors = validateContextRegion(contextRegion);
errors.push(...contextRegionErrors);
const region = location.physicalLocation?.region;
const regionErrors = validateRegion(region);
errors.push(...regionErrors);
const filePath = location.physicalLocation?.artifactLocation?.uri;
if (!filePath) {
errors.push('No file path found in the SARIF result location');
}
if (errors.length > 0) {
return { processedLocation: undefined, errors };
}
const processedLocation = {
message,
filePath,
codeSnippet: {
startLine: contextRegion!.startLine,
endLine: contextRegion!.endLine,
text: contextRegion!.snippet!.text
},
highlightedRegion: {
startLine: region!.startLine,
startColumn: region!.startColumn,
endLine: region!.endLine,
endColumn: region!.endColumn
}
} as Location;
return { processedLocation, errors: [] };
}
function extractCodeFlows(
result: sarif.Result
): {
codeFlows: CodeFlow[],
errors: string[]
} {
const codeFlows = [];
const errors = [];
if (result.codeFlows) {
for (const codeFlow of result.codeFlows) {
const threadFlows = [];
for (const threadFlow of codeFlow.threadFlows) {
for (const location of threadFlow.locations) {
const { processedLocation, errors: locationErrors } = extractLocation(location);
if (locationErrors.length > 0) {
errors.push(...locationErrors);
continue;
}
threadFlows.push({
filePath: processedLocation!.filePath,
codeSnippet: processedLocation!.codeSnippet,
highlightedRegion: processedLocation!.highlightedRegion,
message: processedLocation!.message
} as ThreadFlow);
}
}
codeFlows.push({ threadFlows } as CodeFlow);
}
}
return { errors, codeFlows: [] };
}

View File

@@ -8,10 +8,12 @@ export interface AnalysisResults {
export interface AnalysisAlert {
message: string;
shortDescription: string;
severity: ResultSeverity;
filePath: string;
codeSnippet: CodeSnippet
highlightedRegion: HighlightedRegion
codeSnippet: CodeSnippet;
highlightedRegion: HighlightedRegion;
codeFlows: CodeFlow[];
}
export interface CodeSnippet {
@@ -27,4 +29,15 @@ export interface HighlightedRegion {
endColumn: number;
}
export interface CodeFlow {
threadFlows: ThreadFlow[];
}
export interface ThreadFlow {
filePath: string;
codeSnippet: CodeSnippet;
highlightedRegion: HighlightedRegion;
message?: string;
}
export type ResultSeverity = 'Recommendation' | 'Warning' | 'Error';

View File

@@ -1,8 +1,10 @@
import * as React from 'react';
import { AnalysisAlert } from '../shared/analysis-result';
import CodePaths from './CodePaths';
import FileCodeSnippet from './FileCodeSnippet';
const AnalysisAlertResult = ({ alert }: { alert: AnalysisAlert }) => {
const showPathsLink = alert.codeFlows.length > 0;
return <FileCodeSnippet
filePath={alert.filePath}
@@ -10,6 +12,14 @@ const AnalysisAlertResult = ({ alert }: { alert: AnalysisAlert }) => {
highlightedRegion={alert.highlightedRegion}
severity={alert.severity}
message={alert.message}
messageChildren={
showPathsLink && <CodePaths
codeFlows={alert.codeFlows}
ruleDescription={alert.shortDescription}
severity={alert.severity}
message={alert.message}
/>
}
/>;
};

View File

@@ -0,0 +1,180 @@
import { TriangleDownIcon, XCircleIcon } from '@primer/octicons-react';
import { ActionList, ActionMenu, Box, Button, Label, Link, Overlay } from '@primer/react';
import * as React from 'react';
import { useRef, useState } from 'react';
import styled from 'styled-components';
import { CodeFlow, ResultSeverity } from '../shared/analysis-result';
import FileCodeSnippet from './FileCodeSnippet';
import SectionTitle from './SectionTitle';
import VerticalSpace from './VerticalSpace';
const StyledCloseButton = styled.button`
position: absolute;
top: 1em;
right: 4em;
background-color: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
border: none;
&:focus-visible {
outline: none
}
`;
const OverlayContainer = styled.div`
padding: 1em;
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} >
<XCircleIcon size={24} />
</StyledCloseButton>
);
const CodePath = ({
codeFlow,
message,
severity
}: {
codeFlow: CodeFlow;
message: string;
severity: ResultSeverity;
}) => {
return <>
{codeFlow.threadFlows.map((threadFlow, index) =>
<div key={`thread-flow-${index}`}>
{index !== 0 && <VerticalSpace size={3} />}
<Box display="flex" justifyContent="center" alignItems="center" width="42.5em">
<Box flexGrow={1} p={0} border="none">
<SectionTitle>Step {index + 1}</SectionTitle>
</Box>
{index === 0 &&
<Box p={0} border="none">
<Label>Source</Label>
</Box>
}
{index === codeFlow.threadFlows.length - 1 &&
<Box p={0} border="none">
<Label>Sink</Label>
</Box>
}
</Box>
<VerticalSpace size={2} />
<FileCodeSnippet
filePath={threadFlow.filePath}
codeSnippet={threadFlow.codeSnippet}
highlightedRegion={threadFlow.highlightedRegion}
severity={severity}
message={index === codeFlow.threadFlows.length - 1 ? message : threadFlow.message} />
</div>
)}
</>;
};
const getCodeFlowName = (codeFlow: CodeFlow) => {
const filePath = codeFlow.threadFlows[codeFlow.threadFlows.length - 1].filePath;
return filePath.substring(filePath.lastIndexOf('/') + 1);
};
const Menu = ({
codeFlows,
setSelectedCodeFlow
}: {
codeFlows: CodeFlow[],
setSelectedCodeFlow: (value: React.SetStateAction<CodeFlow>) => void
}) => {
return <ActionMenu>
<ActionMenu.Anchor>
<Button variant="invisible" sx={{ fontWeight: 'normal', color: 'var(--vscode-editor-foreground);', padding: 0 }} >
{getCodeFlowName(codeFlows[0])}
<TriangleDownIcon size={16} />
</Button>
</ActionMenu.Anchor>
<ActionMenu.Overlay sx={{ backgroundColor: 'var(--vscode-editor-background)' }}>
<ActionList>
{codeFlows.map((codeFlow, index) =>
<ActionList.Item
key={`codeflow-${index}'`}
onSelect={(e: React.MouseEvent) => { setSelectedCodeFlow(codeFlow); }}>
{getCodeFlowName(codeFlow)}
</ActionList.Item>
)}
</ActionList>
</ActionMenu.Overlay>
</ActionMenu>;
};
const CodePaths = ({
codeFlows,
ruleDescription,
message,
severity
}: {
codeFlows: CodeFlow[],
ruleDescription: string,
message: string,
severity: ResultSeverity
}) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedCodeFlow, setSelectedCodeFlow] = useState(codeFlows[0]);
const anchorRef = useRef<HTMLDivElement>(null);
const linkRef = useRef<HTMLAnchorElement>(null);
const closeOverlay = () => setIsOpen(false);
return (
<Box ref={anchorRef}>
<Link
onClick={() => setIsOpen(true)}
ref={linkRef}
sx={{ cursor: 'pointer' }}>
Show paths
</Link>
{isOpen && (
<Overlay
returnFocusRef={linkRef}
onEscape={closeOverlay}
onClickOutside={closeOverlay}
anchorSide="outside-top">
<OverlayContainer>
<CloseButton onClick={closeOverlay} />
<SectionTitle>{ruleDescription}</SectionTitle>
<VerticalSpace size={2} />
<Box display="flex" justifyContent="center" alignItems="center">
<Box p={0} border="none">
{codeFlows.length} paths available: {selectedCodeFlow.threadFlows.length} steps in
</Box>
<Box flexGrow={1} p={0} paddingLeft="0.2em" border="none">
<Menu codeFlows={codeFlows} setSelectedCodeFlow={setSelectedCodeFlow} />
</Box>
</Box>
<VerticalSpace size={2} />
<CodePath
codeFlow={selectedCodeFlow}
severity={severity}
message={message} />
<VerticalSpace size={3} />
</OverlayContainer>
</Overlay>
)}
</Box>
);
};
export default CodePaths;