Skip to content

Commit c90eede

Browse files
authored
Merge pull request #1572 from github/koesie10/request-repo-results-message
Implement `requestRepoResults` message
2 parents dc6ae6c + 95cbe02 commit c90eede

File tree

9 files changed

+161
-23
lines changed

9 files changed

+161
-23
lines changed

extensions/ql-vscode/src/extension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,12 @@ async function activateWithInstalledDistribution(
956956
})
957957
);
958958

959+
ctx.subscriptions.push(
960+
commandRunner('codeQL.loadVariantAnalysisRepoResults', async (variantAnalysisId: number, repositoryFullName: string) => {
961+
await variantAnalysisManager.loadResults(variantAnalysisId, repositoryFullName);
962+
})
963+
);
964+
959965
// The "openVariantAnalysisView" command is internal-only.
960966
ctx.subscriptions.push(
961967
commandRunner('codeQL.openVariantAnalysisView', async (variantAnalysisId: number) => {

extensions/ql-vscode/src/pure/interface-types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,11 +459,17 @@ export interface SetRepoStatesMessage {
459459
repoStates: VariantAnalysisScannedRepositoryState[];
460460
}
461461

462+
export interface RequestRepositoryResultsMessage {
463+
t: 'requestRepositoryResults';
464+
repositoryFullName: string;
465+
}
466+
462467
export type ToVariantAnalysisMessage =
463468
| SetVariantAnalysisMessage
464469
| SetRepoResultsMessage
465470
| SetRepoStatesMessage;
466471

467472
export type FromVariantAnalysisMessage =
468473
| ViewLoadedMsg
469-
| StopVariantAnalysisMessage;
474+
| StopVariantAnalysisMessage
475+
| RequestRepositoryResultsMessage;

extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import {
1313
VariantAnalysis,
1414
VariantAnalysisScannedRepositoryDownloadStatus,
15+
VariantAnalysisScannedRepositoryResult,
1516
VariantAnalysisScannedRepositoryState
1617
} from './shared/variant-analysis';
1718
import { getErrorMessage } from '../pure/helpers-pure';
@@ -26,6 +27,7 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
2627

2728
private readonly variantAnalysisMonitor: VariantAnalysisMonitor;
2829
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager;
30+
private readonly variantAnalyses = new Map<number, VariantAnalysis>();
2931
private readonly views = new Map<number, VariantAnalysisView>();
3032

3133
constructor(
@@ -39,6 +41,7 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
3941
this.variantAnalysisMonitor.onVariantAnalysisChange(this.onVariantAnalysisUpdated.bind(this));
4042

4143
this.variantAnalysisResultsManager = this.push(new VariantAnalysisResultsManager(cliServer, storagePath, logger));
44+
this.variantAnalysisResultsManager.onResultLoaded(this.onRepoResultLoaded.bind(this));
4245
}
4346

4447
public async showView(variantAnalysisId: number): Promise<void> {
@@ -68,18 +71,33 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
6871
return this.views.get(variantAnalysisId);
6972
}
7073

74+
public async loadResults(variantAnalysisId: number, repositoryFullName: string): Promise<void> {
75+
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
76+
if (!variantAnalysis) {
77+
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
78+
}
79+
80+
await this.variantAnalysisResultsManager.loadResults(variantAnalysisId, repositoryFullName);
81+
}
82+
7183
private async onVariantAnalysisUpdated(variantAnalysis: VariantAnalysis | undefined): Promise<void> {
7284
if (!variantAnalysis) {
7385
return;
7486
}
7587

88+
this.variantAnalyses.set(variantAnalysis.id, variantAnalysis);
89+
7690
await this.getView(variantAnalysis.id)?.updateView(variantAnalysis);
7791
}
7892

7993
public onVariantAnalysisSubmitted(variantAnalysis: VariantAnalysis): void {
8094
this._onVariantAnalysisAdded.fire(variantAnalysis);
8195
}
8296

97+
private async onRepoResultLoaded(repositoryResult: VariantAnalysisScannedRepositoryResult): Promise<void> {
98+
await this.getView(repositoryResult.variantAnalysisId)?.sendRepositoryResults([repositoryResult]);
99+
}
100+
83101
private async onRepoStateUpdated(variantAnalysisId: number, repoState: VariantAnalysisScannedRepositoryState): Promise<void> {
84102
await this.getView(variantAnalysisId)?.updateRepoState(repoState);
85103
}

extensions/ql-vscode/src/remote-queries/variant-analysis-monitor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export class VariantAnalysisMonitor extends DisposableObject {
4141
let attemptCount = 0;
4242
const scannedReposDownloaded: number[] = [];
4343

44+
this._onVariantAnalysisChange.fire(variantAnalysis);
45+
4446
while (attemptCount <= VariantAnalysisMonitor.maxAttemptCount) {
4547
await this.sleep(VariantAnalysisMonitor.sleepTime);
4648

extensions/ql-vscode/src/remote-queries/variant-analysis-results-manager.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@ import { unzipFile } from '../pure/zip';
1818

1919
type CacheKey = `${number}/${string}`;
2020

21-
const createCacheKey = (variantAnalysisId: number, repoTask: VariantAnalysisRepoTask): CacheKey => `${variantAnalysisId}/${repoTask.repository.full_name}`;
21+
const createCacheKey = (variantAnalysisId: number, repositoryFullName: string): CacheKey => `${variantAnalysisId}/${repositoryFullName}`;
2222

2323
export type ResultDownloadedEvent = {
2424
variantAnalysisId: number;
2525
repoTask: VariantAnalysisRepoTask;
2626
}
2727

2828
export class VariantAnalysisResultsManager extends DisposableObject {
29+
private static readonly REPO_TASK_FILENAME = 'repo_task.json';
30+
private static readonly RESULTS_DIRECTORY = 'results';
31+
2932
private readonly cachedResults: Map<CacheKey, VariantAnalysisScannedRepositoryResult>;
3033

3134
private readonly _onResultDownloaded = this.push(new EventEmitter<ResultDownloadedEvent>());
@@ -63,8 +66,10 @@ export class VariantAnalysisResultsManager extends DisposableObject {
6366
await fs.mkdir(resultDirectory, { recursive: true });
6467
}
6568

69+
await fs.outputJson(path.join(resultDirectory, VariantAnalysisResultsManager.REPO_TASK_FILENAME), repoTask);
70+
6671
const zipFilePath = path.join(resultDirectory, 'results.zip');
67-
const unzippedFilesDirectory = path.join(resultDirectory, 'results');
72+
const unzippedFilesDirectory = path.join(resultDirectory, VariantAnalysisResultsManager.RESULTS_DIRECTORY);
6873

6974
fs.writeFileSync(zipFilePath, Buffer.from(result));
7075
await unzipFile(zipFilePath, unzippedFilesDirectory);
@@ -77,41 +82,44 @@ export class VariantAnalysisResultsManager extends DisposableObject {
7782

7883
public async loadResults(
7984
variantAnalysisId: number,
80-
repoTask: VariantAnalysisRepoTask
85+
repositoryFullName: string
8186
): Promise<VariantAnalysisScannedRepositoryResult> {
82-
const result = this.cachedResults.get(createCacheKey(variantAnalysisId, repoTask));
87+
const result = this.cachedResults.get(createCacheKey(variantAnalysisId, repositoryFullName));
8388

84-
return result ?? await this.loadResultsIntoMemory(variantAnalysisId, repoTask);
89+
return result ?? await this.loadResultsIntoMemory(variantAnalysisId, repositoryFullName);
8590
}
8691

8792
private async loadResultsIntoMemory(
8893
variantAnalysisId: number,
89-
repoTask: VariantAnalysisRepoTask,
94+
repositoryFullName: string,
9095
): Promise<VariantAnalysisScannedRepositoryResult> {
91-
const result = await this.loadResultsFromStorage(variantAnalysisId, repoTask);
92-
this.cachedResults.set(createCacheKey(variantAnalysisId, repoTask), result);
96+
const result = await this.loadResultsFromStorage(variantAnalysisId, repositoryFullName);
97+
this.cachedResults.set(createCacheKey(variantAnalysisId, repositoryFullName), result);
9398
this._onResultLoaded.fire(result);
9499
return result;
95100
}
96101

97102
private async loadResultsFromStorage(
98103
variantAnalysisId: number,
99-
repoTask: VariantAnalysisRepoTask,
104+
repositoryFullName: string,
100105
): Promise<VariantAnalysisScannedRepositoryResult> {
101-
if (!(await this.isVariantAnalysisRepoDownloaded(variantAnalysisId, repoTask))) {
106+
if (!(await this.isVariantAnalysisRepoDownloaded(variantAnalysisId, repositoryFullName))) {
102107
throw new Error('Variant analysis results not downloaded');
103108
}
104109

110+
const storageDirectory = this.getRepoStorageDirectory(variantAnalysisId, repositoryFullName);
111+
112+
const repoTask: VariantAnalysisRepoTask = await fs.readJson(path.join(storageDirectory, VariantAnalysisResultsManager.REPO_TASK_FILENAME));
113+
105114
if (!repoTask.database_commit_sha || !repoTask.source_location_prefix) {
106115
throw new Error('Missing database commit SHA');
107116
}
108117

109118
const fileLinkPrefix = this.createGitHubDotcomFileLinkPrefix(repoTask.repository.full_name, repoTask.database_commit_sha);
110119

111-
const storageDirectory = this.getRepoStorageDirectory(variantAnalysisId, repoTask.repository.full_name);
112-
113-
const sarifPath = path.join(storageDirectory, 'results.sarif');
114-
const bqrsPath = path.join(storageDirectory, 'results.bqrs');
120+
const resultsDirectory = path.join(storageDirectory, VariantAnalysisResultsManager.RESULTS_DIRECTORY);
121+
const sarifPath = path.join(resultsDirectory, 'results.sarif');
122+
const bqrsPath = path.join(resultsDirectory, 'results.bqrs');
115123
if (await fs.pathExists(sarifPath)) {
116124
const interpretedResults = await this.readSarifResults(sarifPath, fileLinkPrefix);
117125

@@ -137,9 +145,9 @@ export class VariantAnalysisResultsManager extends DisposableObject {
137145

138146
private async isVariantAnalysisRepoDownloaded(
139147
variantAnalysisId: number,
140-
repoTask: VariantAnalysisRepoTask,
148+
repositoryFullName: string,
141149
): Promise<boolean> {
142-
return await fs.pathExists(this.getRepoStorageDirectory(variantAnalysisId, repoTask.repository.full_name));
150+
return await fs.pathExists(this.getRepoStorageDirectory(variantAnalysisId, repositoryFullName));
143151
}
144152

145153
private async readBqrsResults(filePath: string, fileLinkPrefix: string, sourceLocationPrefix: string): Promise<AnalysisRawResults> {

extensions/ql-vscode/src/remote-queries/variant-analysis-view.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ExtensionContext, ViewColumn } from 'vscode';
1+
import { commands, ExtensionContext, ViewColumn } from 'vscode';
22
import { AbstractWebview, WebviewPanelConfig } from '../abstract-webview';
33
import { logger } from '../logging';
44
import { FromVariantAnalysisMessage, ToVariantAnalysisMessage } from '../pure/interface-types';
@@ -7,6 +7,7 @@ import {
77
VariantAnalysis,
88
VariantAnalysisQueryLanguage,
99
VariantAnalysisRepoStatus,
10+
VariantAnalysisScannedRepositoryResult,
1011
VariantAnalysisScannedRepositoryState,
1112
VariantAnalysisStatus
1213
} from './shared/variant-analysis';
@@ -53,6 +54,17 @@ export class VariantAnalysisView extends AbstractWebview<ToVariantAnalysisMessag
5354
});
5455
}
5556

57+
public async sendRepositoryResults(repositoryResult: VariantAnalysisScannedRepositoryResult[]): Promise<void> {
58+
if (!this.isShowingPanel) {
59+
return;
60+
}
61+
62+
await this.postMessage({
63+
t: 'setRepoResults',
64+
repoResults: repositoryResult,
65+
});
66+
}
67+
5668
protected getPanelConfig(): WebviewPanelConfig {
5769
return {
5870
viewId: VariantAnalysisView.viewType,
@@ -83,6 +95,9 @@ export class VariantAnalysisView extends AbstractWebview<ToVariantAnalysisMessag
8395
case 'stopVariantAnalysis':
8496
void logger.log(`Stop variant analysis: ${msg.variantAnalysisId}`);
8597
break;
98+
case 'requestRepositoryResults':
99+
void commands.executeCommand('codeQL.loadVariantAnalysisRepoResults', this.variantAnalysisId, msg.repositoryFullName);
100+
break;
86101
default:
87102
assertNever(msg);
88103
}

extensions/ql-vscode/src/view/variant-analysis/RepoRow.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { useCallback, useState } from 'react';
2+
import { useCallback, useEffect, useState } from 'react';
33
import styled from 'styled-components';
44
import { VSCodeBadge, VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react';
55
import {
@@ -11,6 +11,7 @@ import { formatDecimal } from '../../pure/number';
1111
import { Codicon, ErrorIcon, LoadingIcon, SuccessIcon, WarningIcon } from '../common';
1212
import { Repository } from '../../remote-queries/shared/repository';
1313
import { AnalysisAlert, AnalysisRawResults } from '../../remote-queries/shared/analysis-result';
14+
import { vscode } from '../vscode-api';
1415
import { AnalyzedRepoItemContent } from './AnalyzedRepoItemContent';
1516

1617
// This will ensure that these icons have a className which we can use in the TitleContainer
@@ -82,12 +83,36 @@ export const RepoRow = ({
8283
rawResults,
8384
}: RepoRowProps) => {
8485
const [isExpanded, setExpanded] = useState(false);
86+
const resultsLoaded = !!interpretedResults || !!rawResults;
87+
const [resultsLoading, setResultsLoading] = useState(false);
8588

86-
const toggleExpanded = useCallback(() => {
87-
setExpanded(oldIsExpanded => !oldIsExpanded);
88-
}, []);
89+
const toggleExpanded = useCallback(async () => {
90+
if (resultsLoading) {
91+
return;
92+
}
93+
94+
if (resultsLoaded || status !== VariantAnalysisRepoStatus.Succeeded) {
95+
setExpanded(oldIsExpanded => !oldIsExpanded);
96+
return;
97+
}
98+
99+
vscode.postMessage({
100+
t: 'requestRepositoryResults',
101+
repositoryFullName: repository.fullName,
102+
});
103+
104+
setResultsLoading(true);
105+
}, [resultsLoading, resultsLoaded, repository.fullName, status]);
106+
107+
useEffect(() => {
108+
if (resultsLoaded && resultsLoading) {
109+
setResultsLoading(false);
110+
setExpanded(true);
111+
}
112+
}, [resultsLoaded, resultsLoading]);
89113

90114
const disabled = !status || !isCompletedAnalysisRepoStatus(status);
115+
const expandableContentLoaded = status && (status !== VariantAnalysisRepoStatus.Succeeded || resultsLoaded);
91116

92117
return (
93118
<div>
@@ -107,7 +132,7 @@ export const RepoRow = ({
107132
</span>
108133
{downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.InProgress && <LoadingIcon label="Downloading" />}
109134
</TitleContainer>
110-
{isExpanded && status &&
135+
{isExpanded && expandableContentLoaded &&
111136
<AnalyzedRepoItemContent status={status} interpretedResults={interpretedResults} rawResults={rawResults} />}
112137
</div>
113138
);

extensions/ql-vscode/src/view/variant-analysis/__tests__/RepoRow.spec.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,56 @@ describe(RepoRow.name, () => {
164164
screen.getByText('Error: Timed out');
165165
});
166166

167+
it('can expand the repo item when succeeded and loaded', async () => {
168+
render({
169+
status: VariantAnalysisRepoStatus.Succeeded,
170+
interpretedResults: [],
171+
});
172+
173+
await userEvent.click(screen.getByRole('button', {
174+
expanded: false
175+
}));
176+
177+
expect(screen.getByRole('button', {
178+
expanded: true,
179+
})).toBeInTheDocument();
180+
});
181+
182+
it('can expand the repo item when succeeded and not loaded', async () => {
183+
const { rerender } = render({
184+
status: VariantAnalysisRepoStatus.Succeeded,
185+
});
186+
187+
await userEvent.click(screen.getByRole('button', {
188+
expanded: false
189+
}));
190+
191+
expect((window as any).vsCodeApi.postMessage).toHaveBeenCalledWith({
192+
t: 'requestRepositoryResults',
193+
repositoryFullName: 'octodemo/hello-world-1',
194+
});
195+
196+
expect(screen.getByRole('button', {
197+
expanded: false,
198+
})).toBeInTheDocument();
199+
200+
rerender(
201+
<RepoRow
202+
repository={{
203+
id: 1,
204+
fullName: 'octodemo/hello-world-1',
205+
private: false,
206+
}}
207+
status={VariantAnalysisRepoStatus.Succeeded}
208+
interpretedResults={[]}
209+
/>
210+
);
211+
212+
expect(screen.getByRole('button', {
213+
expanded: true,
214+
})).toBeInTheDocument();
215+
});
216+
167217
it('does not allow expanding the repo item when status is undefined', async () => {
168218
render({
169219
status: undefined,

extensions/ql-vscode/test/jest.setup.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,11 @@ Object.defineProperty(window, 'matchMedia', {
1414
dispatchEvent: jest.fn(),
1515
})),
1616
});
17+
18+
// Store this on the window so we can mock it
19+
(window as any).vsCodeApi = {
20+
postMessage: jest.fn(),
21+
setState: jest.fn(),
22+
};
23+
24+
(window as any).acquireVsCodeApi = () => (window as any).vsCodeApi;

0 commit comments

Comments
 (0)