Merge branch 'main' into starcke/general-setup
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
- When running variant analyses, don't download artifacts for repositories with no results. [#2736](https://github.com/github/vscode-codeql/pull/2736)
|
||||
- Group the extension settings, so that they're easier to find in the Settings UI. [#2706](https://github.com/github/vscode-codeql/pull/2706)
|
||||
|
||||
## 1.8.10 - 15 August 2023
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
"site_admin": true
|
||||
},
|
||||
"query_language": "javascript",
|
||||
"query_pack_url": "https://objects-origin.githubusercontent.com/codeql-query-console/variant_analyses/146/query_pack?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=queryconsoleprod%2F20221026%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221026T124815Z&X-Amz-Expires=3600&X-Amz-Signature=0f42201f27ad08ac1fa9caccf5005d9dac7bdcae33e88e33d62e2fc337194164&X-Amz-SignedHeaders=host",
|
||||
"query_pack_url": "https://objects-origin.githubusercontent.com/codeql-query-console/variant_analyses/146/query_pack?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=queryconsoleprod%2F20221026%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221026T124659Z&X-Amz-Expires=3600&X-Amz-Signature=b791ede467cd1783d31d7ee148763d0d4f9eb7abad7506ef9c25017c94aaa1df&X-Amz-SignedHeaders=host",
|
||||
"created_at": "2022-10-26T12:45:12Z",
|
||||
"updated_at": "2022-10-26T12:45:15Z",
|
||||
"actions_workflow_run_id": 3329095282,
|
||||
@@ -122,9 +122,7 @@
|
||||
"stargazers_count": 192,
|
||||
"updated_at": "2022-03-17T08:34:30Z"
|
||||
},
|
||||
"analysis_status": "succeeded",
|
||||
"artifact_size_in_bytes": 156706,
|
||||
"result_count": 8
|
||||
"analysis_status": "in_progress"
|
||||
},
|
||||
{
|
||||
"repository": {
|
||||
@@ -148,9 +146,7 @@
|
||||
"stargazers_count": 19033,
|
||||
"updated_at": "2022-11-02T10:25:24Z"
|
||||
},
|
||||
"analysis_status": "succeeded",
|
||||
"artifact_size_in_bytes": 710,
|
||||
"result_count": 0
|
||||
"analysis_status": "in_progress"
|
||||
},
|
||||
{
|
||||
"repository": {
|
||||
Binary file not shown.
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"request": {
|
||||
"kind": "getVariantAnalysisRepoResult",
|
||||
"repositoryId": 257485422
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"body": "file:15-getVariantAnalysisRepoResult.body.zip",
|
||||
"contentType": "application/zip"
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@
|
||||
"site_admin": true
|
||||
},
|
||||
"query_language": "javascript",
|
||||
"query_pack_url": "https://objects-origin.githubusercontent.com/codeql-query-console/variant_analyses/146/query_pack?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=queryconsoleprod%2F20221026%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221026T124659Z&X-Amz-Expires=3600&X-Amz-Signature=b791ede467cd1783d31d7ee148763d0d4f9eb7abad7506ef9c25017c94aaa1df&X-Amz-SignedHeaders=host",
|
||||
"query_pack_url": "https://objects-origin.githubusercontent.com/codeql-query-console/variant_analyses/146/query_pack?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=queryconsoleprod%2F20221026%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221026T124705Z&X-Amz-Expires=3600&X-Amz-Signature=dae9aa87c393b62ad6f84fd280c731fcd8e4551917920bf978eac5dd6102caec&X-Amz-SignedHeaders=host",
|
||||
"created_at": "2022-10-26T12:45:12Z",
|
||||
"updated_at": "2022-10-26T12:45:15Z",
|
||||
"actions_workflow_run_id": 3329095282,
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
"site_admin": true
|
||||
},
|
||||
"query_language": "javascript",
|
||||
"query_pack_url": "https://objects-origin.githubusercontent.com/codeql-query-console/variant_analyses/146/query_pack?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=queryconsoleprod%2F20221026%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221026T124705Z&X-Amz-Expires=3600&X-Amz-Signature=dae9aa87c393b62ad6f84fd280c731fcd8e4551917920bf978eac5dd6102caec&X-Amz-SignedHeaders=host",
|
||||
"query_pack_url": "https://objects-origin.githubusercontent.com/codeql-query-console/variant_analyses/146/query_pack?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=queryconsoleprod%2F20221026%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221026T124726Z&X-Amz-Expires=3600&X-Amz-Signature=6698289315bd8d8378a17784ba0be9955e6e9161497137c4ccb29b1d2a3f3587&X-Amz-SignedHeaders=host",
|
||||
"created_at": "2022-10-26T12:45:12Z",
|
||||
"updated_at": "2022-10-26T12:45:15Z",
|
||||
"actions_workflow_run_id": 3329095282,
|
||||
@@ -122,7 +122,9 @@
|
||||
"stargazers_count": 192,
|
||||
"updated_at": "2022-03-17T08:34:30Z"
|
||||
},
|
||||
"analysis_status": "in_progress"
|
||||
"analysis_status": "succeeded",
|
||||
"artifact_size_in_bytes": 156706,
|
||||
"result_count": 8
|
||||
},
|
||||
{
|
||||
"repository": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"body": "file:20-getVariantAnalysisRepoResult.body.zip",
|
||||
"body": "file:19-getVariantAnalysisRepoResult.body.zip",
|
||||
"contentType": "application/zip"
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@
|
||||
"site_admin": true
|
||||
},
|
||||
"query_language": "javascript",
|
||||
"query_pack_url": "https://objects-origin.githubusercontent.com/codeql-query-console/variant_analyses/146/query_pack?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=queryconsoleprod%2F20221026%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221026T124726Z&X-Amz-Expires=3600&X-Amz-Signature=6698289315bd8d8378a17784ba0be9955e6e9161497137c4ccb29b1d2a3f3587&X-Amz-SignedHeaders=host",
|
||||
"query_pack_url": "https://objects-origin.githubusercontent.com/codeql-query-console/variant_analyses/146/query_pack?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=queryconsoleprod%2F20221026%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221026T124732Z&X-Amz-Expires=3600&X-Amz-Signature=ad791eb1a754f473a50454616e9bd04663dfed60a5c84f2c61f27ced47ee25d0&X-Amz-SignedHeaders=host",
|
||||
"created_at": "2022-10-26T12:45:12Z",
|
||||
"updated_at": "2022-10-26T12:45:15Z",
|
||||
"actions_workflow_run_id": 3329095282,
|
||||
@@ -94,7 +94,7 @@
|
||||
"site_admin": true
|
||||
},
|
||||
"query_language": "javascript",
|
||||
"query_pack_url": "https://objects-origin.githubusercontent.com/codeql-query-console/variant_analyses/146/query_pack?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=queryconsoleprod%2F20221026%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221026T124732Z&X-Amz-Expires=3600&X-Amz-Signature=ad791eb1a754f473a50454616e9bd04663dfed60a5c84f2c61f27ced47ee25d0&X-Amz-SignedHeaders=host",
|
||||
"query_pack_url": "https://objects-origin.githubusercontent.com/codeql-query-console/variant_analyses/146/query_pack?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=queryconsoleprod%2F20221026%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221026T124759Z&X-Amz-Expires=3600&X-Amz-Signature=5b0be34c426152c37e3d4c761bf576a07c9d4bb6222f32c438e02605600f0394&X-Amz-SignedHeaders=host",
|
||||
"created_at": "2022-10-26T12:45:12Z",
|
||||
"updated_at": "2022-10-26T12:45:15Z",
|
||||
"actions_workflow_run_id": 3329095282,
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
"site_admin": true
|
||||
},
|
||||
"query_language": "javascript",
|
||||
"query_pack_url": "https://objects-origin.githubusercontent.com/codeql-query-console/variant_analyses/146/query_pack?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=queryconsoleprod%2F20221026%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221026T124759Z&X-Amz-Expires=3600&X-Amz-Signature=5b0be34c426152c37e3d4c761bf576a07c9d4bb6222f32c438e02605600f0394&X-Amz-SignedHeaders=host",
|
||||
"query_pack_url": "https://objects-origin.githubusercontent.com/codeql-query-console/variant_analyses/146/query_pack?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=queryconsoleprod%2F20221026%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221026T124805Z&X-Amz-Expires=3600&X-Amz-Signature=73b43f47ee53938b91fa7d23c33e750c466fff777ea94ceb052771ee8dc25b6c&X-Amz-SignedHeaders=host",
|
||||
"created_at": "2022-10-26T12:45:12Z",
|
||||
"updated_at": "2022-10-26T12:45:15Z",
|
||||
"actions_workflow_run_id": 3329095282,
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
"site_admin": true
|
||||
},
|
||||
"query_language": "javascript",
|
||||
"query_pack_url": "https://objects-origin.githubusercontent.com/codeql-query-console/variant_analyses/146/query_pack?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=queryconsoleprod%2F20221026%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221026T124805Z&X-Amz-Expires=3600&X-Amz-Signature=73b43f47ee53938b91fa7d23c33e750c466fff777ea94ceb052771ee8dc25b6c&X-Amz-SignedHeaders=host",
|
||||
"query_pack_url": "https://objects-origin.githubusercontent.com/codeql-query-console/variant_analyses/146/query_pack?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=queryconsoleprod%2F20221026%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221026T124815Z&X-Amz-Expires=3600&X-Amz-Signature=0f42201f27ad08ac1fa9caccf5005d9dac7bdcae33e88e33d62e2fc337194164&X-Amz-SignedHeaders=host",
|
||||
"created_at": "2022-10-26T12:45:12Z",
|
||||
"updated_at": "2022-10-26T12:45:15Z",
|
||||
"actions_workflow_run_id": 3329095282,
|
||||
@@ -148,7 +148,9 @@
|
||||
"stargazers_count": 19033,
|
||||
"updated_at": "2022-11-02T10:25:24Z"
|
||||
},
|
||||
"analysis_status": "in_progress"
|
||||
"analysis_status": "succeeded",
|
||||
"artifact_size_in_bytes": 710,
|
||||
"result_count": 0
|
||||
},
|
||||
{
|
||||
"repository": {
|
||||
|
||||
Binary file not shown.
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"request": {
|
||||
"kind": "getVariantAnalysisRepoResult",
|
||||
"repositoryId": 236095576
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"body": "file:26-getVariantAnalysisRepoResult.body.zip",
|
||||
"contentType": "application/zip"
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,13 @@ import { join } from "path";
|
||||
|
||||
import { DisposableObject, DisposeHandler } from "../disposable-object";
|
||||
import { tmpDir } from "../../tmp-dir";
|
||||
import { getHtmlForWebview, WebviewMessage, WebviewView } from "./webview-html";
|
||||
import { getHtmlForWebview, WebviewMessage, WebviewKind } from "./webview-html";
|
||||
|
||||
export type WebviewPanelConfig = {
|
||||
viewId: string;
|
||||
title: string;
|
||||
viewColumn: ViewColumn;
|
||||
view: WebviewView;
|
||||
view: WebviewKind;
|
||||
preserveFocus?: boolean;
|
||||
iconPath?: Uri | { dark: Uri; light: Uri };
|
||||
additionalOptions?: WebviewPanelOptions & WebviewOptions;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ExtensionContext, Uri, Webview } from "vscode";
|
||||
import { randomBytes } from "crypto";
|
||||
import { EOL } from "os";
|
||||
|
||||
export type WebviewView =
|
||||
export type WebviewKind =
|
||||
| "results"
|
||||
| "compare"
|
||||
| "variant-analysis"
|
||||
@@ -20,7 +20,7 @@ export interface WebviewMessage {
|
||||
export function getHtmlForWebview(
|
||||
ctx: ExtensionContext,
|
||||
webview: Webview,
|
||||
view: WebviewView,
|
||||
view: WebviewKind,
|
||||
{
|
||||
allowInlineStyles,
|
||||
allowWasmEval,
|
||||
|
||||
@@ -55,6 +55,7 @@ function mapVariantAnalysisDtoToDto(
|
||||
filePath: variantAnalysis.query.filePath,
|
||||
language: mapQueryLanguageToDto(variantAnalysis.query.language),
|
||||
text: variantAnalysis.query.text,
|
||||
kind: variantAnalysis.query.kind,
|
||||
},
|
||||
databases: {
|
||||
repositories: variantAnalysis.databases.repositories,
|
||||
|
||||
@@ -55,6 +55,7 @@ function mapVariantAnalysisToDomainModel(
|
||||
filePath: variantAnalysis.query.filePath,
|
||||
language: mapQueryLanguageToDomainModel(variantAnalysis.query.language),
|
||||
text: variantAnalysis.query.text,
|
||||
kind: variantAnalysis.query.kind,
|
||||
},
|
||||
databases: {
|
||||
repositories: variantAnalysis.databases.repositories,
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface VariantAnalysisDto {
|
||||
filePath: string;
|
||||
language: QueryLanguageDto;
|
||||
text: string;
|
||||
kind?: string;
|
||||
};
|
||||
databases: {
|
||||
repositories?: string[];
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface VariantAnalysis {
|
||||
filePath: string;
|
||||
language: QueryLanguage;
|
||||
text: string;
|
||||
kind?: string;
|
||||
};
|
||||
databases: {
|
||||
repositories?: string[];
|
||||
@@ -138,6 +139,7 @@ export interface VariantAnalysisSubmission {
|
||||
filePath: string;
|
||||
language: QueryLanguage;
|
||||
text: string;
|
||||
kind?: string;
|
||||
|
||||
// Base64 encoded query pack.
|
||||
pack: string;
|
||||
@@ -231,7 +233,11 @@ export function isRepoScanSuccessful(
|
||||
export function repoHasDownloadableArtifact(
|
||||
repo: VariantAnalysisScannedRepository,
|
||||
): boolean {
|
||||
return repo.analysisStatus === VariantAnalysisRepoStatus.Succeeded;
|
||||
return (
|
||||
repo.analysisStatus === VariantAnalysisRepoStatus.Succeeded &&
|
||||
repo.resultCount !== undefined &&
|
||||
repo.resultCount > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -245,6 +245,7 @@ export class VariantAnalysisManager
|
||||
pack: base64Pack,
|
||||
language: variantAnalysisLanguage,
|
||||
text: queryText,
|
||||
kind: queryMetadata?.kind,
|
||||
},
|
||||
databases: {
|
||||
repositories: repoSelection.repositories,
|
||||
|
||||
@@ -32,6 +32,7 @@ export function processVariantAnalysis(
|
||||
filePath: submission.query.filePath,
|
||||
language: submission.query.language,
|
||||
text: submission.query.text,
|
||||
kind: submission.query.kind,
|
||||
},
|
||||
databases: submission.databases,
|
||||
executionStartTime: submission.startTime,
|
||||
|
||||
@@ -199,34 +199,35 @@ export class VariantAnalysisResultsManager extends DisposableObject {
|
||||
);
|
||||
const sarifPath = join(resultsDirectory, "results.sarif");
|
||||
const bqrsPath = join(resultsDirectory, "results.bqrs");
|
||||
|
||||
let interpretedResults: AnalysisAlert[] | undefined;
|
||||
let rawResults: AnalysisRawResults | undefined;
|
||||
|
||||
if (await pathExists(sarifPath)) {
|
||||
const interpretedResults = await this.readSarifResults(
|
||||
interpretedResults = await this.readSarifResults(
|
||||
sarifPath,
|
||||
fileLinkPrefix,
|
||||
);
|
||||
|
||||
return {
|
||||
variantAnalysisId,
|
||||
repositoryId: repoTask.repository.id,
|
||||
interpretedResults,
|
||||
};
|
||||
}
|
||||
|
||||
if (await pathExists(bqrsPath)) {
|
||||
const rawResults = await this.readBqrsResults(
|
||||
rawResults = await this.readBqrsResults(
|
||||
bqrsPath,
|
||||
fileLinkPrefix,
|
||||
repoTask.sourceLocationPrefix,
|
||||
);
|
||||
|
||||
return {
|
||||
variantAnalysisId,
|
||||
repositoryId: repoTask.repository.id,
|
||||
rawResults,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error("Missing results file");
|
||||
if (!interpretedResults && !rawResults) {
|
||||
throw new Error("Missing results file");
|
||||
}
|
||||
|
||||
return {
|
||||
variantAnalysisId,
|
||||
repositoryId: repoTask.repository.id,
|
||||
interpretedResults,
|
||||
rawResults,
|
||||
};
|
||||
}
|
||||
|
||||
public async isVariantAnalysisRepoDownloaded(
|
||||
|
||||
@@ -20,37 +20,36 @@ import { AlertTableHeader } from "./AlertTableHeader";
|
||||
import { AlertTableNoResults } from "./AlertTableNoResults";
|
||||
import { AlertTableTruncatedMessage } from "./AlertTableTruncatedMessage";
|
||||
import { AlertTableResultRow } from "./AlertTableResultRow";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
type AlertTableProps = ResultTableProps & {
|
||||
resultSet: InterpretedResultSet<SarifInterpretationData>;
|
||||
};
|
||||
interface AlertTableState {
|
||||
expanded: Set<string>;
|
||||
selectedItem: undefined | Keys.ResultKey;
|
||||
}
|
||||
|
||||
export class AlertTable extends React.Component<
|
||||
AlertTableProps,
|
||||
AlertTableState
|
||||
> {
|
||||
private scroller = new ScrollIntoViewHelper();
|
||||
export function AlertTable(props: AlertTableProps) {
|
||||
const { databaseUri, resultSet } = props;
|
||||
|
||||
constructor(props: AlertTableProps) {
|
||||
super(props);
|
||||
this.state = { expanded: new Set<string>(), selectedItem: undefined };
|
||||
this.handleNavigationEvent = this.handleNavigationEvent.bind(this);
|
||||
const scroller = useRef<ScrollIntoViewHelper | undefined>(undefined);
|
||||
if (scroller.current === undefined) {
|
||||
scroller.current = new ScrollIntoViewHelper();
|
||||
}
|
||||
useEffect(() => scroller.current?.update());
|
||||
|
||||
const [expanded, setExpanded] = useState<Set<string>>(new Set<string>());
|
||||
const [selectedItem, setSelectedItem] = useState<Keys.ResultKey | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
/**
|
||||
* Given a list of `keys`, toggle the first, and if we 'open' the
|
||||
* first item, open all the rest as well. This mimics vscode's file
|
||||
* explorer tree view behavior.
|
||||
*/
|
||||
toggle(e: React.MouseEvent, keys: Keys.ResultKey[]) {
|
||||
const toggle = useCallback((e: React.MouseEvent, keys: Keys.ResultKey[]) => {
|
||||
const keyStrings = keys.map(Keys.keyToString);
|
||||
this.setState((previousState) => {
|
||||
const expanded = new Set(previousState.expanded);
|
||||
if (previousState.expanded.has(keyStrings[0])) {
|
||||
setExpanded((previousExpanded) => {
|
||||
const expanded = new Set(previousExpanded);
|
||||
if (previousExpanded.has(keyStrings[0])) {
|
||||
expanded.delete(keyStrings[0]);
|
||||
} else {
|
||||
for (const str of keyStrings) {
|
||||
@@ -60,127 +59,16 @@ export class AlertTable extends React.Component<
|
||||
if (expanded) {
|
||||
sendTelemetry("local-results-alert-table-path-expanded");
|
||||
}
|
||||
return { expanded };
|
||||
return expanded;
|
||||
});
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}, []);
|
||||
|
||||
render(): JSX.Element {
|
||||
const { databaseUri, resultSet } = this.props;
|
||||
|
||||
const { numTruncatedResults, sourceLocationPrefix } =
|
||||
resultSet.interpretation;
|
||||
|
||||
const updateSelectionCallback = (
|
||||
resultKey: Keys.PathNode | Keys.Result | undefined,
|
||||
) => {
|
||||
this.setState((previousState) => ({
|
||||
...previousState,
|
||||
selectedItem: resultKey,
|
||||
}));
|
||||
sendTelemetry("local-results-alert-table-path-selected");
|
||||
};
|
||||
|
||||
if (!resultSet.interpretation.data.runs?.[0]?.results?.length) {
|
||||
return <AlertTableNoResults {...this.props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<table className={className}>
|
||||
<AlertTableHeader sortState={resultSet.interpretation.data.sortState} />
|
||||
<tbody>
|
||||
{resultSet.interpretation.data.runs[0].results.map(
|
||||
(result, resultIndex) => (
|
||||
<AlertTableResultRow
|
||||
key={resultIndex}
|
||||
result={result}
|
||||
resultIndex={resultIndex}
|
||||
expanded={this.state.expanded}
|
||||
selectedItem={this.state.selectedItem}
|
||||
databaseUri={databaseUri}
|
||||
sourceLocationPrefix={sourceLocationPrefix}
|
||||
updateSelectionCallback={updateSelectionCallback}
|
||||
toggleExpanded={this.toggle.bind(this)}
|
||||
scroller={this.scroller}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<AlertTableTruncatedMessage
|
||||
numTruncatedResults={numTruncatedResults}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
private handleNavigationEvent(event: NavigateMsg) {
|
||||
this.setState((prevState) => {
|
||||
const key = this.getNewSelection(prevState.selectedItem, event.direction);
|
||||
const data = this.props.resultSet.interpretation.data;
|
||||
|
||||
// Check if the selected node actually exists (bounds check) and get its location if relevant
|
||||
let jumpLocation: Sarif.Location | undefined;
|
||||
if (key.pathNodeIndex !== undefined) {
|
||||
jumpLocation = Keys.getPathNode(data, key);
|
||||
if (jumpLocation === undefined) {
|
||||
return prevState; // Result does not exist
|
||||
}
|
||||
} else if (key.pathIndex !== undefined) {
|
||||
if (Keys.getPath(data, key) === undefined) {
|
||||
return prevState; // Path does not exist
|
||||
}
|
||||
jumpLocation = undefined; // When selecting a 'path', don't jump anywhere.
|
||||
} else {
|
||||
jumpLocation = Keys.getResult(data, key)?.locations?.[0];
|
||||
if (jumpLocation === undefined) {
|
||||
return prevState; // Path step does not exist.
|
||||
}
|
||||
}
|
||||
if (jumpLocation !== undefined) {
|
||||
const parsedLocation = parseSarifLocation(
|
||||
jumpLocation,
|
||||
this.props.resultSet.interpretation.sourceLocationPrefix,
|
||||
);
|
||||
if (!isNoLocation(parsedLocation)) {
|
||||
jumpToLocation(parsedLocation, this.props.databaseUri);
|
||||
}
|
||||
}
|
||||
|
||||
const expanded = new Set(prevState.expanded);
|
||||
if (event.direction === NavigationDirection.right) {
|
||||
// When stepping right, expand to ensure the selected node is visible
|
||||
expanded.add(Keys.keyToString({ resultIndex: key.resultIndex }));
|
||||
if (key.pathIndex !== undefined) {
|
||||
expanded.add(
|
||||
Keys.keyToString({
|
||||
resultIndex: key.resultIndex,
|
||||
pathIndex: key.pathIndex,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else if (event.direction === NavigationDirection.left) {
|
||||
// When stepping left, collapse immediately
|
||||
expanded.delete(Keys.keyToString(key));
|
||||
} else {
|
||||
// When stepping up or down, collapse the previous node
|
||||
if (prevState.selectedItem !== undefined) {
|
||||
expanded.delete(Keys.keyToString(prevState.selectedItem));
|
||||
}
|
||||
}
|
||||
this.scroller.scrollIntoViewOnNextUpdate();
|
||||
return {
|
||||
...prevState,
|
||||
expanded,
|
||||
selectedItem: key,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getNewSelection(
|
||||
const getNewSelection = (
|
||||
key: Keys.ResultKey | undefined,
|
||||
direction: NavigationDirection,
|
||||
): Keys.ResultKey {
|
||||
): Keys.ResultKey => {
|
||||
if (key === undefined) {
|
||||
return { resultIndex: 0 };
|
||||
}
|
||||
@@ -218,18 +106,113 @@ export class AlertTable extends React.Component<
|
||||
return key;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigationEvent = useCallback(
|
||||
(event: NavigateMsg) => {
|
||||
const key = getNewSelection(selectedItem, event.direction);
|
||||
const data = resultSet.interpretation.data;
|
||||
|
||||
// Check if the selected node actually exists (bounds check) and get its location if relevant
|
||||
let jumpLocation: Sarif.Location | undefined;
|
||||
if (key.pathNodeIndex !== undefined) {
|
||||
jumpLocation = Keys.getPathNode(data, key);
|
||||
if (jumpLocation === undefined) {
|
||||
return; // Result does not exist
|
||||
}
|
||||
} else if (key.pathIndex !== undefined) {
|
||||
if (Keys.getPath(data, key) === undefined) {
|
||||
return; // Path does not exist
|
||||
}
|
||||
jumpLocation = undefined; // When selecting a 'path', don't jump anywhere.
|
||||
} else {
|
||||
jumpLocation = Keys.getResult(data, key)?.locations?.[0];
|
||||
if (jumpLocation === undefined) {
|
||||
return; // Path step does not exist.
|
||||
}
|
||||
}
|
||||
if (jumpLocation !== undefined) {
|
||||
const parsedLocation = parseSarifLocation(
|
||||
jumpLocation,
|
||||
resultSet.interpretation.sourceLocationPrefix,
|
||||
);
|
||||
if (!isNoLocation(parsedLocation)) {
|
||||
jumpToLocation(parsedLocation, databaseUri);
|
||||
}
|
||||
}
|
||||
|
||||
const newExpanded = new Set(expanded);
|
||||
if (event.direction === NavigationDirection.right) {
|
||||
// When stepping right, expand to ensure the selected node is visible
|
||||
newExpanded.add(Keys.keyToString({ resultIndex: key.resultIndex }));
|
||||
if (key.pathIndex !== undefined) {
|
||||
newExpanded.add(
|
||||
Keys.keyToString({
|
||||
resultIndex: key.resultIndex,
|
||||
pathIndex: key.pathIndex,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else if (event.direction === NavigationDirection.left) {
|
||||
// When stepping left, collapse immediately
|
||||
newExpanded.delete(Keys.keyToString(key));
|
||||
} else {
|
||||
// When stepping up or down, collapse the previous node
|
||||
if (selectedItem !== undefined) {
|
||||
newExpanded.delete(Keys.keyToString(selectedItem));
|
||||
}
|
||||
}
|
||||
scroller.current?.scrollIntoViewOnNextUpdate();
|
||||
setExpanded(newExpanded);
|
||||
setSelectedItem(key);
|
||||
},
|
||||
[databaseUri, expanded, resultSet, selectedItem],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onNavigation.addListener(handleNavigationEvent);
|
||||
return () => {
|
||||
onNavigation.removeListener(handleNavigationEvent);
|
||||
};
|
||||
}, [handleNavigationEvent]);
|
||||
|
||||
const { numTruncatedResults, sourceLocationPrefix } =
|
||||
resultSet.interpretation;
|
||||
|
||||
const updateSelectionCallback = useCallback(
|
||||
(resultKey: Keys.PathNode | Keys.Result | undefined) => {
|
||||
setSelectedItem(resultKey);
|
||||
sendTelemetry("local-results-alert-table-path-selected");
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (!resultSet.interpretation.data.runs?.[0]?.results?.length) {
|
||||
return <AlertTableNoResults {...props} />;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.scroller.update();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.scroller.update();
|
||||
onNavigation.addListener(this.handleNavigationEvent);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
onNavigation.removeListener(this.handleNavigationEvent);
|
||||
}
|
||||
return (
|
||||
<table className={className}>
|
||||
<AlertTableHeader sortState={resultSet.interpretation.data.sortState} />
|
||||
<tbody>
|
||||
{resultSet.interpretation.data.runs[0].results.map(
|
||||
(result, resultIndex) => (
|
||||
<AlertTableResultRow
|
||||
key={resultIndex}
|
||||
result={result}
|
||||
resultIndex={resultIndex}
|
||||
expanded={expanded}
|
||||
selectedItem={selectedItem}
|
||||
databaseUri={databaseUri}
|
||||
sourceLocationPrefix={sourceLocationPrefix}
|
||||
updateSelectionCallback={updateSelectionCallback}
|
||||
toggleExpanded={toggle}
|
||||
scroller={scroller.current}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<AlertTableTruncatedMessage numTruncatedResults={numTruncatedResults} />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
updateSelectionCallback: (
|
||||
resultKey: Keys.PathNode | Keys.Result | undefined,
|
||||
) => void;
|
||||
scroller: ScrollIntoViewHelper;
|
||||
scroller?: ScrollIntoViewHelper;
|
||||
}
|
||||
|
||||
export function AlertTablePathNodeRow(props: Props) {
|
||||
@@ -51,7 +51,7 @@ export function AlertTablePathNodeRow(props: Props) {
|
||||
const zebraIndex = resultIndex + stepIndex;
|
||||
return (
|
||||
<tr
|
||||
ref={scroller.ref(isSelected)}
|
||||
ref={scroller?.ref(isSelected)}
|
||||
className={isSelected ? "vscode-codeql__selected-path-node" : undefined}
|
||||
>
|
||||
<td className="vscode-codeql__icon-cell">
|
||||
|
||||
@@ -19,7 +19,7 @@ interface Props {
|
||||
resultKey: Keys.PathNode | Keys.Result | undefined,
|
||||
) => void;
|
||||
toggleExpanded: (e: React.MouseEvent, keys: Keys.ResultKey[]) => void;
|
||||
scroller: ScrollIntoViewHelper;
|
||||
scroller?: ScrollIntoViewHelper;
|
||||
}
|
||||
|
||||
export function AlertTablePathRow(props: Props) {
|
||||
@@ -50,7 +50,7 @@ export function AlertTablePathRow(props: Props) {
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
ref={scroller.ref(isPathSpecificallySelected)}
|
||||
ref={scroller?.ref(isPathSpecificallySelected)}
|
||||
{...selectableZebraStripe(isPathSpecificallySelected, resultIndex)}
|
||||
>
|
||||
<td className="vscode-codeql__icon-cell">
|
||||
|
||||
@@ -21,7 +21,7 @@ interface Props {
|
||||
resultKey: Keys.PathNode | Keys.Result | undefined,
|
||||
) => void;
|
||||
toggleExpanded: (e: React.MouseEvent, keys: Keys.ResultKey[]) => void;
|
||||
scroller: ScrollIntoViewHelper;
|
||||
scroller?: ScrollIntoViewHelper;
|
||||
}
|
||||
|
||||
export function AlertTableResultRow(props: Props) {
|
||||
@@ -81,7 +81,7 @@ export function AlertTableResultRow(props: Props) {
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
ref={scroller.ref(resultRowIsSelected)}
|
||||
ref={scroller?.ref(resultRowIsSelected)}
|
||||
{...selectableZebraStripe(resultRowIsSelected, resultIndex)}
|
||||
>
|
||||
{result.codeFlows === undefined ? (
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Usage,
|
||||
ExternalApiUsage,
|
||||
CallClassification,
|
||||
} from "../../../src/data-extensions-editor/external-api-usage";
|
||||
import { ModeledMethodType } from "../../../src/data-extensions-editor/modeled-method";
|
||||
import { ResolvableLocationValue } from "../../../src/common/bqrs-cli-types";
|
||||
|
||||
export function createExternalApiUsage({
|
||||
library = "test",
|
||||
supported = true,
|
||||
supportedType = "none" as ModeledMethodType,
|
||||
usages = [],
|
||||
signature = "test",
|
||||
packageName = "test",
|
||||
typeName = "test",
|
||||
methodName = "test",
|
||||
methodParameters = "test",
|
||||
}: {
|
||||
library?: string;
|
||||
supported?: boolean;
|
||||
supportedType?: ModeledMethodType;
|
||||
usages?: Usage[];
|
||||
signature?: string;
|
||||
packageName?: string;
|
||||
typeName?: string;
|
||||
methodName?: string;
|
||||
methodParameters?: string;
|
||||
} = {}): ExternalApiUsage {
|
||||
return {
|
||||
library,
|
||||
supported,
|
||||
supportedType,
|
||||
usages,
|
||||
signature,
|
||||
packageName,
|
||||
typeName,
|
||||
methodName,
|
||||
methodParameters,
|
||||
};
|
||||
}
|
||||
|
||||
export function createUsage({
|
||||
classification = CallClassification.Unknown,
|
||||
label = "test",
|
||||
url = {} as ResolvableLocationValue,
|
||||
}: {
|
||||
classification?: CallClassification;
|
||||
label?: string;
|
||||
url?: ResolvableLocationValue;
|
||||
} = {}): Usage {
|
||||
return {
|
||||
classification,
|
||||
label,
|
||||
url,
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export function createMockSubmission(): VariantAnalysisSubmission {
|
||||
filePath: "query-file-path",
|
||||
language: QueryLanguage.Javascript,
|
||||
text: "query-text",
|
||||
kind: "table",
|
||||
pack: "base64-encoded-string",
|
||||
},
|
||||
databases: {
|
||||
|
||||
@@ -51,6 +51,7 @@ describe(processVariantAnalysis.name, () => {
|
||||
language: QueryLanguage.Javascript,
|
||||
name: "query-name",
|
||||
text: mockSubmission.query.text,
|
||||
kind: "table",
|
||||
},
|
||||
databases: {
|
||||
repositories: ["1", "2", "3"],
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
readQueryResults,
|
||||
runQuery,
|
||||
setUpPack,
|
||||
} from "../../../../src/data-extensions-editor/external-api-usage-query";
|
||||
} from "../../../../src/data-extensions-editor/external-api-usage-queries";
|
||||
import { createMockLogger } from "../../../__mocks__/loggerMock";
|
||||
import { DatabaseKind } from "../../../../src/databases/local-databases";
|
||||
import { dirSync, file } from "tmp-promise";
|
||||
|
||||
@@ -2,20 +2,28 @@ import { CodeQLCliServer } from "../../../../../src/codeql-cli/cli";
|
||||
import { ExternalApiUsage } from "../../../../../src/data-extensions-editor/external-api-usage";
|
||||
import { ModelDetailsDataProvider } from "../../../../../src/data-extensions-editor/model-details/model-details-data-provider";
|
||||
import { DatabaseItem } from "../../../../../src/databases/local-databases";
|
||||
import {
|
||||
createExternalApiUsage,
|
||||
createUsage,
|
||||
} from "../../../../factories/data-extension/external-api-factories";
|
||||
import { mockedObject } from "../../../utils/mocking.helpers";
|
||||
|
||||
describe("ModelDetailsDataProvider", () => {
|
||||
const mockCliServer = mockedObject<CodeQLCliServer>({});
|
||||
let dataProvider: ModelDetailsDataProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
dataProvider = new ModelDetailsDataProvider(mockCliServer);
|
||||
});
|
||||
|
||||
describe("setState", () => {
|
||||
const hideModeledApis: boolean = false;
|
||||
const hideModeledApis = false;
|
||||
const externalApiUsages: ExternalApiUsage[] = [];
|
||||
const dbItem = mockedObject<DatabaseItem>({
|
||||
getSourceLocationPrefix: () => "test",
|
||||
});
|
||||
|
||||
it("should not emit onDidChangeTreeData event when state has not changed", async () => {
|
||||
const dataProvider = new ModelDetailsDataProvider(mockCliServer);
|
||||
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
|
||||
|
||||
const onDidChangeTreeDataListener = jest.fn();
|
||||
@@ -29,7 +37,6 @@ describe("ModelDetailsDataProvider", () => {
|
||||
it("should emit onDidChangeTreeData event when externalApiUsages has changed", async () => {
|
||||
const externalApiUsages2: ExternalApiUsage[] = [];
|
||||
|
||||
const dataProvider = new ModelDetailsDataProvider(mockCliServer);
|
||||
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
|
||||
|
||||
const onDidChangeTreeDataListener = jest.fn();
|
||||
@@ -45,7 +52,6 @@ describe("ModelDetailsDataProvider", () => {
|
||||
getSourceLocationPrefix: () => "test",
|
||||
});
|
||||
|
||||
const dataProvider = new ModelDetailsDataProvider(mockCliServer);
|
||||
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
|
||||
|
||||
const onDidChangeTreeDataListener = jest.fn();
|
||||
@@ -57,7 +63,6 @@ describe("ModelDetailsDataProvider", () => {
|
||||
});
|
||||
|
||||
it("should emit onDidChangeTreeData event when hideModeledApis has changed", async () => {
|
||||
const dataProvider = new ModelDetailsDataProvider(mockCliServer);
|
||||
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
|
||||
|
||||
const onDidChangeTreeDataListener = jest.fn();
|
||||
@@ -74,7 +79,6 @@ describe("ModelDetailsDataProvider", () => {
|
||||
});
|
||||
const externalApiUsages2: ExternalApiUsage[] = [];
|
||||
|
||||
const dataProvider = new ModelDetailsDataProvider(mockCliServer);
|
||||
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
|
||||
|
||||
const onDidChangeTreeDataListener = jest.fn();
|
||||
@@ -89,4 +93,45 @@ describe("ModelDetailsDataProvider", () => {
|
||||
expect(onDidChangeTreeDataListener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getChildren", () => {
|
||||
const supportedExternalApiUsage = createExternalApiUsage({
|
||||
supported: true,
|
||||
});
|
||||
|
||||
const unsupportedExternalApiUsage = createExternalApiUsage({
|
||||
supported: false,
|
||||
});
|
||||
|
||||
const externalApiUsages: ExternalApiUsage[] = [
|
||||
supportedExternalApiUsage,
|
||||
unsupportedExternalApiUsage,
|
||||
];
|
||||
const dbItem = mockedObject<DatabaseItem>({
|
||||
getSourceLocationPrefix: () => "test",
|
||||
});
|
||||
|
||||
const usage = createUsage({});
|
||||
|
||||
it("should return [] if item is a usage", async () => {
|
||||
expect(dataProvider.getChildren(usage)).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return usages if item is external api usage", async () => {
|
||||
const externalApiUsage = createExternalApiUsage({ usages: [usage] });
|
||||
expect(dataProvider.getChildren(externalApiUsage)).toEqual([usage]);
|
||||
});
|
||||
|
||||
it("should show all externalApiUsages if hideModeledApis is false and looking at the root", async () => {
|
||||
const hideModeledApis = false;
|
||||
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
|
||||
expect(dataProvider.getChildren().length).toEqual(2);
|
||||
});
|
||||
|
||||
it("should filter externalApiUsages if hideModeledApis is true and looking at the root", async () => {
|
||||
const hideModeledApis = true;
|
||||
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
|
||||
expect(dataProvider.getChildren().length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { window, TreeView } from "vscode";
|
||||
import { CodeQLCliServer } from "../../../../../src/codeql-cli/cli";
|
||||
import { ExternalApiUsage } from "../../../../../src/data-extensions-editor/external-api-usage";
|
||||
import { ModelDetailsPanel } from "../../../../../src/data-extensions-editor/model-details/model-details-panel";
|
||||
import { DatabaseItem } from "../../../../../src/databases/local-databases";
|
||||
import { mockedObject } from "../../../utils/mocking.helpers";
|
||||
import {
|
||||
createExternalApiUsage,
|
||||
createUsage,
|
||||
} from "../../../../factories/data-extension/external-api-factories";
|
||||
|
||||
describe("ModelDetailsPanel", () => {
|
||||
const mockCliServer = mockedObject<CodeQLCliServer>({});
|
||||
const dbItem = mockedObject<DatabaseItem>({
|
||||
getSourceLocationPrefix: () => "test",
|
||||
});
|
||||
|
||||
describe("setState", () => {
|
||||
const hideModeledApis = false;
|
||||
const externalApiUsages: ExternalApiUsage[] = [createExternalApiUsage()];
|
||||
|
||||
it("should update the tree view with the correct batch number", async () => {
|
||||
const mockTreeView = {
|
||||
badge: undefined,
|
||||
} as TreeView<unknown>;
|
||||
jest.spyOn(window, "createTreeView").mockReturnValue(mockTreeView);
|
||||
|
||||
const panel = new ModelDetailsPanel(mockCliServer);
|
||||
await panel.setState(externalApiUsages, dbItem, hideModeledApis);
|
||||
|
||||
expect(mockTreeView.badge?.value).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("revealItem", () => {
|
||||
let mockTreeView: TreeView<unknown>;
|
||||
|
||||
const hideModeledApis: boolean = false;
|
||||
const usage = createUsage();
|
||||
|
||||
beforeEach(() => {
|
||||
mockTreeView = {
|
||||
reveal: jest.fn(),
|
||||
} as unknown as TreeView<unknown>;
|
||||
jest.spyOn(window, "createTreeView").mockReturnValue(mockTreeView);
|
||||
});
|
||||
|
||||
it("should reveal the correct item in the tree view", async () => {
|
||||
const externalApiUsages = [
|
||||
createExternalApiUsage({
|
||||
usages: [usage],
|
||||
}),
|
||||
];
|
||||
|
||||
const panel = new ModelDetailsPanel(mockCliServer);
|
||||
await panel.setState(externalApiUsages, dbItem, hideModeledApis);
|
||||
|
||||
await panel.revealItem(usage);
|
||||
|
||||
expect(mockTreeView.reveal).toHaveBeenCalledWith(usage);
|
||||
});
|
||||
|
||||
it("should do nothing if usage cannot be found", async () => {
|
||||
const externalApiUsages = [createExternalApiUsage({})];
|
||||
const panel = new ModelDetailsPanel(mockCliServer);
|
||||
await panel.setState(externalApiUsages, dbItem, hideModeledApis);
|
||||
|
||||
await panel.revealItem(usage);
|
||||
|
||||
expect(mockTreeView.reveal).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user