wip: show sarif diffs in compare view
This commit is contained in:
Родитель
d28cc6e3f2
Коммит
895d56637e
|
@ -353,14 +353,17 @@ export interface SetComparisonsMessage {
|
|||
time: string;
|
||||
};
|
||||
};
|
||||
readonly columns: readonly Column[];
|
||||
readonly commonResultSetNames: string[];
|
||||
readonly currentResultSetName: string;
|
||||
readonly rows: QueryCompareResult | undefined;
|
||||
readonly result: QueryCompareResult | undefined;
|
||||
readonly message: string | undefined;
|
||||
readonly databaseUri: string;
|
||||
}
|
||||
|
||||
export type QueryCompareResult =
|
||||
| RawQueryCompareResult
|
||||
| InterpretedQueryCompareResult;
|
||||
|
||||
/**
|
||||
* from is the set of rows that have changes in the "from" query.
|
||||
* to is the set of rows that have changes in the "to" query.
|
||||
|
@ -370,11 +373,29 @@ export interface SetComparisonsMessage {
|
|||
* If an array element is null, that means that the element was removed
|
||||
* (or added) in the comparison.
|
||||
*/
|
||||
export type QueryCompareResult = {
|
||||
export type RawQueryCompareResult = {
|
||||
type: "raw";
|
||||
readonly columns: readonly Column[];
|
||||
from: ResultRow[];
|
||||
to: ResultRow[];
|
||||
};
|
||||
|
||||
/**
|
||||
* from is the set of rows that have changes in the "from" query.
|
||||
* to is the set of rows that have changes in the "to" query.
|
||||
* They are in the same order, so element 1 in "from" corresponds to
|
||||
* element 1 in "to".
|
||||
*
|
||||
* If an array element is null, that means that the element was removed
|
||||
* (or added) in the comparison.
|
||||
*/
|
||||
export type InterpretedQueryCompareResult = {
|
||||
type: "interpreted";
|
||||
sourceLocationPrefix: string;
|
||||
from: sarif.Result[];
|
||||
to: sarif.Result[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract the name of the default result. Prefer returning
|
||||
* 'alerts', or '#select'. Otherwise return the first in the list.
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import { ViewColumn } from "vscode";
|
||||
import { Uri, ViewColumn } from "vscode";
|
||||
|
||||
import * as sarif from "sarif";
|
||||
import {
|
||||
FromCompareViewMessage,
|
||||
ToCompareViewMessage,
|
||||
RawQueryCompareResult,
|
||||
ALERTS_TABLE_NAME,
|
||||
QueryCompareResult,
|
||||
InterpretedQueryCompareResult,
|
||||
} from "../common/interface-types";
|
||||
import { Logger, showAndLogExceptionWithTelemetry } from "../common/logging";
|
||||
import { extLogger } from "../common/logging/vscode";
|
||||
|
@ -26,12 +30,31 @@ import {
|
|||
import { telemetryListener } from "../common/vscode/telemetry";
|
||||
import { redactableError } from "../common/errors";
|
||||
import { App } from "../common/app";
|
||||
import { pathExists } from "fs-extra";
|
||||
import { sarifParser } from "../common/sarif-parser";
|
||||
import sarifDiff from "./sarif-diff";
|
||||
|
||||
interface ComparePair {
|
||||
from: CompletedLocalQueryInfo;
|
||||
to: CompletedLocalQueryInfo;
|
||||
}
|
||||
|
||||
type CommonResultSet =
|
||||
| {
|
||||
type: "raw";
|
||||
commonResultSetNames: string[];
|
||||
currentResultSetName: string;
|
||||
fromSchemas: BQRSInfo;
|
||||
toSchemas: BQRSInfo;
|
||||
fromResultSetName: string;
|
||||
toResultSetName: string;
|
||||
}
|
||||
| {
|
||||
type: "interpreted";
|
||||
commonResultSetNames: string[];
|
||||
currentResultSetName: string;
|
||||
};
|
||||
|
||||
export class CompareView extends AbstractWebview<
|
||||
ToCompareViewMessage,
|
||||
FromCompareViewMessage
|
||||
|
@ -61,19 +84,35 @@ export class CompareView extends AbstractWebview<
|
|||
panel.reveal(undefined, true);
|
||||
|
||||
await this.waitForPanelLoaded();
|
||||
const [
|
||||
commonResultSetNames,
|
||||
currentResultSetName,
|
||||
fromResultSet,
|
||||
toResultSet,
|
||||
] = await this.findCommonResultSetNames(from, to, selectedResultSetName);
|
||||
const commonResultSet = await this.findCommonResultSet(
|
||||
from,
|
||||
to,
|
||||
selectedResultSetName,
|
||||
);
|
||||
const { commonResultSetNames, currentResultSetName } = commonResultSet;
|
||||
if (currentResultSetName) {
|
||||
let rows: QueryCompareResult | undefined;
|
||||
let result: QueryCompareResult | undefined;
|
||||
let message: string | undefined;
|
||||
try {
|
||||
rows = this.compareResults(fromResultSet, toResultSet);
|
||||
} catch (e) {
|
||||
message = getErrorMessage(e);
|
||||
|
||||
if (commonResultSet.type === "interpreted") {
|
||||
try {
|
||||
result = await this.compareInterpretedResults(from, to);
|
||||
} catch (e) {
|
||||
message = getErrorMessage(e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
result = await this.compareRawResults(
|
||||
from,
|
||||
to,
|
||||
commonResultSet.fromSchemas,
|
||||
commonResultSet.toSchemas,
|
||||
commonResultSet.fromResultSetName,
|
||||
commonResultSet.toResultSetName,
|
||||
);
|
||||
} catch (e) {
|
||||
message = getErrorMessage(e);
|
||||
}
|
||||
}
|
||||
|
||||
await this.postMessage({
|
||||
|
@ -93,10 +132,9 @@ export class CompareView extends AbstractWebview<
|
|||
time: to.startTime,
|
||||
},
|
||||
},
|
||||
columns: fromResultSet.schema.columns,
|
||||
commonResultSetNames,
|
||||
currentResultSetName,
|
||||
rows,
|
||||
result,
|
||||
message,
|
||||
databaseUri: to.initialInfo.databaseInfo.databaseUri,
|
||||
});
|
||||
|
@ -165,11 +203,11 @@ export class CompareView extends AbstractWebview<
|
|||
}
|
||||
}
|
||||
|
||||
private async findCommonResultSetNames(
|
||||
private async findCommonResultSet(
|
||||
from: CompletedLocalQueryInfo,
|
||||
to: CompletedLocalQueryInfo,
|
||||
selectedResultSetName: string | undefined,
|
||||
): Promise<[string[], string, RawResultSet, RawResultSet]> {
|
||||
): Promise<CommonResultSet> {
|
||||
const fromSchemas = await this.cliServer.bqrsInfo(
|
||||
from.completedQuery.query.resultsPaths.resultsPath,
|
||||
);
|
||||
|
@ -180,6 +218,22 @@ export class CompareView extends AbstractWebview<
|
|||
(schema) => schema.name,
|
||||
);
|
||||
const toSchemaNames = toSchemas["result-sets"].map((schema) => schema.name);
|
||||
|
||||
if (
|
||||
await pathExists(
|
||||
from.completedQuery.query.resultsPaths.interpretedResultsPath,
|
||||
)
|
||||
) {
|
||||
fromSchemaNames.push(ALERTS_TABLE_NAME);
|
||||
}
|
||||
if (
|
||||
await pathExists(
|
||||
to.completedQuery.query.resultsPaths.interpretedResultsPath,
|
||||
)
|
||||
) {
|
||||
toSchemaNames.push(ALERTS_TABLE_NAME);
|
||||
}
|
||||
|
||||
const commonResultSetNames = fromSchemaNames.filter((name) =>
|
||||
toSchemaNames.includes(name),
|
||||
);
|
||||
|
@ -203,23 +257,28 @@ export class CompareView extends AbstractWebview<
|
|||
|
||||
const currentResultSetName =
|
||||
selectedResultSetName || commonResultSetNames[0];
|
||||
const fromResultSet = await this.getResultSet(
|
||||
fromSchemas,
|
||||
currentResultSetName || defaultFromResultSetName!,
|
||||
from.completedQuery.query.resultsPaths.resultsPath,
|
||||
);
|
||||
const toResultSet = await this.getResultSet(
|
||||
toSchemas,
|
||||
currentResultSetName || defaultToResultSetName!,
|
||||
to.completedQuery.query.resultsPaths.resultsPath,
|
||||
);
|
||||
return [
|
||||
commonResultSetNames,
|
||||
|
||||
const displayCurrentResultSetName =
|
||||
currentResultSetName ||
|
||||
`${defaultFromResultSetName} <-> ${defaultToResultSetName}`,
|
||||
fromResultSet,
|
||||
toResultSet,
|
||||
];
|
||||
`${defaultFromResultSetName} <-> ${defaultToResultSetName}`;
|
||||
|
||||
if (currentResultSetName === ALERTS_TABLE_NAME) {
|
||||
return {
|
||||
type: "interpreted",
|
||||
commonResultSetNames,
|
||||
currentResultSetName: displayCurrentResultSetName,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "raw",
|
||||
commonResultSetNames,
|
||||
currentResultSetName: displayCurrentResultSetName,
|
||||
fromSchemas,
|
||||
toSchemas,
|
||||
fromResultSetName: currentResultSetName || defaultFromResultSetName!,
|
||||
toResultSetName: currentResultSetName || defaultToResultSetName!,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async changeTable(newResultSetName: string) {
|
||||
|
@ -248,14 +307,92 @@ export class CompareView extends AbstractWebview<
|
|||
return transformBqrsResultSet(schema, chunk);
|
||||
}
|
||||
|
||||
private compareResults(
|
||||
fromResults: RawResultSet,
|
||||
toResults: RawResultSet,
|
||||
): QueryCompareResult {
|
||||
private async getInterpretedResults(
|
||||
interpretedResultsPath: string,
|
||||
): Promise<sarif.Log | undefined> {
|
||||
if (!(await pathExists(interpretedResultsPath))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await sarifParser(interpretedResultsPath);
|
||||
}
|
||||
|
||||
private async compareRawResults(
|
||||
fromQuery: CompletedLocalQueryInfo,
|
||||
toQuery: CompletedLocalQueryInfo,
|
||||
fromSchemas: BQRSInfo,
|
||||
toSchemas: BQRSInfo,
|
||||
fromResultSetName: string,
|
||||
toResultSetName: string,
|
||||
): Promise<RawQueryCompareResult> {
|
||||
const fromResults = await this.getResultSet(
|
||||
fromSchemas,
|
||||
fromResultSetName,
|
||||
fromQuery.completedQuery.query.resultsPaths.resultsPath,
|
||||
);
|
||||
|
||||
const toResults = await this.getResultSet(
|
||||
toSchemas,
|
||||
toResultSetName,
|
||||
toQuery.completedQuery.query.resultsPaths.resultsPath,
|
||||
);
|
||||
|
||||
// Only compare columns that have the same name
|
||||
return resultsDiff(fromResults, toResults);
|
||||
}
|
||||
|
||||
private async compareInterpretedResults(
|
||||
fromQuery: CompletedLocalQueryInfo,
|
||||
toQuery: CompletedLocalQueryInfo,
|
||||
): Promise<InterpretedQueryCompareResult> {
|
||||
const fromResultSet = await this.getInterpretedResults(
|
||||
fromQuery.completedQuery.query.resultsPaths.interpretedResultsPath,
|
||||
);
|
||||
|
||||
const toResultSet = await this.getInterpretedResults(
|
||||
toQuery.completedQuery.query.resultsPaths.interpretedResultsPath,
|
||||
);
|
||||
|
||||
if (!fromResultSet || !toResultSet) {
|
||||
throw new Error(
|
||||
"Could not find interpreted results for one or both queries.",
|
||||
);
|
||||
}
|
||||
|
||||
const database = this.databaseManager.findDatabaseItem(
|
||||
Uri.parse(toQuery.initialInfo.databaseInfo.databaseUri),
|
||||
);
|
||||
if (!database) {
|
||||
throw new Error(
|
||||
"Could not find database the queries. Please check that the database still exists.",
|
||||
);
|
||||
}
|
||||
|
||||
const sourceLocationPrefix = await database.getSourceLocationPrefix(
|
||||
this.cliServer,
|
||||
);
|
||||
|
||||
const fromResults = fromResultSet.runs[0].results;
|
||||
const toResults = toResultSet.runs[0].results;
|
||||
|
||||
if (!fromResults) {
|
||||
throw new Error("No results found in the 'from' query.");
|
||||
}
|
||||
|
||||
if (!toResults) {
|
||||
throw new Error("No results found in the 'to' query.");
|
||||
}
|
||||
|
||||
const { from, to } = sarifDiff(fromResults, toResults);
|
||||
|
||||
return {
|
||||
type: "interpreted",
|
||||
sourceLocationPrefix,
|
||||
from,
|
||||
to,
|
||||
};
|
||||
}
|
||||
|
||||
private async openQuery(kind: "from" | "to") {
|
||||
const toOpen =
|
||||
kind === "from" ? this.comparePair?.from : this.comparePair?.to;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { RawResultSet } from "../common/bqrs-cli-types";
|
||||
import { QueryCompareResult } from "../common/interface-types";
|
||||
import { RawQueryCompareResult } from "../common/interface-types";
|
||||
|
||||
/**
|
||||
* Compare the rows of two queries. Use deep equality to determine if
|
||||
|
@ -22,7 +22,7 @@ import { QueryCompareResult } from "../common/interface-types";
|
|||
export default function resultsDiff(
|
||||
fromResults: RawResultSet,
|
||||
toResults: RawResultSet,
|
||||
): QueryCompareResult {
|
||||
): RawQueryCompareResult {
|
||||
if (fromResults.schema.columns.length !== toResults.schema.columns.length) {
|
||||
throw new Error("CodeQL Compare: Columns do not match.");
|
||||
}
|
||||
|
@ -35,7 +35,9 @@ export default function resultsDiff(
|
|||
throw new Error("CodeQL Compare: Target query has no results.");
|
||||
}
|
||||
|
||||
const results = {
|
||||
const results: RawQueryCompareResult = {
|
||||
type: "raw",
|
||||
columns: fromResults.schema.columns,
|
||||
from: arrayDiff(fromResults.rows, toResults.rows),
|
||||
to: arrayDiff(toResults.rows, fromResults.rows),
|
||||
};
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import * as sarif from "sarif";
|
||||
|
||||
/**
|
||||
* Compare the alerts of two queries. Use deep equality to determine if
|
||||
* results have been added or removed across two invocations of a query.
|
||||
*
|
||||
* Assumptions:
|
||||
*
|
||||
* 1. Queries have the same sort order
|
||||
* 2. Results are not changed or re-ordered, they are only added or removed
|
||||
*
|
||||
* @param fromResults the source query
|
||||
* @param toResults the target query
|
||||
*
|
||||
* @throws Error when:
|
||||
* 1. If either query is empty
|
||||
* 2. If the queries are 100% disjoint
|
||||
*/
|
||||
export default function sarifDiff(
|
||||
fromResults: sarif.Result[],
|
||||
toResults: sarif.Result[],
|
||||
) {
|
||||
if (!fromResults.length) {
|
||||
throw new Error("CodeQL Compare: Source query has no results.");
|
||||
}
|
||||
|
||||
if (!toResults.length) {
|
||||
throw new Error("CodeQL Compare: Target query has no results.");
|
||||
}
|
||||
|
||||
const results = {
|
||||
from: arrayDiff(fromResults, toResults),
|
||||
to: arrayDiff(toResults, fromResults),
|
||||
};
|
||||
|
||||
if (
|
||||
fromResults.length === results.from.length &&
|
||||
toResults.length === results.to.length
|
||||
) {
|
||||
throw new Error("CodeQL Compare: No overlap between the selected queries.");
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function arrayDiff<T>(source: readonly T[], toRemove: readonly T[]): T[] {
|
||||
// Stringify the object so that we can compare hashes in the set
|
||||
const rest = new Set(toRemove.map((item) => JSON.stringify(item)));
|
||||
return source.filter((element) => !rest.has(JSON.stringify(element)));
|
||||
}
|
|
@ -14,8 +14,7 @@ import "../results/resultsView.css";
|
|||
const emptyComparison: SetComparisonsMessage = {
|
||||
t: "setComparisons",
|
||||
stats: {},
|
||||
rows: undefined,
|
||||
columns: [],
|
||||
result: undefined,
|
||||
commonResultSetNames: [],
|
||||
currentResultSetName: "",
|
||||
databaseUri: "",
|
||||
|
@ -28,8 +27,8 @@ export function Compare(_: Record<string, never>): JSX.Element {
|
|||
|
||||
const message = comparison.message || "Empty comparison";
|
||||
const hasRows =
|
||||
comparison.rows &&
|
||||
(comparison.rows.to.length || comparison.rows.from.length);
|
||||
comparison.result &&
|
||||
(comparison.result.to.length || comparison.result.from.length);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { SetComparisonsMessage } from "../../common/interface-types";
|
||||
import RawTableHeader from "../results/RawTableHeader";
|
||||
import { className } from "../results/result-table-utils";
|
||||
import { ResultRow } from "../../common/bqrs-cli-types";
|
||||
import RawTableRow from "../results/RawTableRow";
|
||||
import { vscode } from "../vscode-api";
|
||||
import { sendTelemetry } from "../common/telemetry";
|
||||
import TextButton from "../common/TextButton";
|
||||
import { styled } from "styled-components";
|
||||
import { RawCompareResultTable } from "./RawCompareResultTable";
|
||||
import { InterpretedCompareResultTable } from "./InterpretedCompareResultTable";
|
||||
|
||||
interface Props {
|
||||
comparison: SetComparisonsMessage;
|
||||
|
@ -22,7 +20,7 @@ const OpenButton = styled(TextButton)`
|
|||
|
||||
export default function CompareTable(props: Props) {
|
||||
const comparison = props.comparison;
|
||||
const rows = props.comparison.rows!;
|
||||
const result = props.comparison.result!;
|
||||
|
||||
async function openQuery(kind: "from" | "to") {
|
||||
vscode.postMessage({
|
||||
|
@ -31,24 +29,6 @@ export default function CompareTable(props: Props) {
|
|||
});
|
||||
}
|
||||
|
||||
function createRows(rows: ResultRow[], databaseUri: string) {
|
||||
return (
|
||||
<tbody>
|
||||
{rows.map((row, rowIndex) => (
|
||||
<RawTableRow
|
||||
key={rowIndex}
|
||||
rowIndex={rowIndex}
|
||||
row={row}
|
||||
databaseUri={databaseUri}
|
||||
onSelected={() => {
|
||||
sendTelemetry("comapre-view-result-clicked");
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="vscode-codeql__compare-body">
|
||||
<thead>
|
||||
|
@ -69,30 +49,50 @@ export default function CompareTable(props: Props) {
|
|||
<td>{comparison.stats.toQuery?.time}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{rows.from.length} rows removed</th>
|
||||
<th>{rows.to.length} rows added</th>
|
||||
<th>{result.from.length} rows removed</th>
|
||||
<th>{result.to.length} rows added</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<table className={className}>
|
||||
<RawTableHeader
|
||||
columns={comparison.columns}
|
||||
{result.type === "raw" && (
|
||||
<RawCompareResultTable
|
||||
rows={result.from}
|
||||
columns={result.columns}
|
||||
schemaName={comparison.currentResultSetName}
|
||||
preventSort={true}
|
||||
databaseUri={comparison.databaseUri}
|
||||
className={className}
|
||||
/>
|
||||
{createRows(rows.from, comparison.databaseUri)}
|
||||
</table>
|
||||
)}
|
||||
{result.type === "interpreted" && (
|
||||
<InterpretedCompareResultTable
|
||||
results={result.from}
|
||||
databaseUri={comparison.databaseUri}
|
||||
sourceLocationPrefix={result.sourceLocationPrefix}
|
||||
className={className}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<table className={className}>
|
||||
<RawTableHeader
|
||||
columns={comparison.columns}
|
||||
schemaName={comparison.currentResultSetName}
|
||||
preventSort={true}
|
||||
/>
|
||||
{createRows(rows.to, comparison.databaseUri)}
|
||||
{result.type === "raw" && (
|
||||
<RawCompareResultTable
|
||||
rows={result.to}
|
||||
columns={result.columns}
|
||||
schemaName={comparison.currentResultSetName}
|
||||
databaseUri={comparison.databaseUri}
|
||||
className={className}
|
||||
/>
|
||||
)}
|
||||
{result.type === "interpreted" && (
|
||||
<InterpretedCompareResultTable
|
||||
results={result.to}
|
||||
databaseUri={comparison.databaseUri}
|
||||
sourceLocationPrefix={result.sourceLocationPrefix}
|
||||
className={className}
|
||||
/>
|
||||
)}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import * as React from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import * as sarif from "sarif";
|
||||
import { AlertTableResultRow } from "../results/AlertTableResultRow";
|
||||
import * as Keys from "../results/result-keys";
|
||||
import { useScrollIntoView } from "../results/useScrollIntoView";
|
||||
import { sendTelemetry } from "../common/telemetry";
|
||||
|
||||
type Props = {
|
||||
results: sarif.Result[];
|
||||
databaseUri: string;
|
||||
sourceLocationPrefix: string;
|
||||
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const InterpretedCompareResultTable = ({
|
||||
results,
|
||||
databaseUri,
|
||||
sourceLocationPrefix,
|
||||
className,
|
||||
}: Props) => {
|
||||
const [expanded, setExpanded] = useState<Set<string>>(new Set<string>());
|
||||
const [selectedItem, setSelectedItem] = useState<Keys.ResultKey | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const selectedItemRef = useRef<any>();
|
||||
useScrollIntoView(selectedItem, selectedItemRef);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const toggle = useCallback((e: React.MouseEvent, keys: Keys.ResultKey[]) => {
|
||||
const keyStrings = keys.map(Keys.keyToString);
|
||||
setExpanded((previousExpanded) => {
|
||||
const expanded = new Set(previousExpanded);
|
||||
if (previousExpanded.has(keyStrings[0])) {
|
||||
expanded.delete(keyStrings[0]);
|
||||
} else {
|
||||
for (const str of keyStrings) {
|
||||
expanded.add(str);
|
||||
}
|
||||
}
|
||||
if (expanded) {
|
||||
sendTelemetry("local-results-alert-table-path-expanded");
|
||||
}
|
||||
return expanded;
|
||||
});
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}, []);
|
||||
|
||||
const updateSelectionCallback = useCallback(
|
||||
(resultKey: Keys.PathNode | Keys.Result | undefined) => {
|
||||
setSelectedItem(resultKey);
|
||||
sendTelemetry("local-results-alert-table-path-selected");
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<table className={className}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan={2}></th>
|
||||
<th className={`vscode-codeql__alert-message-cell`} colSpan={3}>
|
||||
Message
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{results.map((result, resultIndex) => (
|
||||
<AlertTableResultRow
|
||||
key={resultIndex}
|
||||
result={result}
|
||||
resultIndex={resultIndex}
|
||||
expanded={expanded}
|
||||
selectedItem={selectedItem}
|
||||
selectedItemRef={selectedItemRef}
|
||||
databaseUri={databaseUri}
|
||||
sourceLocationPrefix={sourceLocationPrefix}
|
||||
updateSelectionCallback={updateSelectionCallback}
|
||||
toggleExpanded={toggle}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
import * as React from "react";
|
||||
import { Column, ResultRow } from "../../common/bqrs-cli-types";
|
||||
import RawTableHeader from "../results/RawTableHeader";
|
||||
import RawTableRow from "../results/RawTableRow";
|
||||
import { sendTelemetry } from "../common/telemetry";
|
||||
|
||||
type Props = {
|
||||
rows: ResultRow[];
|
||||
columns: readonly Column[];
|
||||
schemaName: string;
|
||||
databaseUri: string;
|
||||
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const RawCompareResultTable = ({
|
||||
rows,
|
||||
columns,
|
||||
schemaName,
|
||||
databaseUri,
|
||||
className,
|
||||
}: Props) => {
|
||||
return (
|
||||
<table className={className}>
|
||||
<RawTableHeader
|
||||
columns={columns}
|
||||
schemaName={schemaName}
|
||||
preventSort={true}
|
||||
/>
|
||||
<tbody>
|
||||
{rows.map((row, rowIndex) => (
|
||||
<RawTableRow
|
||||
key={rowIndex}
|
||||
rowIndex={rowIndex}
|
||||
row={row}
|
||||
databaseUri={databaseUri}
|
||||
onSelected={() => {
|
||||
sendTelemetry("comapre-view-result-clicked");
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
Загрузка…
Ссылка в новой задаче