Show remote analyses results status (#1108)
This commit is contained in:
1250
extensions/ql-vscode/package-lock.json
generated
1250
extensions/ql-vscode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}">`;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export type AnalysisResultStatus = 'InProgress' | 'Completed' | 'Failed';
|
||||
|
||||
export interface AnalysisResults {
|
||||
nwo: string;
|
||||
status: AnalysisResultStatus;
|
||||
results: QueryResult[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
const HorizontalSpace = () => (
|
||||
<span className="vscode-codeql__horizontal-space" />
|
||||
);
|
||||
|
||||
export default HorizontalSpace;
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user