Merge remote-tracking branch 'origin/main' into koesie10/data-extension-editor-generate-flow-model

This commit is contained in:
Koen Vlaswinkel
2023-04-06 12:43:51 +02:00
12 changed files with 527 additions and 114 deletions

View File

@@ -3,6 +3,7 @@ import {
ExtensionContext,
Uri,
ViewColumn,
window,
workspace,
} from "vscode";
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
@@ -28,6 +29,8 @@ import { asError, assertNever, getErrorMessage } from "../pure/helpers-pure";
import { generateFlowModel } from "./generate-flow-model";
import { promptImportGithubDatabase } from "../databaseFetcher";
import { App } from "../common/app";
import { ResolvableLocationValue } from "../pure/bqrs-cli-types";
import { showResolvableLocation } from "../interface-utils";
import { decodeBqrsToExternalApiUsages } from "./bqrs";
import { redactableError } from "../pure/errors";
import { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
@@ -78,6 +81,10 @@ export class DataExtensionsEditorView extends AbstractWebview<
case "viewLoaded":
await this.onWebViewLoaded();
break;
case "jumpToUsage":
await this.jumpToUsage(msg.location);
break;
case "saveModeledMethods":
await this.saveModeledMethods(
@@ -105,6 +112,26 @@ export class DataExtensionsEditorView extends AbstractWebview<
]);
}
protected async jumpToUsage(
location: ResolvableLocationValue,
): Promise<void> {
try {
await showResolvableLocation(location, this.databaseItem);
} catch (e) {
if (e instanceof Error) {
if (e.message.match(/File not found/)) {
void window.showErrorMessage(
"Original file of this result is not in the database's source archive.",
);
} else {
void extLogger.log(`Unable to handleMsgFromView: ${e.message}`);
}
} else {
void extLogger.log(`Unable to handleMsgFromView: ${e}`);
}
}
}
protected async saveModeledMethods(
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,

View File

@@ -172,10 +172,6 @@ async function runQuery(
*/
export class QueryInProgress {
public queryEvalInfo: QueryEvaluationInfo;
/**
* Note that in the {@link readQueryHistoryFromFile} method, we create a QueryEvaluationInfo instance
* by explicitly setting the prototype in order to avoid calling this constructor.
*/
constructor(
readonly querySaveDir: string,
readonly dbItemPath: string,

View File

@@ -506,6 +506,16 @@ export interface AddModeledMethodsMessage {
overrideNone?: boolean;
}
export interface JumpToUsageMessage {
t: "jumpToUsage";
location: ResolvableLocationValue;
}
export interface SetExistingModeledMethods {
t: "setExistingModeledMethods";
existingModeledMethods: Record<string, ModeledMethod>;
}
export interface SaveModeledMethods {
t: "saveModeledMethods";
externalApiUsages: ExternalApiUsage[];
@@ -523,5 +533,6 @@ export type ToDataExtensionsEditorMessage =
export type FromDataExtensionsEditorMessage =
| ViewLoadedMsg
| JumpToUsageMessage
| SaveModeledMethods
| GenerateExternalApiMessage;

View File

@@ -0,0 +1,105 @@
import {
LocalQueryInfo,
CompletedQueryInfo,
InitialQueryInfo,
} from "../../query-results";
import { QueryEvaluationInfo } from "../../run-queries-shared";
import { QueryHistoryInfo } from "../query-history-info";
import { VariantAnalysisHistoryItem } from "../variant-analysis-history-item";
import {
CompletedQueryInfoData,
QueryEvaluationInfoData,
InitialQueryInfoData,
LocalQueryDataItem,
} from "./local-query-data-item";
import { QueryHistoryDataItem } from "./query-history-data";
// Maps Query History Data Models to Domain Models
export function mapQueryHistoryToDomainModels(
queries: QueryHistoryDataItem[],
): QueryHistoryInfo[] {
return queries.map((d) => {
if (d.t === "variant-analysis") {
const query: VariantAnalysisHistoryItem = d;
return query;
} else if (d.t === "local") {
return mapLocalQueryDataItemToDomainModel(d);
}
throw Error(
`Unexpected or corrupted query history file. Unknown query history item: ${JSON.stringify(
d,
)}`,
);
});
}
function mapLocalQueryDataItemToDomainModel(
localQuery: LocalQueryDataItem,
): LocalQueryInfo {
return new LocalQueryInfo(
mapInitialQueryInfoDataToDomainModel(localQuery.initialInfo),
undefined,
localQuery.failureReason,
localQuery.completedQuery &&
mapCompletedQueryInfoDataToDomainModel(localQuery.completedQuery),
localQuery.evalLogLocation,
localQuery.evalLogSummaryLocation,
localQuery.jsonEvalLogSummaryLocation,
localQuery.evalLogSummarySymbolsLocation,
);
}
function mapCompletedQueryInfoDataToDomainModel(
completedQuery: CompletedQueryInfoData,
): CompletedQueryInfo {
return new CompletedQueryInfo(
mapQueryEvaluationInfoDataToDomainModel(completedQuery.query),
{
runId: completedQuery.result.runId,
queryId: completedQuery.result.queryId,
resultType: completedQuery.result.resultType,
evaluationTime: completedQuery.result.evaluationTime,
message: completedQuery.result.message,
logFileLocation: completedQuery.result.logFileLocation,
},
completedQuery.logFileLocation,
completedQuery.successful ?? completedQuery.sucessful,
completedQuery.message,
completedQuery.interpretedResultsSortState,
completedQuery.resultCount,
completedQuery.sortedResultsInfo,
);
}
function mapInitialQueryInfoDataToDomainModel(
initialInfo: InitialQueryInfoData,
): InitialQueryInfo {
return {
userSpecifiedLabel: initialInfo.userSpecifiedLabel,
queryText: initialInfo.queryText,
isQuickQuery: initialInfo.isQuickQuery,
isQuickEval: initialInfo.isQuickEval,
quickEvalPosition: initialInfo.quickEvalPosition,
queryPath: initialInfo.queryPath,
databaseInfo: {
databaseUri: initialInfo.databaseInfo.databaseUri,
name: initialInfo.databaseInfo.name,
},
start: new Date(initialInfo.start),
id: initialInfo.id,
};
}
function mapQueryEvaluationInfoDataToDomainModel(
evaluationInfo: QueryEvaluationInfoData,
): QueryEvaluationInfo {
return new QueryEvaluationInfo(
evaluationInfo.querySaveDir,
evaluationInfo.dbItemPath,
evaluationInfo.databaseHasMetadataFile,
evaluationInfo.quickEvalPosition,
evaluationInfo.metadata,
);
}

View File

@@ -0,0 +1,90 @@
import { assertNever } from "../../pure/helpers-pure";
import { LocalQueryInfo, InitialQueryInfo } from "../../query-results";
import { QueryEvaluationInfo } from "../../run-queries-shared";
import { QueryHistoryInfo } from "../query-history-info";
import {
LocalQueryDataItem,
InitialQueryInfoData,
QueryEvaluationInfoData,
} from "./local-query-data-item";
import { QueryHistoryDataItem } from "./query-history-data";
import { VariantAnalysisDataItem } from "./variant-analysis-data-item";
// Maps Query History Domain Models to Data Models
export function mapQueryHistoryToDataModels(
queries: QueryHistoryInfo[],
): QueryHistoryDataItem[] {
return queries.map((q) => {
if (q.t === "variant-analysis") {
const query: VariantAnalysisDataItem = q;
return query;
} else if (q.t === "local") {
return mapLocalQueryInfoToDataModel(q);
} else {
assertNever(q);
}
});
}
function mapLocalQueryInfoToDataModel(
query: LocalQueryInfo,
): LocalQueryDataItem {
return {
initialInfo: mapInitialQueryInfoToDataModel(query.initialInfo),
t: "local",
evalLogLocation: query.evalLogLocation,
evalLogSummaryLocation: query.evalLogSummaryLocation,
jsonEvalLogSummaryLocation: query.jsonEvalLogSummaryLocation,
evalLogSummarySymbolsLocation: query.evalLogSummarySymbolsLocation,
failureReason: query.failureReason,
completedQuery: query.completedQuery && {
query: mapQueryEvaluationInfoToDataModel(query.completedQuery.query),
result: {
runId: query.completedQuery.result.runId,
queryId: query.completedQuery.result.queryId,
resultType: query.completedQuery.result.resultType,
evaluationTime: query.completedQuery.result.evaluationTime,
message: query.completedQuery.result.message,
logFileLocation: query.completedQuery.result.logFileLocation,
},
logFileLocation: query.completedQuery.logFileLocation,
successful: query.completedQuery.successful,
message: query.completedQuery.message,
resultCount: query.completedQuery.resultCount,
sortedResultsInfo: query.completedQuery.sortedResultsInfo,
},
};
}
function mapInitialQueryInfoToDataModel(
localQueryInitialInfo: InitialQueryInfo,
): InitialQueryInfoData {
return {
userSpecifiedLabel: localQueryInitialInfo.userSpecifiedLabel,
queryText: localQueryInitialInfo.queryText,
isQuickQuery: localQueryInitialInfo.isQuickQuery,
isQuickEval: localQueryInitialInfo.isQuickEval,
quickEvalPosition: localQueryInitialInfo.quickEvalPosition,
queryPath: localQueryInitialInfo.queryPath,
databaseInfo: {
databaseUri: localQueryInitialInfo.databaseInfo.databaseUri,
name: localQueryInitialInfo.databaseInfo.name,
},
start: localQueryInitialInfo.start,
id: localQueryInitialInfo.id,
};
}
function mapQueryEvaluationInfoToDataModel(
queryEvaluationInfo: QueryEvaluationInfo,
): QueryEvaluationInfoData {
return {
querySaveDir: queryEvaluationInfo.querySaveDir,
dbItemPath: queryEvaluationInfo.dbItemPath,
databaseHasMetadataFile: queryEvaluationInfo.databaseHasMetadataFile,
quickEvalPosition: queryEvaluationInfo.quickEvalPosition,
metadata: queryEvaluationInfo.metadata,
resultsPaths: queryEvaluationInfo.resultsPaths,
};
}

View File

@@ -0,0 +1,100 @@
export interface LocalQueryDataItem {
initialInfo: InitialQueryInfoData;
t: "local";
evalLogLocation?: string;
evalLogSummaryLocation?: string;
jsonEvalLogSummaryLocation?: string;
evalLogSummarySymbolsLocation?: string;
completedQuery?: CompletedQueryInfoData;
failureReason?: string;
}
export interface InitialQueryInfoData {
userSpecifiedLabel?: string;
queryText: string;
isQuickQuery: boolean;
isQuickEval: boolean;
quickEvalPosition?: PositionData;
queryPath: string;
databaseInfo: DatabaseInfoData;
start: Date;
id: string;
}
interface DatabaseInfoData {
name: string;
databaseUri: string;
}
interface PositionData {
line: number;
column: number;
endLine: number;
endColumn: number;
fileName: string;
}
export interface CompletedQueryInfoData {
query: QueryEvaluationInfoData;
message?: string;
successful?: boolean;
// There once was a typo in the data model, which is why we need to support both
sucessful?: boolean;
result: EvaluationResultData;
logFileLocation?: string;
resultCount: number;
sortedResultsInfo: Record<string, SortedResultSetInfo>;
interpretedResultsSortState?: InterpretedResultsSortState;
}
interface InterpretedResultsSortState {
sortBy: InterpretedResultsSortColumn;
sortDirection: SortDirection;
}
type InterpretedResultsSortColumn = "alert-message";
interface SortedResultSetInfo {
resultsPath: string;
sortState: RawResultsSortState;
}
interface RawResultsSortState {
columnIndex: number;
sortDirection: SortDirection;
}
enum SortDirection {
asc,
desc,
}
interface EvaluationResultData {
runId: number;
queryId: number;
resultType: number;
evaluationTime: number;
message?: string;
logFileLocation?: string;
}
export interface QueryEvaluationInfoData {
querySaveDir: string;
dbItemPath: string;
databaseHasMetadataFile: boolean;
quickEvalPosition?: PositionData;
metadata?: QueryMetadataData;
resultsPaths: {
resultsPath: string;
interpretedResultsPath: string;
};
}
interface QueryMetadataData {
name?: string;
description?: string;
id?: string;
kind?: string;
scored?: string;
}

View File

@@ -0,0 +1,14 @@
// Contains models and consts for the data we want to store in the query history store.
// Changes to these models should be done carefully and account for backwards compatibility of data.
import { LocalQueryDataItem } from "./local-query-data-item";
import { VariantAnalysisDataItem } from "./variant-analysis-data-item";
export const ALLOWED_QUERY_HISTORY_VERSIONS = [1, 2];
export interface QueryHistoryData {
version: number;
queries: QueryHistoryDataItem[];
}
export type QueryHistoryDataItem = LocalQueryDataItem | VariantAnalysisDataItem;

View File

@@ -1,4 +1,4 @@
import { pathExists, readFile, remove, mkdir, writeFile } from "fs-extra";
import { pathExists, remove, mkdir, writeFile, readJson } from "fs-extra";
import { dirname } from "path";
import { showAndLogExceptionWithTelemetry } from "../../helpers";
@@ -8,11 +8,15 @@ import {
getErrorMessage,
getErrorStack,
} from "../../pure/helpers-pure";
import { CompletedQueryInfo, LocalQueryInfo } from "../../query-results";
import { QueryHistoryInfo } from "../query-history-info";
import { QueryEvaluationInfo } from "../../run-queries-shared";
import { QueryResultType } from "../../pure/legacy-messages";
import { redactableError } from "../../pure/errors";
import {
ALLOWED_QUERY_HISTORY_VERSIONS,
QueryHistoryData,
QueryHistoryDataItem,
} from "./query-history-data";
import { mapQueryHistoryToDomainModels } from "./data-mapper";
import { mapQueryHistoryToDataModels } from "./domain-mapper";
export async function readQueryHistoryFromFile(
fsPath: string,
@@ -22,9 +26,11 @@ export async function readQueryHistoryFromFile(
return [];
}
const data = await readFile(fsPath, "utf8");
const obj = JSON.parse(data);
if (![1, 2].includes(obj.version)) {
const obj: QueryHistoryData = await readJson(fsPath, {
encoding: "utf8",
});
if (!ALLOWED_QUERY_HISTORY_VERSIONS.includes(obj.version)) {
void showAndLogExceptionWithTelemetry(
redactableError`Can't parse query history. Unsupported query history format: v${obj.version}.`,
);
@@ -32,61 +38,33 @@ export async function readQueryHistoryFromFile(
}
const queries = obj.queries;
const parsedQueries = queries
// Remove remote queries, which are not supported anymore.
.filter((q: QueryHistoryInfo | { t: "remote" }) => q.t !== "remote")
.map((q: QueryHistoryInfo) => {
// Need to explicitly set prototype since reading in from JSON will not
// do this automatically. Note that we can't call the constructor here since
// the constructor invokes extra logic that we don't want to do.
if (q.t === "local") {
Object.setPrototypeOf(q, LocalQueryInfo.prototype);
// Remove remote queries, which are not supported anymore.
const parsedQueries = queries.filter(
(q: QueryHistoryDataItem | { t: "remote" }) => q.t !== "remote",
);
// Date instances are serialized as strings. Need to
// convert them back to Date instances.
(q.initialInfo as any).start = new Date(q.initialInfo.start);
if (q.completedQuery) {
// Again, need to explicitly set prototypes.
Object.setPrototypeOf(
q.completedQuery,
CompletedQueryInfo.prototype,
);
Object.setPrototypeOf(
q.completedQuery.query,
QueryEvaluationInfo.prototype,
);
// Previously, there was a typo in the completedQuery type. There was a field
// `sucessful` and it was renamed to `successful`. We need to handle this case.
if ("sucessful" in q.completedQuery) {
(q.completedQuery as any).successful = (
q.completedQuery as any
).sucessful;
delete (q.completedQuery as any).sucessful;
}
if (!("successful" in q.completedQuery)) {
(q.completedQuery as any).successful =
q.completedQuery.result?.resultType === QueryResultType.SUCCESS;
}
}
}
return q;
});
// Map the data models to the domain models.
const domainModels: QueryHistoryInfo[] =
mapQueryHistoryToDomainModels(parsedQueries);
// filter out queries that have been deleted on disk
// most likely another workspace has deleted them because the
// queries aged out.
return asyncFilter(parsedQueries, async (q) => {
if (q.t === "variant-analysis") {
// the deserializer doesn't know where the remote queries are stored
// so we need to assume here that they exist. Later, we check to
// see if they exist on disk.
return true;
}
const resultsPath = q.completedQuery?.query.resultsPaths.resultsPath;
return !!resultsPath && (await pathExists(resultsPath));
});
const filteredDomainModels: Promise<QueryHistoryInfo[]> = asyncFilter(
domainModels,
async (q) => {
if (q.t === "variant-analysis") {
// the query history store doesn't know where variant analysises are
// stored so we need to assume here that they exist. We check later
// to see if they exist on disk.
return true;
}
const resultsPath = q.completedQuery?.query.resultsPaths.resultsPath;
return !!resultsPath && (await pathExists(resultsPath));
},
);
return filteredDomainModels;
} catch (e) {
void showAndLogExceptionWithTelemetry(
redactableError(asError(e))`Error loading query history.`,
@@ -121,13 +99,17 @@ export async function writeQueryHistoryToFile(
const filteredQueries = queries.filter((q) =>
q.t === "local" ? q.completedQuery !== undefined : true,
);
// map domain model queries to data model
const queryHistoryData = mapQueryHistoryToDataModels(filteredQueries);
const data = JSON.stringify(
{
// version 2:
// - adds the `variant-analysis` type
// - ensures a `successful` property exists on completedQuery
version: 2,
queries: filteredQueries,
queries: queryHistoryData,
},
null,
2,

View File

@@ -0,0 +1,83 @@
import { QueryLanguage } from "../../common/query-language";
import { QueryStatus } from "../../query-status";
import {
VariantAnalysisFailureReason,
VariantAnalysisRepoStatus,
VariantAnalysisStatus,
} from "../../variant-analysis/shared/variant-analysis";
// Data Model for Variant Analysis Query History Items
// All data points are modelled, except enums.
export interface VariantAnalysisDataItem {
readonly t: "variant-analysis";
failureReason?: string;
resultCount?: number;
status: QueryStatus;
completed: boolean;
variantAnalysis: VariantAnalysisQueryHistoryData;
userSpecifiedLabel?: string;
}
export interface VariantAnalysisQueryHistoryData {
id: number;
controllerRepo: {
id: number;
fullName: string;
private: boolean;
};
query: {
name: string;
filePath: string;
language: QueryLanguage;
text: string;
};
databases: {
repositories?: string[];
repositoryLists?: string[];
repositoryOwners?: string[];
};
createdAt: string;
updatedAt: string;
executionStartTime: number;
status: VariantAnalysisStatus;
completedAt?: string;
actionsWorkflowRunId?: number;
failureReason?: VariantAnalysisFailureReason;
scannedRepos?: VariantAnalysisScannedRepositoryData[];
skippedRepos?: VariantAnalysisSkippedRepositoriesData;
}
export interface VariantAnalysisScannedRepositoryData {
repository: {
id: number;
fullName: string;
private: boolean;
stargazersCount: number;
updatedAt: string | null;
};
analysisStatus: VariantAnalysisRepoStatus;
resultCount?: number;
artifactSizeInBytes?: number;
failureMessage?: string;
}
export interface VariantAnalysisSkippedRepositoriesData {
accessMismatchRepos?: VariantAnalysisSkippedRepositoryGroupData;
notFoundRepos?: VariantAnalysisSkippedRepositoryGroupData;
noCodeqlDbRepos?: VariantAnalysisSkippedRepositoryGroupData;
overLimitRepos?: VariantAnalysisSkippedRepositoryGroupData;
}
export interface VariantAnalysisSkippedRepositoryGroupData {
repositoryCount: number;
repositories: VariantAnalysisSkippedRepositoryData[];
}
export interface VariantAnalysisSkippedRepositoryData {
id?: number;
fullName: string;
private?: boolean;
stargazersCount?: number;
updatedAt?: string | null;
}

View File

@@ -49,45 +49,31 @@ export interface InitialQueryInfo {
}
export class CompletedQueryInfo implements QueryWithResults {
readonly query: QueryEvaluationInfo;
readonly message?: string;
readonly successful?: boolean;
/**
* The legacy result. This is only set when loading from the query history.
*/
readonly result: legacyMessages.EvaluationResult;
readonly logFileLocation?: string;
resultCount: number;
constructor(
public readonly query: QueryEvaluationInfo,
/**
* Map from result set name to SortedResultSetInfo.
*/
sortedResultsInfo: Record<string, SortedResultSetInfo>;
/**
* The legacy result. This is only set when loading from the query history.
*/
public readonly result: legacyMessages.EvaluationResult,
public readonly logFileLocation: string | undefined,
public readonly successful: boolean | undefined,
public readonly message: string | undefined,
/**
* How we're currently sorting alerts. This is not mere interface
* state due to truncation; on re-sort, we want to read in the file
* again, sort it, and only ship off a reasonable number of results
* to the webview. Undefined means to use whatever order is in the
* sarif file.
*/
public interpretedResultsSortState: InterpretedResultsSortState | undefined,
public resultCount: number = 0,
/**
* How we're currently sorting alerts. This is not mere interface
* state due to truncation; on re-sort, we want to read in the file
* again, sort it, and only ship off a reasonable number of results
* to the webview. Undefined means to use whatever order is in the
* sarif file.
*/
interpretedResultsSortState: InterpretedResultsSortState | undefined;
/**
* Note that in the {@link readQueryHistoryFromFile} method, we create a CompletedQueryInfo instance
* by explicitly setting the prototype in order to avoid calling this constructor.
*/
constructor(evaluation: QueryWithResults) {
this.query = evaluation.query;
this.logFileLocation = evaluation.logFileLocation;
this.result = evaluation.result;
this.message = evaluation.message;
this.successful = evaluation.successful;
this.sortedResultsInfo = {};
this.resultCount = 0;
}
/**
* Map from result set name to SortedResultSetInfo.
*/
public sortedResultsInfo: Record<string, SortedResultSetInfo> = {},
) {}
setResultCount(value: number) {
this.resultCount = value;
@@ -220,20 +206,15 @@ export type CompletedLocalQueryInfo = LocalQueryInfo & {
export class LocalQueryInfo {
readonly t = "local";
public failureReason: string | undefined;
public completedQuery: CompletedQueryInfo | undefined;
public evalLogLocation: string | undefined;
public evalLogSummaryLocation: string | undefined;
public jsonEvalLogSummaryLocation: string | undefined;
public evalLogSummarySymbolsLocation: string | undefined;
/**
* Note that in the {@link readQueryHistoryFromFile} method, we create a FullQueryInfo instance
* by explicitly setting the prototype in order to avoid calling this constructor.
*/
constructor(
public readonly initialInfo: InitialQueryInfo,
private cancellationSource?: CancellationTokenSource, // used to cancel in progress queries
public failureReason?: string,
public completedQuery?: CompletedQueryInfo,
public evalLogLocation?: string,
public evalLogSummaryLocation?: string,
public jsonEvalLogSummaryLocation?: string,
public evalLogSummarySymbolsLocation?: string,
) {
/**/
}
@@ -301,7 +282,14 @@ export class LocalQueryInfo {
}
completeThisQuery(info: QueryWithResults): void {
this.completedQuery = new CompletedQueryInfo(info);
this.completedQuery = new CompletedQueryInfo(
info.query,
info.result,
info.logFileLocation,
info.successful,
info.message,
undefined,
);
// dispose of the cancellation token source and also ensure the source is not serialized as JSON
this.cancellationSource?.dispose();

View File

@@ -137,7 +137,7 @@ export class QueryEvaluationInfo extends QueryOutputDir {
constructor(
querySaveDir: string,
public readonly dbItemPath: string,
private readonly databaseHasMetadataFile: boolean,
public readonly databaseHasMetadataFile: boolean,
public readonly quickEvalPosition?: messages.Position,
public readonly metadata?: QueryMetadata,
) {
@@ -382,10 +382,10 @@ export class QueryEvaluationInfo extends QueryOutputDir {
export interface QueryWithResults {
readonly query: QueryEvaluationInfo;
readonly result: legacyMessages.EvaluationResult;
readonly logFileLocation?: string;
readonly successful?: boolean;
readonly message?: string;
readonly result: legacyMessages.EvaluationResult;
}
/**

View File

@@ -8,6 +8,7 @@ import {
import * as React from "react";
import { useCallback, useMemo } from "react";
import styled from "styled-components";
import { vscode } from "../vscode-api";
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
import {
@@ -31,6 +32,13 @@ const SupportSpan = styled.span<SupportedUnsupportedSpanProps>`
color: ${(props) => (props.supported ? "green" : "red")};
`;
const UsagesButton = styled.button`
color: var(--vscode-editor-foreground);
background-color: transparent;
border: none;
cursor: pointer;
`;
type Props = {
externalApiUsage: ExternalApiUsage;
modeledMethod: ModeledMethod | undefined;
@@ -115,6 +123,13 @@ export const MethodRow = ({
[onChange, externalApiUsage, modeledMethod],
);
const jumpToUsage = useCallback(() => {
vscode.postMessage({
t: "jumpToUsage",
location: externalApiUsage.usages[0].url,
});
}, [externalApiUsage]);
return (
<VSCodeDataGridRow>
<VSCodeDataGridCell gridColumn={1}>
@@ -129,7 +144,9 @@ export const MethodRow = ({
</SupportSpan>
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={3}>
{externalApiUsage.usages.length}
<UsagesButton onClick={jumpToUsage}>
{externalApiUsage.usages.length}
</UsagesButton>
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={4}>
{(!externalApiUsage.supported ||