Show remote analyses results status (#1108)

This commit is contained in:
Charis Kyriakou
2022-02-01 17:55:10 +00:00
committed by GitHub
parent 0672133bca
commit 5a9b49b9bb
14 changed files with 1462 additions and 112 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1005,6 +1005,7 @@
},
"dependencies": {
"@octokit/rest": "^18.5.6",
"@primer/react": "^34.3.0",
"child-process-promise": "^2.2.1",
"classnames": "~2.2.6",
"fs-extra": "^9.0.1",
@@ -1019,6 +1020,7 @@
"stream": "^0.0.2",
"stream-chain": "~2.2.4",
"stream-json": "~1.7.3",
"styled-components": "^5.3.3",
"tmp": "^0.1.0",
"tmp-promise": "~3.0.2",
"tree-kill": "~1.2.2",

View File

@@ -137,7 +137,8 @@ export class CompareInterfaceManager extends DisposableObject {
panel.webview.html = getHtmlForWebview(
panel.webview,
scriptPathOnDisk,
[stylesheetPathOnDisk]
[stylesheetPathOnDisk],
false
);
this.push(panel.webview.onDidReceiveMessage(
async (e) => this.handleMsgFromView(e),

View File

@@ -83,7 +83,7 @@ import { RemoteQuery } from './remote-queries/remote-query';
import { RemoteQueryResult } from './remote-queries/remote-query-result';
import { URLSearchParams } from 'url';
import { RemoteQueriesInterfaceManager } from './remote-queries/remote-queries-interface';
import { sampleRemoteQuery, sampleRemoteQueryResult } from './remote-queries/sample-data';
import * as sampleData from './remote-queries/sample-data';
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
import { AnalysesResultsManager } from './remote-queries/analyses-results-manager';
@@ -839,7 +839,11 @@ async function activateWithInstalledDistribution(
commandRunner('codeQL.showFakeRemoteQueryResults', async () => {
const analysisResultsManager = new AnalysesResultsManager(ctx, logger);
const rqim = new RemoteQueriesInterfaceManager(ctx, logger, analysisResultsManager);
await rqim.showResults(sampleRemoteQuery, sampleRemoteQueryResult);
await rqim.showResults(sampleData.sampleRemoteQuery, sampleData.sampleRemoteQueryResult);
await rqim.setAnalysisResults(sampleData.sampleAnalysesResultsStage1);
await rqim.setAnalysisResults(sampleData.sampleAnalysesResultsStage2);
await rqim.setAnalysisResults(sampleData.sampleAnalysesResultsStage3);
}));
ctx.subscriptions.push(

View File

@@ -119,6 +119,7 @@ export function getHtmlForWebview(
webview: Webview,
scriptUriOnDisk: Uri,
stylesheetUrisOnDisk: Uri[],
allowInlineStyles: boolean
): string {
// Convert the on-disk URIs into webview URIs.
const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk);
@@ -128,8 +129,13 @@ export function getHtmlForWebview(
// Use a nonce in the content security policy to uniquely identify the above resources.
const nonce = getNonce();
const stylesheetsHtmlLines = stylesheetWebviewUris.map(stylesheetWebviewUri =>
`<link nonce="${nonce}" rel="stylesheet" href="${stylesheetWebviewUri}">`);
const stylesheetsHtmlLines = allowInlineStyles
? stylesheetWebviewUris.map(uri => createStylesLinkWithoutNonce(uri))
: stylesheetWebviewUris.map(uri => createStylesLinkWithNonce(nonce, uri));
const styleSrc = allowInlineStyles
? 'https://*.vscode-webview.net/ vscode-file: \'unsafe-inline\''
: `'nonce-${nonce}'`;
/*
* Content security policy:
@@ -143,7 +149,7 @@ export function getHtmlForWebview(
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src ${webview.cspSource};">
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src ${styleSrc}; connect-src ${webview.cspSource};">
${stylesheetsHtmlLines.join(` ${os.EOL}`)}
</head>
<body>
@@ -243,3 +249,11 @@ export async function jumpToLocation(
}
}
}
function createStylesLinkWithNonce(nonce: string, uri: Uri): string {
return `<link nonce="${nonce}" rel="stylesheet" href="${uri}">`;
}
function createStylesLinkWithoutNonce(uri: Uri): string {
return `<link rel="stylesheet" href="${uri}">`;
}

View File

@@ -194,7 +194,8 @@ export class InterfaceManager extends DisposableObject {
panel.webview.html = getHtmlForWebview(
panel.webview,
scriptPathOnDisk,
[stylesheetPathOnDisk]
[stylesheetPathOnDisk],
false
);
this.push(panel.webview.onDidReceiveMessage(
async (e) => this.handleMsgFromView(e),

View File

@@ -6,6 +6,7 @@ import * as path from 'path';
import { AnalysisSummary } from './shared/remote-query-result';
import { AnalysisResults, QueryResult } from './shared/analysis-result';
import { UserCancellationException } from '../commandRunner';
import * as os from 'os';
import { sarifParser } from '../sarif-parser';
export class AnalysesResultsManager {
@@ -32,8 +33,7 @@ export class AnalysesResultsManager {
void this.logger.log(`Downloading and processing results for ${analysisSummary.nwo}`);
await this.downloadSingleAnalysisResults(analysisSummary, credentials);
await publishResults(this.analysesResults);
await this.downloadSingleAnalysisResults(analysisSummary, credentials, publishResults);
}
public async downloadAnalysesResults(
@@ -47,6 +47,7 @@ export class AnalysesResultsManager {
const batchSize = 3;
const numOfBatches = Math.ceil(analysesToDownload.length / batchSize);
const allFailures = [];
for (let i = 0; i < analysesToDownload.length; i += batchSize) {
if (token?.isCancellationRequested) {
@@ -54,14 +55,22 @@ export class AnalysesResultsManager {
}
const batch = analysesToDownload.slice(i, i + batchSize);
const batchTasks = batch.map(analysis => this.downloadSingleAnalysisResults(analysis, credentials));
const batchTasks = batch.map(analysis => this.downloadSingleAnalysisResults(analysis, credentials, publishResults));
const nwos = batch.map(a => a.nwo).join(', ');
void this.logger.log(`Downloading batch ${Math.floor(i / batchSize) + 1} of ${numOfBatches} (${nwos})`);
await Promise.all(batchTasks);
const taskResults = await Promise.allSettled(batchTasks);
const failedTasks = taskResults.filter(x => x.status === 'rejected') as Array<PromiseRejectedResult>;
if (failedTasks.length > 0) {
const failures = failedTasks.map(t => t.reason.message);
failures.forEach(f => void this.logger.log(f));
allFailures.push(...failures);
}
}
await publishResults(this.analysesResults);
if (allFailures.length > 0) {
throw Error(allFailures.join(os.EOL));
}
}
@@ -71,21 +80,36 @@ export class AnalysesResultsManager {
private async downloadSingleAnalysisResults(
analysis: AnalysisSummary,
credentials: Credentials
credentials: Credentials,
publishResults: (analysesResults: AnalysisResults[]) => Promise<void>
): Promise<void> {
const artifactPath = await downloadArtifactFromLink(credentials, analysis.downloadLink);
const analysisResults: AnalysisResults = {
nwo: analysis.nwo,
status: 'InProgress',
results: []
};
let analysisResults: AnalysisResults;
this.analysesResults.push(analysisResults);
void publishResults(this.analysesResults);
let artifactPath;
try {
artifactPath = await downloadArtifactFromLink(credentials, analysis.downloadLink);
}
catch (e) {
throw new Error(`Could not download the analysis results for ${analysis.nwo}: ${e.message}`);
}
if (path.extname(artifactPath) === '.sarif') {
const queryResults = await this.readResults(artifactPath);
analysisResults = { nwo: analysis.nwo, results: queryResults };
analysisResults.results = queryResults;
analysisResults.status = 'Completed';
} else {
void this.logger.log('Cannot download results. Only alert and path queries are fully supported.');
analysisResults = { nwo: analysis.nwo, results: [] };
analysisResults.status = 'Failed';
}
this.analysesResults.push(analysisResults);
void publishResults(this.analysesResults);
}
private async readResults(filePath: string): Promise<QueryResult[]> {

View File

@@ -125,7 +125,8 @@ export class RemoteQueriesInterfaceManager {
panel.webview.html = getHtmlForWebview(
panel.webview,
scriptPathOnDisk,
[baseStylesheetUriOnDisk, stylesheetPathOnDisk]
[baseStylesheetUriOnDisk, stylesheetPathOnDisk],
true
);
ctx.subscriptions.push(
panel.webview.onDidReceiveMessage(

View File

@@ -1,5 +1,6 @@
import { RemoteQuery } from './remote-query';
import { RemoteQueryResult } from './remote-query-result';
import { AnalysisResults } from './shared/analysis-result';
export const sampleRemoteQuery: RemoteQuery = {
queryName: 'Inefficient regular expression',
@@ -84,3 +85,95 @@ export const sampleRemoteQueryResult: RemoteQueryResult = {
}
]
};
const createAnalysisResults = (n: number) => Array(n).fill({ 'message': 'Sample text' });
export const sampleAnalysesResultsStage1: AnalysisResults[] = [
{
nwo: 'big-corp/repo1',
status: 'InProgress',
results: []
},
{
nwo: 'big-corp/repo2',
status: 'InProgress',
results: []
},
{
nwo: 'big-corp/repo3',
status: 'InProgress',
results: []
},
// No entries for repo4
];
export const sampleAnalysesResultsStage2: AnalysisResults[] = [
{
nwo: 'big-corp/repo1',
status: 'Completed',
results: createAnalysisResults(85)
},
{
nwo: 'big-corp/repo2',
status: 'Completed',
results: createAnalysisResults(20)
},
{
nwo: 'big-corp/repo3',
status: 'InProgress',
results: []
},
{
nwo: 'big-corp/repo4',
status: 'InProgress',
results: []
},
];
export const sampleAnalysesResultsStage3: AnalysisResults[] = [
{
nwo: 'big-corp/repo1',
status: 'Completed',
results: createAnalysisResults(85)
},
{
nwo: 'big-corp/repo2',
status: 'Completed',
results: createAnalysisResults(20)
},
{
nwo: 'big-corp/repo3',
status: 'Completed',
results: createAnalysisResults(8)
},
{
nwo: 'big-corp/repo4',
status: 'Completed',
results: createAnalysisResults(3)
},
];
export const sampleAnalysesResultsWithFailure: AnalysisResults[] = [
{
nwo: 'big-corp/repo1',
status: 'Completed',
results: createAnalysisResults(85)
},
{
nwo: 'big-corp/repo2',
status: 'Completed',
results: createAnalysisResults(20)
},
{
nwo: 'big-corp/repo3',
status: 'Failed',
results: []
},
{
nwo: 'big-corp/repo4',
status: 'Completed',
results: createAnalysisResults(3)
},
];

View File

@@ -1,5 +1,8 @@
export type AnalysisResultStatus = 'InProgress' | 'Completed' | 'Failed';
export interface AnalysisResults {
nwo: string;
status: AnalysisResultStatus;
results: QueryResult[];
}

View File

@@ -0,0 +1,10 @@
import { Spinner } from '@primer/react';
import * as React from 'react';
const DownloadSpinner = () => (
<span className="vscode-codeql__download-spinner">
<Spinner size="small" />
</span>
);
export default DownloadSpinner;

View File

@@ -0,0 +1,7 @@
import * as React from 'react';
const HorizontalSpace = () => (
<span className="vscode-codeql__horizontal-space" />
);
export default HorizontalSpace;

View File

@@ -1,6 +1,7 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import * as Rdom from 'react-dom';
import { ThemeProvider } from '@primer/react';
import { ToRemoteQueriesMessage } from '../../pure/interface-types';
import { AnalysisSummary, RemoteQueryResult } from '../shared/remote-query-result';
import * as octicons from '../../view/octicons';
@@ -9,10 +10,12 @@ import { vscode } from '../../view/vscode-api';
import SectionTitle from './SectionTitle';
import VerticalSpace from './VerticalSpace';
import HorizontalSpace from './HorizontalSpace';
import Badge from './Badge';
import ViewTitle from './ViewTitle';
import DownloadButton from './DownloadButton';
import { AnalysisResults } from '../shared/analysis-result';
import DownloadSpinner from './DownloadSpinner';
const numOfReposInContractedMode = 10;
@@ -61,6 +64,9 @@ const openQueryTextVirtualFile = (queryResult: RemoteQueryResult) => {
});
};
const sumAnalysesResults = (analysesResults: AnalysisResults[]) =>
analysesResults.reduce((acc, curr) => acc + curr.results.length, 0);
const QueryInfo = (queryResult: RemoteQueryResult) => (
<>
<VerticalSpace />
@@ -80,14 +86,26 @@ const QueryInfo = (queryResult: RemoteQueryResult) => (
</>
);
const SummaryTitleWithResults = (queryResult: RemoteQueryResult) => (
<div className="vscode-codeql__query-summary-container">
<SectionTitle text={`Repositories with results (${queryResult.affectedRepositoryCount}):`} />
<DownloadButton
text="Download all"
onClick={() => downloadAllAnalysesResults(queryResult)} />
</div>
);
const SummaryTitleWithResults = ({
queryResult,
analysesResults
}: {
queryResult: RemoteQueryResult,
analysesResults: AnalysisResults[]
}) => {
const showDownloadButton = queryResult.totalResultCount !== sumAnalysesResults(analysesResults);
return (
<div className="vscode-codeql__query-summary-container">
<SectionTitle text={`Repositories with results (${queryResult.affectedRepositoryCount}):`} />
{
showDownloadButton && <DownloadButton
text="Download all"
onClick={() => downloadAllAnalysesResults(queryResult)} />
}
</div>
);
};
const SummaryTitleNoResults = () => (
<div className="vscode-codeql__query-summary-container">
@@ -95,19 +113,55 @@ const SummaryTitleNoResults = () => (
</div>
);
const SummaryItem = (props: AnalysisSummary) => (
const SummaryItemDownload = ({
analysisSummary,
analysisResults
}: {
analysisSummary: AnalysisSummary,
analysisResults: AnalysisResults | undefined
}) => {
if (!analysisResults || analysisResults.status === 'Failed') {
return <DownloadButton
text={analysisSummary.fileSize}
onClick={() => downloadAnalysisResults(analysisSummary)} />;
}
if (analysisResults.status === 'InProgress') {
return <>
<HorizontalSpace />
<DownloadSpinner />
</>;
}
return (<></>);
};
const SummaryItem = ({
analysisSummary,
analysisResults
}: {
analysisSummary: AnalysisSummary,
analysisResults: AnalysisResults | undefined
}) => (
<span>
<span className="vscode-codeql__analysis-item">{octicons.repo}</span>
<span className="vscode-codeql__analysis-item">{props.nwo}</span>
<span className="vscode-codeql__analysis-item"><Badge text={props.resultCount.toString()} /></span>
<span className="vscode-codeql__analysis-item">{analysisSummary.nwo}</span>
<span className="vscode-codeql__analysis-item"><Badge text={analysisSummary.resultCount.toString()} /></span>
<span className="vscode-codeql__analysis-item">
<DownloadButton
text={props.fileSize}
onClick={() => downloadAnalysisResults(props)} />
<SummaryItemDownload
analysisSummary={analysisSummary}
analysisResults={analysisResults} />
</span>
</span>
);
const Summary = (queryResult: RemoteQueryResult) => {
const Summary = ({
queryResult,
analysesResults
}: {
queryResult: RemoteQueryResult,
analysesResults: AnalysisResults[]
}) => {
const [repoListExpanded, setRepoListExpanded] = useState(false);
const numOfReposToShow = repoListExpanded ? queryResult.analysisSummaries.length : numOfReposInContractedMode;
@@ -116,13 +170,17 @@ const Summary = (queryResult: RemoteQueryResult) => {
{
queryResult.affectedRepositoryCount === 0
? <SummaryTitleNoResults />
: <SummaryTitleWithResults {...queryResult} />
: <SummaryTitleWithResults
queryResult={queryResult}
analysesResults={analysesResults} />
}
<ul className="vscode-codeql__analysis-summaries-list">
{queryResult.analysisSummaries.slice(0, numOfReposToShow).map((summary, i) =>
<li key={summary.nwo} className="vscode-codeql__analysis-summaries-list-item">
<SummaryItem {...summary} />
<SummaryItem
analysisSummary={summary}
analysisResults={analysesResults.find(a => a.nwo === summary.nwo)} />
</li>
)}
</ul>
@@ -157,7 +215,7 @@ const AnalysesResultsDescription = ({ totalAnalysesResults, totalResults }: { to
};
const AnalysesResults = ({ analysesResults, totalResults }: { analysesResults: AnalysisResults[], totalResults: number }) => {
const totalAnalysesResults = analysesResults.reduce((acc, curr) => acc + curr.results.length, 0);
const totalAnalysesResults = sumAnalysesResults(analysesResults);
if (totalResults === 0) {
return <></>;
@@ -204,10 +262,12 @@ export function RemoteQueries(): JSX.Element {
try {
return <div>
<ViewTitle title={queryResult.queryTitle} />
<QueryInfo {...queryResult} />
<Summary {...queryResult} />
<AnalysesResults analysesResults={analysesResults} totalResults={queryResult.totalResultCount} />
<ThemeProvider>
<ViewTitle title={queryResult.queryTitle} />
<QueryInfo {...queryResult} />
<Summary queryResult={queryResult} analysesResults={analysesResults} />
<AnalysesResults analysesResults={analysesResults} totalResults={queryResult.totalResultCount} />
</ThemeProvider>
</div>;
} catch (err) {
console.error(err);

View File

@@ -35,6 +35,16 @@ body {
height: 0.5rem;
}
/* -------------------------------------------------------------------------- */
/* HorizontalSpace component */
/* -------------------------------------------------------------------------- */
.vscode-codeql__horizontal-space {
flex: 0 0 auto;
display: inline-block;
width: 0.2rem;
}
/* -------------------------------------------------------------------------- */
/* Badge component */
/* -------------------------------------------------------------------------- */
@@ -73,3 +83,15 @@ body {
.vscode-codeql__download-button svg {
fill: var(--vscode-textLink-foreground);
}
/* -------------------------------------------------------------------------- */
/* DownloadSpinner component */
/* -------------------------------------------------------------------------- */
.vscode-codeql__download-spinner {
vertical-align: middle;
}
.vscode-codeql__download-spinner svg {
width: 0.8em;
height: 0.8em;
}