Merge remote-tracking branch 'origin/main' into koesie10/export-progress

This commit is contained in:
Koen Vlaswinkel
2022-12-02 14:25:05 +01:00
22 changed files with 897 additions and 106 deletions

View File

@@ -10,14 +10,53 @@ export interface DbConfigDatabases {
local: LocalDbConfig;
}
export interface SelectedDbItem {
kind: SelectedDbItemKind;
value: string;
}
export type SelectedDbItem =
| SelectedLocalUserDefinedList
| SelectedLocalDatabase
| SelectedRemoteSystemDefinedList
| SelectedRemoteUserDefinedList
| SelectedRemoteOwner
| SelectedRemoteRepository;
export enum SelectedDbItemKind {
ConfigDefined = "configDefined",
LocalUserDefinedList = "localUserDefinedList",
LocalDatabase = "localDatabase",
RemoteSystemDefinedList = "remoteSystemDefinedList",
RemoteUserDefinedList = "remoteUserDefinedList",
RemoteOwner = "remoteOwner",
RemoteRepository = "remoteRepository",
}
export interface SelectedLocalUserDefinedList {
kind: SelectedDbItemKind.LocalUserDefinedList;
listName: string;
}
export interface SelectedLocalDatabase {
kind: SelectedDbItemKind.LocalDatabase;
databaseName: string;
listName?: string;
}
export interface SelectedRemoteSystemDefinedList {
kind: SelectedDbItemKind.RemoteSystemDefinedList;
listName: string;
}
export interface SelectedRemoteUserDefinedList {
kind: SelectedDbItemKind.RemoteUserDefinedList;
listName: string;
}
export interface SelectedRemoteOwner {
kind: SelectedDbItemKind.RemoteOwner;
ownerName: string;
}
export interface SelectedRemoteRepository {
kind: SelectedDbItemKind.RemoteRepository;
repositoryName: string;
listName?: string;
}
export interface RemoteDbConfig {
@@ -70,10 +109,44 @@ export function cloneDbConfig(config: DbConfig): DbConfig {
},
},
selected: config.selected
? {
kind: config.selected.kind,
value: config.selected.value,
}
? cloneDbConfigSelectedItem(config.selected)
: undefined,
};
}
function cloneDbConfigSelectedItem(selected: SelectedDbItem): SelectedDbItem {
switch (selected.kind) {
case SelectedDbItemKind.LocalUserDefinedList:
return {
kind: SelectedDbItemKind.LocalUserDefinedList,
listName: selected.listName,
};
case SelectedDbItemKind.LocalDatabase:
return {
kind: SelectedDbItemKind.LocalDatabase,
databaseName: selected.databaseName,
listName: selected.listName,
};
case SelectedDbItemKind.RemoteSystemDefinedList:
return {
kind: SelectedDbItemKind.RemoteSystemDefinedList,
listName: selected.listName,
};
case SelectedDbItemKind.RemoteUserDefinedList:
return {
kind: SelectedDbItemKind.RemoteUserDefinedList,
listName: selected.listName,
};
case SelectedDbItemKind.RemoteOwner:
return {
kind: SelectedDbItemKind.RemoteOwner,
ownerName: selected.ownerName,
};
case SelectedDbItemKind.RemoteRepository:
return {
kind: SelectedDbItemKind.RemoteRepository,
repositoryName: selected.repositoryName,
listName: selected.listName,
};
}
}

View File

@@ -20,12 +20,14 @@ export type LocalDbItem = LocalListDbItem | LocalDatabaseDbItem;
export interface LocalListDbItem {
kind: DbItemKind.LocalList;
selected: boolean;
listName: string;
databases: LocalDatabaseDbItem[];
}
export interface LocalDatabaseDbItem {
kind: DbItemKind.LocalDatabase;
selected: boolean;
databaseName: string;
dateAdded: number;
language: string;
@@ -51,6 +53,7 @@ export type RemoteDbItem =
export interface RemoteSystemDefinedListDbItem {
kind: DbItemKind.RemoteSystemDefinedList;
selected: boolean;
listName: string;
listDisplayName: string;
listDescription: string;
@@ -58,16 +61,66 @@ export interface RemoteSystemDefinedListDbItem {
export interface RemoteUserDefinedListDbItem {
kind: DbItemKind.RemoteUserDefinedList;
selected: boolean;
listName: string;
repos: RemoteRepoDbItem[];
}
export interface RemoteOwnerDbItem {
kind: DbItemKind.RemoteOwner;
selected: boolean;
ownerName: string;
}
export interface RemoteRepoDbItem {
kind: DbItemKind.RemoteRepo;
selected: boolean;
repoFullName: string;
}
export function isRemoteSystemDefinedListDbItem(
dbItem: DbItem,
): dbItem is RemoteSystemDefinedListDbItem {
return dbItem.kind === DbItemKind.RemoteSystemDefinedList;
}
export function isRemoteUserDefinedListDbItem(
dbItem: DbItem,
): dbItem is RemoteUserDefinedListDbItem {
return dbItem.kind === DbItemKind.RemoteUserDefinedList;
}
export function isRemoteOwnerDbItem(
dbItem: DbItem,
): dbItem is RemoteOwnerDbItem {
return dbItem.kind === DbItemKind.RemoteOwner;
}
export function isRemoteRepoDbItem(dbItem: DbItem): dbItem is RemoteRepoDbItem {
return dbItem.kind === DbItemKind.RemoteRepo;
}
export function isLocalListDbItem(dbItem: DbItem): dbItem is LocalListDbItem {
return dbItem.kind === DbItemKind.LocalList;
}
export function isLocalDatabaseDbItem(
dbItem: DbItem,
): dbItem is LocalDatabaseDbItem {
return dbItem.kind === DbItemKind.LocalDatabase;
}
export type SelectableDbItem = RemoteDbItem | LocalDbItem;
export function isSelectableDbItem(dbItem: DbItem): dbItem is SelectableDbItem {
return SelectableDbItemKinds.includes(dbItem.kind);
}
const SelectableDbItemKinds = [
DbItemKind.LocalList,
DbItemKind.LocalDatabase,
DbItemKind.RemoteSystemDefinedList,
DbItemKind.RemoteUserDefinedList,
DbItemKind.RemoteOwner,
DbItemKind.RemoteRepo,
];

View File

@@ -1,3 +1,4 @@
import { window } from "vscode";
import { App, AppMode } from "../common/app";
import { isCanary, isNewQueryRunExperienceEnabled } from "../config";
import { extLogger } from "../common";
@@ -5,6 +6,7 @@ import { DisposableObject } from "../pure/disposable-object";
import { DbConfigStore } from "./config/db-config-store";
import { DbManager } from "./db-manager";
import { DbPanel } from "./ui/db-panel";
import { DbSelectionDecorationProvider } from "./ui/db-selection-decoration-provider";
export class DbModule extends DisposableObject {
public async initialize(app: App): Promise<void> {
@@ -30,6 +32,10 @@ export class DbModule extends DisposableObject {
this.push(dbPanel);
this.push(dbConfigStore);
const dbSelectionDecorationProvider = new DbSelectionDecorationProvider();
window.registerFileDecorationProvider(dbSelectionDecorationProvider);
}
}

View File

@@ -3,6 +3,7 @@ import {
LocalDatabase,
LocalList,
RemoteRepositoryList,
SelectedDbItemKind,
} from "./config/db-config";
import {
DbItemKind,
@@ -18,16 +19,20 @@ import {
export function createRemoteTree(dbConfig: DbConfig): RootRemoteDbItem {
const systemDefinedLists = [
createSystemDefinedList(10),
createSystemDefinedList(100),
createSystemDefinedList(1000),
createSystemDefinedList(10, dbConfig),
createSystemDefinedList(100, dbConfig),
createSystemDefinedList(1000, dbConfig),
];
const userDefinedRepoLists = dbConfig.databases.remote.repositoryLists.map(
createUserDefinedList,
(r) => createRemoteUserDefinedList(r, dbConfig),
);
const owners = dbConfig.databases.remote.owners.map((o) =>
createOwnerItem(o, dbConfig),
);
const repos = dbConfig.databases.remote.repositories.map((r) =>
createRepoItem(r, dbConfig),
);
const owners = dbConfig.databases.remote.owners.map(createOwnerItem);
const repos = dbConfig.databases.remote.repositories.map(createRepoItem);
return {
kind: DbItemKind.RootRemote,
@@ -41,8 +46,12 @@ export function createRemoteTree(dbConfig: DbConfig): RootRemoteDbItem {
}
export function createLocalTree(dbConfig: DbConfig): RootLocalDbItem {
const localLists = dbConfig.databases.local.lists.map(createLocalList);
const localDbs = dbConfig.databases.local.databases.map(createLocalDb);
const localLists = dbConfig.databases.local.lists.map((l) =>
createLocalList(l, dbConfig),
);
const localDbs = dbConfig.databases.local.databases.map((l) =>
createLocalDb(l, dbConfig),
);
return {
kind: DbItemKind.RootLocal,
@@ -50,53 +59,105 @@ export function createLocalTree(dbConfig: DbConfig): RootLocalDbItem {
};
}
function createSystemDefinedList(n: number): RemoteSystemDefinedListDbItem {
function createSystemDefinedList(
n: number,
dbConfig: DbConfig,
): RemoteSystemDefinedListDbItem {
const listName = `top_${n}`;
const selected =
dbConfig.selected &&
dbConfig.selected.kind === SelectedDbItemKind.RemoteSystemDefinedList &&
dbConfig.selected.listName === listName;
return {
kind: DbItemKind.RemoteSystemDefinedList,
listName: `top_${n}`,
listName,
listDisplayName: `Top ${n} repositories`,
listDescription: `Top ${n} repositories of a language`,
selected: !!selected,
};
}
function createUserDefinedList(
function createRemoteUserDefinedList(
list: RemoteRepositoryList,
dbConfig: DbConfig,
): RemoteUserDefinedListDbItem {
const selected =
dbConfig.selected &&
dbConfig.selected.kind === SelectedDbItemKind.RemoteUserDefinedList &&
dbConfig.selected.listName === list.name;
return {
kind: DbItemKind.RemoteUserDefinedList,
listName: list.name,
repos: list.repositories.map((r) => createRepoItem(r)),
repos: list.repositories.map((r) => createRepoItem(r, dbConfig, list.name)),
selected: !!selected,
};
}
function createOwnerItem(owner: string): RemoteOwnerDbItem {
function createOwnerItem(owner: string, dbConfig: DbConfig): RemoteOwnerDbItem {
const selected =
dbConfig.selected &&
dbConfig.selected.kind === SelectedDbItemKind.RemoteOwner &&
dbConfig.selected.ownerName === owner;
return {
kind: DbItemKind.RemoteOwner,
ownerName: owner,
selected: !!selected,
};
}
function createRepoItem(repo: string): RemoteRepoDbItem {
function createRepoItem(
repo: string,
dbConfig: DbConfig,
listName?: string,
): RemoteRepoDbItem {
const selected =
dbConfig.selected &&
dbConfig.selected.kind === SelectedDbItemKind.RemoteRepository &&
dbConfig.selected.repositoryName === repo &&
dbConfig.selected.listName === listName;
return {
kind: DbItemKind.RemoteRepo,
repoFullName: repo,
selected: !!selected,
};
}
function createLocalList(list: LocalList): LocalListDbItem {
function createLocalList(list: LocalList, dbConfig: DbConfig): LocalListDbItem {
const selected =
dbConfig.selected &&
dbConfig.selected.kind === SelectedDbItemKind.LocalUserDefinedList &&
dbConfig.selected.listName === list.name;
return {
kind: DbItemKind.LocalList,
listName: list.name,
databases: list.databases.map(createLocalDb),
databases: list.databases.map((d) => createLocalDb(d, dbConfig, list.name)),
selected: !!selected,
};
}
function createLocalDb(db: LocalDatabase): LocalDatabaseDbItem {
function createLocalDb(
db: LocalDatabase,
dbConfig: DbConfig,
listName?: string,
): LocalDatabaseDbItem {
const selected =
dbConfig.selected &&
dbConfig.selected.kind === SelectedDbItemKind.LocalDatabase &&
dbConfig.selected.databaseName === db.name &&
dbConfig.selected.listName === listName;
return {
kind: DbItemKind.LocalDatabase,
databaseName: db.name,
dateAdded: db.dateAdded,
language: db.language,
storagePath: db.storagePath,
selected: !!selected,
};
}

View File

@@ -0,0 +1,22 @@
import {
CancellationToken,
FileDecoration,
FileDecorationProvider,
ProviderResult,
Uri,
} from "vscode";
export class DbSelectionDecorationProvider implements FileDecorationProvider {
provideFileDecoration(
uri: Uri,
_token: CancellationToken,
): ProviderResult<FileDecoration> {
if (uri?.query === "selected=true") {
return {
badge: "✔",
};
}
return undefined;
}
}

View File

@@ -1,6 +1,7 @@
import * as vscode from "vscode";
import {
DbItem,
isSelectableDbItem,
LocalDatabaseDbItem,
LocalListDbItem,
RemoteOwnerDbItem,
@@ -28,6 +29,16 @@ export class DbTreeViewItem extends vscode.TreeItem {
public readonly children: DbTreeViewItem[],
) {
super(label, collapsibleState);
if (dbItem && isSelectableDbItem(dbItem)) {
if (dbItem.selected) {
// Define the resource id to drive the UI to render this item as selected.
this.resourceUri = vscode.Uri.parse("codeql://databases?selected=true");
} else {
// Define a context value to drive the UI to show an action to select the item.
this.contextValue = "selectableDbItem";
}
}
}
}

View File

@@ -2,13 +2,13 @@ import { join } from "path";
import { ensureDir, writeFile } from "fs-extra";
import {
window,
commands,
Uri,
ExtensionContext,
workspace,
ViewColumn,
CancellationToken,
ExtensionContext,
Uri,
ViewColumn,
window,
workspace,
} from "vscode";
import { Credentials } from "../authentication";
import { ProgressCallback, UserCancellationException } from "../commandRunner";
@@ -21,6 +21,7 @@ import {
generateMarkdown,
generateVariantAnalysisMarkdown,
MarkdownFile,
RepositorySummary,
} from "./remote-queries-markdown-generation";
import { RemoteQuery } from "./remote-query";
import { AnalysisResults, sumAnalysesResults } from "./shared/analysis-result";
@@ -30,6 +31,7 @@ import { assertNever } from "../pure/helpers-pure";
import {
VariantAnalysis,
VariantAnalysisScannedRepository,
VariantAnalysisScannedRepositoryDownloadStatus,
VariantAnalysisScannedRepositoryResult,
} from "./shared/variant-analysis";
import {
@@ -162,6 +164,10 @@ export async function exportVariantAnalysisResults(
throw new UserCancellationException("Cancelled");
}
const repoStates = await variantAnalysisManager.getRepoStates(
variantAnalysisId,
);
void extLogger.log(
`Exporting variant analysis results for variant analysis with id ${variantAnalysis.id}`,
);
@@ -197,6 +203,18 @@ export async function exportVariantAnalysisResults(
}
for (const repo of repositories) {
const repoState = repoStates.find(
(r) => r.repositoryId === repo.repository.id,
);
// Do not export if it has not yet completed or the download has not yet succeeded.
if (
repoState?.downloadStatus !==
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded
) {
continue;
}
if (repo.resultCount == 0) {
yield [
repo,
@@ -268,11 +286,14 @@ export async function exportVariantAnalysisAnalysisResults(
message: "Generating Markdown files",
});
const description = buildVariantAnalysisGistDescription(variantAnalysis);
const markdownFiles = await generateVariantAnalysisMarkdown(
const { markdownFiles, summaries } = await generateVariantAnalysisMarkdown(
variantAnalysis,
analysesResults,
"gist",
exportFormat,
);
const description = buildVariantAnalysisGistDescription(
variantAnalysis,
summaries,
);
await exportResults(
@@ -407,20 +428,16 @@ const buildGistDescription = (
*/
const buildVariantAnalysisGistDescription = (
variantAnalysis: VariantAnalysis,
summaries: RepositorySummary[],
) => {
const resultCount =
variantAnalysis.scannedRepos?.reduce(
(acc, item) => acc + (item.resultCount ?? 0),
0,
) ?? 0;
const resultCount = summaries.reduce(
(acc, summary) => acc + (summary.resultCount ?? 0),
0,
);
const resultLabel = pluralize(resultCount, "result", "results");
const repositoryLabel = variantAnalysis.scannedRepos?.length
? `(${pluralize(
variantAnalysis.scannedRepos.length,
"repository",
"repositories",
)})`
const repositoryLabel = summaries.length
? `(${pluralize(summaries.length, "repository", "repositories")})`
: "";
return `${variantAnalysis.query.name} (${variantAnalysis.query.language}) ${resultLabel} ${repositoryLabel}`;
};

View File

@@ -18,6 +18,7 @@ import {
VariantAnalysisScannedRepository,
VariantAnalysisScannedRepositoryResult,
} from "./shared/variant-analysis";
import { RepositoryWithMetadata } from "./shared/repository";
export type MarkdownLinkType = "local" | "gist";
@@ -74,6 +75,17 @@ export function generateMarkdown(
return [summaryFile, ...resultsFiles];
}
export interface RepositorySummary {
fileName: string;
repository: RepositoryWithMetadata;
resultCount: number;
}
export interface VariantAnalysisMarkdown {
markdownFiles: MarkdownFile[];
summaries: RepositorySummary[];
}
/**
* Generates markdown files with variant analysis results.
*/
@@ -83,23 +95,22 @@ export async function generateVariantAnalysisMarkdown(
[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]
>,
linkType: MarkdownLinkType,
): Promise<MarkdownFile[]> {
): Promise<VariantAnalysisMarkdown> {
const resultsFiles: MarkdownFile[] = [];
// Generate summary file with links to individual files
const summaryFile: MarkdownFile =
generateVariantAnalysisMarkdownSummary(variantAnalysis);
const summaries: RepositorySummary[] = [];
for await (const [scannedRepo, result] of results) {
if (scannedRepo.resultCount === 0) {
if (!scannedRepo.resultCount || scannedRepo.resultCount === 0) {
continue;
}
// Append nwo and results count to the summary table
const fullName = scannedRepo.repository.fullName;
const fileName = createFileName(fullName);
const link = createRelativeLink(fileName, linkType);
summaryFile.content.push(
`| ${fullName} | [${scannedRepo.resultCount} result(s)](${link}) |`,
);
summaries.push({
fileName,
repository: scannedRepo.repository,
resultCount: scannedRepo.resultCount,
});
// Generate individual markdown file for each repository
const resultsFileContent = [`### ${scannedRepo.repository.fullName}`, ""];
@@ -121,7 +132,18 @@ export async function generateVariantAnalysisMarkdown(
content: resultsFileContent,
});
}
return [summaryFile, ...resultsFiles];
// Generate summary file with links to individual files
const summaryFile: MarkdownFile = generateVariantAnalysisMarkdownSummary(
variantAnalysis,
summaries,
linkType,
);
return {
markdownFiles: [summaryFile, ...resultsFiles],
summaries,
};
}
export function generateMarkdownSummary(query: RemoteQuery): MarkdownFile {
@@ -147,6 +169,8 @@ export function generateMarkdownSummary(query: RemoteQuery): MarkdownFile {
export function generateVariantAnalysisMarkdownSummary(
variantAnalysis: VariantAnalysis,
summaries: RepositorySummary[],
linkType: MarkdownLinkType,
): MarkdownFile {
const lines: string[] = [];
// Title
@@ -165,7 +189,14 @@ export function generateVariantAnalysisMarkdownSummary(
// Summary table
lines.push("### Summary", "", "| Repository | Results |", "| --- | --- |");
// nwo and result count will be appended to this table
for (const summary of summaries) {
// Append nwo and results count to the summary table
const fullName = summary.repository.fullName;
const link = createRelativeLink(summary.fileName, linkType);
lines.push(`| ${fullName} | [${summary.resultCount} result(s)](${link}) |`);
}
return {
fileName: "_summary",
content: lines,

View File

@@ -13,9 +13,12 @@ import {
VariantAnalysis,
VariantAnalysisScannedRepository,
} from "./shared/variant-analysis";
import { VariantAnalysis as ApiVariantAnalysis } from "./gh-api/variant-analysis";
import { processUpdatedVariantAnalysis } from "./variant-analysis-processor";
import { DisposableObject } from "../pure/disposable-object";
import { sleep } from "../pure/time";
import { getErrorMessage } from "../pure/helpers-pure";
import { showAndLogWarningMessage } from "../helpers";
export class VariantAnalysisMonitor extends DisposableObject {
// With a sleep of 5 seconds, the maximum number of attempts takes
@@ -60,11 +63,19 @@ export class VariantAnalysisMonitor extends DisposableObject {
return;
}
const variantAnalysisSummary = await getVariantAnalysis(
credentials,
variantAnalysis.controllerRepo.id,
variantAnalysis.id,
);
let variantAnalysisSummary: ApiVariantAnalysis;
try {
variantAnalysisSummary = await getVariantAnalysis(
credentials,
variantAnalysis.controllerRepo.id,
variantAnalysis.id,
);
} catch (e) {
void showAndLogWarningMessage(
`Error while monitoring variant analysis: ${getErrorMessage(e)}`,
);
continue;
}
variantAnalysis = processUpdatedVariantAnalysis(
variantAnalysis,

View File

@@ -13,6 +13,7 @@ export interface VariantAnalysisViewManager<
> {
registerView(view: T): void;
unregisterView(view: T): void;
getView(variantAnalysisId: number): T | undefined;
getVariantAnalysis(
variantAnalysisId: number,

View File

@@ -38,6 +38,15 @@ export class VariantAnalysisViewSerializer implements WebviewPanelSerializer {
const manager = await this.waitForExtensionFullyLoaded();
const existingView = manager.getView(
variantAnalysisState.variantAnalysisId,
);
if (existingView) {
await existingView.openView();
webviewPanel.dispose();
return;
}
const view = new VariantAnalysisView(
this.ctx,
variantAnalysisState.variantAnalysisId,

View File

@@ -43,6 +43,7 @@ const variantAnalysis: VariantAnalysisDomainModel = {
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
resultCount: 100,
},
{
repository: {

View File

@@ -47,10 +47,24 @@ InProgress.args = {
variantAnalysisStatus: VariantAnalysisStatus.InProgress,
};
export const InProgressWithResults = Template.bind({});
InProgressWithResults.args = {
variantAnalysisStatus: VariantAnalysisStatus.InProgress,
showResultActions: true,
};
export const InProgressWithoutDownloadedRepos = Template.bind({});
InProgressWithoutDownloadedRepos.args = {
variantAnalysisStatus: VariantAnalysisStatus.InProgress,
showResultActions: true,
exportResultsDisabled: true,
};
export const Succeeded = Template.bind({});
Succeeded.args = {
...InProgress.args,
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
showResultActions: true,
};
export const Failed = Template.bind({});

View File

@@ -144,6 +144,9 @@ export function VariantAnalysis({
<>
<VariantAnalysisHeader
variantAnalysis={variantAnalysis}
repositoryStates={repoStates}
filterSortState={filterSortState}
selectedRepositoryIds={selectedRepositoryIds}
onOpenQueryFileClick={openQueryFile}
onViewQueryTextClick={openQueryText}
onStopQueryClick={stopQuery}

View File

@@ -3,14 +3,17 @@ import styled from "styled-components";
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react";
import { VariantAnalysisStatus } from "../../remote-queries/shared/variant-analysis";
type Props = {
export type VariantAnalysisActionsProps = {
variantAnalysisStatus: VariantAnalysisStatus;
onStopQueryClick: () => void;
stopQueryDisabled?: boolean;
showResultActions?: boolean;
onCopyRepositoryListClick: () => void;
onExportResultsClick: () => void;
copyRepositoryListDisabled?: boolean;
exportResultsDisabled?: boolean;
};
const Container = styled.div`
@@ -26,12 +29,33 @@ const Button = styled(VSCodeButton)`
export const VariantAnalysisActions = ({
variantAnalysisStatus,
onStopQueryClick,
stopQueryDisabled,
showResultActions,
onCopyRepositoryListClick,
onExportResultsClick,
stopQueryDisabled,
}: Props) => {
copyRepositoryListDisabled,
exportResultsDisabled,
}: VariantAnalysisActionsProps) => {
return (
<Container>
{showResultActions && (
<>
<Button
appearance="secondary"
onClick={onCopyRepositoryListClick}
disabled={copyRepositoryListDisabled}
>
Copy repository list
</Button>
<Button
appearance="primary"
onClick={onExportResultsClick}
disabled={exportResultsDisabled}
>
Export results
</Button>
</>
)}
{variantAnalysisStatus === VariantAnalysisStatus.InProgress && (
<Button
appearance="secondary"
@@ -41,16 +65,6 @@ export const VariantAnalysisActions = ({
Stop query
</Button>
)}
{variantAnalysisStatus === VariantAnalysisStatus.Succeeded && (
<>
<Button appearance="secondary" onClick={onCopyRepositoryListClick}>
Copy repository list
</Button>
<Button appearance="primary" onClick={onExportResultsClick}>
Export results
</Button>
</>
)}
</Container>
);
};

View File

@@ -6,15 +6,25 @@ import {
getTotalResultCount,
hasRepoScanCompleted,
VariantAnalysis,
VariantAnalysisScannedRepositoryDownloadStatus,
VariantAnalysisScannedRepositoryState,
} from "../../remote-queries/shared/variant-analysis";
import { QueryDetails } from "./QueryDetails";
import { VariantAnalysisActions } from "./VariantAnalysisActions";
import { VariantAnalysisStats } from "./VariantAnalysisStats";
import { parseDate } from "../../pure/date";
import { basename } from "../common/path";
import {
defaultFilterSortState,
filterAndSortRepositoriesWithResults,
RepositoriesFilterSortState,
} from "../../pure/variant-analysis-filter-sort";
export type VariantAnalysisHeaderProps = {
variantAnalysis: VariantAnalysis;
repositoryStates?: VariantAnalysisScannedRepositoryState[];
filterSortState?: RepositoriesFilterSortState;
selectedRepositoryIds?: number[];
onOpenQueryFileClick: () => void;
onViewQueryTextClick: () => void;
@@ -40,6 +50,9 @@ const Row = styled.div`
export const VariantAnalysisHeader = ({
variantAnalysis,
repositoryStates,
filterSortState,
selectedRepositoryIds,
onOpenQueryFileClick,
onViewQueryTextClick,
onStopQueryClick,
@@ -62,6 +75,36 @@ export const VariantAnalysisHeader = ({
const hasSkippedRepos = useMemo(() => {
return getSkippedRepoCount(variantAnalysis.skippedRepos) > 0;
}, [variantAnalysis.skippedRepos]);
const filteredRepositories = useMemo(() => {
return filterAndSortRepositoriesWithResults(variantAnalysis.scannedRepos, {
...defaultFilterSortState,
...filterSortState,
repositoryIds: selectedRepositoryIds,
});
}, [filterSortState, selectedRepositoryIds, variantAnalysis.scannedRepos]);
const hasDownloadedRepos = useMemo(() => {
const repositoryStatesById = new Map<
number,
VariantAnalysisScannedRepositoryState
>();
if (repositoryStates) {
for (const repositoryState of repositoryStates) {
repositoryStatesById.set(repositoryState.repositoryId, repositoryState);
}
}
return filteredRepositories?.some((repo) => {
return (
repositoryStatesById.get(repo.repository.id)?.downloadStatus ===
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded
);
});
}, [repositoryStates, filteredRepositories]);
const hasReposWithResults = useMemo(() => {
return filteredRepositories?.some(
(repo) => repo.resultCount && repo.resultCount > 0,
);
}, [filteredRepositories]);
return (
<Container>
@@ -74,10 +117,13 @@ export const VariantAnalysisHeader = ({
/>
<VariantAnalysisActions
variantAnalysisStatus={variantAnalysis.status}
showResultActions={(resultCount ?? 0) > 0}
onStopQueryClick={onStopQueryClick}
onCopyRepositoryListClick={onCopyRepositoryListClick}
onExportResultsClick={onExportResultsClick}
stopQueryDisabled={!variantAnalysis.actionsWorkflowRunId}
exportResultsDisabled={!hasDownloadedRepos}
copyRepositoryListDisabled={!hasReposWithResults}
/>
</Row>
<VariantAnalysisStats

View File

@@ -2,7 +2,10 @@ import * as React from "react";
import { render as reactRender, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { VariantAnalysisStatus } from "../../../remote-queries/shared/variant-analysis";
import { VariantAnalysisActions } from "../VariantAnalysisActions";
import {
VariantAnalysisActions,
VariantAnalysisActionsProps,
} from "../VariantAnalysisActions";
describe(VariantAnalysisActions.name, () => {
const onStopQueryClick = jest.fn();
@@ -15,51 +18,78 @@ describe(VariantAnalysisActions.name, () => {
onExportResultsClick.mockReset();
});
const render = (variantAnalysisStatus: VariantAnalysisStatus) =>
const render = (
props: Pick<VariantAnalysisActionsProps, "variantAnalysisStatus"> &
Partial<VariantAnalysisActionsProps>,
) =>
reactRender(
<VariantAnalysisActions
variantAnalysisStatus={variantAnalysisStatus}
onStopQueryClick={onStopQueryClick}
onCopyRepositoryListClick={onCopyRepositoryListClick}
onExportResultsClick={onExportResultsClick}
{...props}
/>,
);
it("renders 1 button when in progress", async () => {
const { container } = render(VariantAnalysisStatus.InProgress);
const { container } = render({
variantAnalysisStatus: VariantAnalysisStatus.InProgress,
});
expect(container.querySelectorAll("vscode-button").length).toEqual(1);
});
it("renders the stop query button when in progress", async () => {
render(VariantAnalysisStatus.InProgress);
render({
variantAnalysisStatus: VariantAnalysisStatus.InProgress,
});
await userEvent.click(screen.getByText("Stop query"));
expect(onStopQueryClick).toHaveBeenCalledTimes(1);
});
it("renders 3 buttons when in progress with results", async () => {
const { container } = render({
variantAnalysisStatus: VariantAnalysisStatus.InProgress,
showResultActions: true,
});
expect(container.querySelectorAll("vscode-button").length).toEqual(3);
});
it("renders 2 buttons when succeeded", async () => {
const { container } = render(VariantAnalysisStatus.Succeeded);
const { container } = render({
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
showResultActions: true,
});
expect(container.querySelectorAll("vscode-button").length).toEqual(2);
});
it("renders the copy repository list button when succeeded", async () => {
render(VariantAnalysisStatus.Succeeded);
render({
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
showResultActions: true,
});
await userEvent.click(screen.getByText("Copy repository list"));
expect(onCopyRepositoryListClick).toHaveBeenCalledTimes(1);
});
it("renders the export results button when succeeded", async () => {
render(VariantAnalysisStatus.Succeeded);
render({
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
showResultActions: true,
});
await userEvent.click(screen.getByText("Export results"));
expect(onExportResultsClick).toHaveBeenCalledTimes(1);
});
it("does not render any buttons when failed", () => {
const { container } = render(VariantAnalysisStatus.Failed);
const { container } = render({
variantAnalysisStatus: VariantAnalysisStatus.Failed,
});
expect(container.querySelectorAll("vscode-button").length).toEqual(0);
});

View File

@@ -1,7 +1,10 @@
import { TreeItemCollapsibleState, ThemeIcon } from "vscode";
import { join } from "path";
import { ensureDir, remove, writeJson } from "fs-extra";
import { DbConfig } from "../../../databases/config/db-config";
import {
DbConfig,
SelectedDbItemKind,
} from "../../../databases/config/db-config";
import { DbManager } from "../../../databases/db-manager";
import { DbConfigStore } from "../../../databases/config/db-config-store";
import { DbTreeDataProvider } from "../../../databases/ui/db-tree-data-provider";
@@ -307,6 +310,7 @@ describe("db panel", () => {
dateAdded: 1668428293677,
language: "cpp",
storagePath: "/path/to/db1/",
selected: false,
},
{
kind: DbItemKind.LocalDatabase,
@@ -314,6 +318,7 @@ describe("db panel", () => {
dateAdded: 1668428472731,
language: "cpp",
storagePath: "/path/to/db2/",
selected: false,
},
]);
checkLocalListItem(localListItems[1], "my-list-2", [
@@ -323,6 +328,7 @@ describe("db panel", () => {
dateAdded: 1668428472731,
language: "ruby",
storagePath: "/path/to/db3/",
selected: false,
},
]);
});
@@ -381,6 +387,7 @@ describe("db panel", () => {
dateAdded: 1668428293677,
language: "csharp",
storagePath: "/path/to/db1/",
selected: false,
});
checkLocalDatabaseItem(localDatabaseItems[1], {
kind: DbItemKind.LocalDatabase,
@@ -388,9 +395,134 @@ describe("db panel", () => {
dateAdded: 1668428472731,
language: "go",
storagePath: "/path/to/db2/",
selected: false,
});
});
it("should mark selected remote db list as selected", async () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "my-list-2",
repositories: ["owner2/repo1", "owner2/repo2"],
},
],
owners: [],
repositories: [],
},
local: {
lists: [],
databases: [],
},
},
selected: {
kind: SelectedDbItemKind.RemoteUserDefinedList,
listName: "my-list-2",
},
};
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
const items = dbTreeItems!;
const remoteRootNode = items[0];
expect(remoteRootNode.dbItem).toBeTruthy();
expect(remoteRootNode.dbItem?.kind).toEqual(DbItemKind.RootRemote);
const list1 = remoteRootNode.children.find(
(c) =>
c.dbItem?.kind === DbItemKind.RemoteUserDefinedList &&
c.dbItem?.listName === "my-list-1",
);
const list2 = remoteRootNode.children.find(
(c) =>
c.dbItem?.kind === DbItemKind.RemoteUserDefinedList &&
c.dbItem?.listName === "my-list-2",
);
expect(list1).toBeTruthy();
expect(list2).toBeTruthy();
expect(isTreeViewItemSelectable(list1!)).toBeTruthy();
expect(isTreeViewItemSelected(list2!)).toBeTruthy();
});
it("should mark selected remote db inside list as selected", async () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "my-list-2",
repositories: ["owner1/repo1", "owner2/repo2"],
},
],
owners: [],
repositories: ["owner1/repo1"],
},
local: {
lists: [],
databases: [],
},
},
selected: {
kind: SelectedDbItemKind.RemoteRepository,
repositoryName: "owner1/repo1",
listName: "my-list-2",
},
};
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
const items = dbTreeItems!;
const remoteRootNode = items[0];
expect(remoteRootNode.dbItem).toBeTruthy();
expect(remoteRootNode.dbItem?.kind).toEqual(DbItemKind.RootRemote);
const list2 = remoteRootNode.children.find(
(c) =>
c.dbItem?.kind === DbItemKind.RemoteUserDefinedList &&
c.dbItem?.listName === "my-list-2",
);
expect(list2).toBeTruthy();
const repo1Node = list2?.children.find(
(c) =>
c.dbItem?.kind === DbItemKind.RemoteRepo &&
c.dbItem?.repoFullName === "owner1/repo1",
);
expect(repo1Node).toBeTruthy();
expect(isTreeViewItemSelected(repo1Node!)).toBeTruthy();
const repo2Node = list2?.children.find(
(c) =>
c.dbItem?.kind === DbItemKind.RemoteRepo &&
c.dbItem?.repoFullName === "owner2/repo2",
);
expect(repo2Node).toBeTruthy();
expect(isTreeViewItemSelectable(repo2Node!)).toBeTruthy();
for (const item of remoteRootNode.children) {
expect(isTreeViewItemSelectable(item)).toBeTruthy();
}
});
async function saveDbConfig(dbConfig: DbConfig): Promise<void> {
await writeJson(dbConfigFilePath, dbConfig);
@@ -471,4 +603,18 @@ describe("db panel", () => {
expect(item.iconPath).toEqual(new ThemeIcon("database"));
expect(item.collapsibleState).toBe(TreeItemCollapsibleState.None);
}
function isTreeViewItemSelectable(treeViewItem: DbTreeViewItem) {
return (
treeViewItem.resourceUri === undefined &&
treeViewItem.contextValue === "selectableDbItem"
);
}
function isTreeViewItemSelected(treeViewItem: DbTreeViewItem) {
return (
treeViewItem.resourceUri?.query === "selected=true" &&
treeViewItem.contextValue === undefined
);
}
});

View File

@@ -46,7 +46,7 @@
}
},
"selected": {
"kind": "configDefined",
"value": "path.to.database"
"kind": "remoteUserDefinedList",
"listName": "repoList1"
}
}

View File

@@ -84,8 +84,8 @@ describe("db config store", () => {
storagePath: "/path/to/database/",
});
expect(config.selected).toEqual({
kind: "configDefined",
value: "path.to.database",
kind: "remoteUserDefinedList",
listName: "repoList1",
});
configStore.dispose();

View File

@@ -1,5 +1,13 @@
import { DbConfig } from "../../../src/databases/config/db-config";
import { DbItemKind } from "../../../src/databases/db-item";
import {
DbConfig,
SelectedDbItemKind,
} from "../../../src/databases/config/db-config";
import {
DbItemKind,
isRemoteOwnerDbItem,
isRemoteRepoDbItem,
isRemoteUserDefinedListDbItem,
} from "../../../src/databases/db-item";
import {
createLocalTree,
createRemoteTree,
@@ -29,18 +37,21 @@ describe("db tree creator", () => {
expect(dbTreeRoot.children.length).toBe(3);
expect(dbTreeRoot.children[0]).toEqual({
kind: DbItemKind.RemoteSystemDefinedList,
selected: false,
listName: "top_10",
listDisplayName: "Top 10 repositories",
listDescription: "Top 10 repositories of a language",
});
expect(dbTreeRoot.children[1]).toEqual({
kind: DbItemKind.RemoteSystemDefinedList,
selected: false,
listName: "top_100",
listDisplayName: "Top 100 repositories",
listDescription: "Top 100 repositories of a language",
});
expect(dbTreeRoot.children[2]).toEqual({
kind: DbItemKind.RemoteSystemDefinedList,
selected: false,
listName: "top_1000",
listDisplayName: "Top 1000 repositories",
listDescription: "Top 1000 repositories of a language",
@@ -76,26 +87,30 @@ describe("db tree creator", () => {
expect(dbTreeRoot).toBeTruthy();
expect(dbTreeRoot.kind).toBe(DbItemKind.RootRemote);
const repositoryListNodes = dbTreeRoot.children.filter(
(child) => child.kind === DbItemKind.RemoteUserDefinedList,
isRemoteUserDefinedListDbItem,
);
expect(repositoryListNodes.length).toBe(2);
expect(repositoryListNodes[0]).toEqual({
kind: DbItemKind.RemoteUserDefinedList,
selected: false,
listName: dbConfig.databases.remote.repositoryLists[0].name,
repos: dbConfig.databases.remote.repositoryLists[0].repositories.map(
(repo) => ({
kind: DbItemKind.RemoteRepo,
selected: false,
repoFullName: repo,
}),
),
});
expect(repositoryListNodes[1]).toEqual({
kind: DbItemKind.RemoteUserDefinedList,
selected: false,
listName: dbConfig.databases.remote.repositoryLists[1].name,
repos: dbConfig.databases.remote.repositoryLists[1].repositories.map(
(repo) => ({
kind: DbItemKind.RemoteRepo,
selected: false,
repoFullName: repo,
}),
),
@@ -121,17 +136,17 @@ describe("db tree creator", () => {
expect(dbTreeRoot).toBeTruthy();
expect(dbTreeRoot.kind).toBe(DbItemKind.RootRemote);
const ownerNodes = dbTreeRoot.children.filter(
(child) => child.kind === DbItemKind.RemoteOwner,
);
const ownerNodes = dbTreeRoot.children.filter(isRemoteOwnerDbItem);
expect(ownerNodes.length).toBe(2);
expect(ownerNodes[0]).toEqual({
kind: DbItemKind.RemoteOwner,
selected: false,
ownerName: dbConfig.databases.remote.owners[0],
});
expect(ownerNodes[1]).toEqual({
kind: DbItemKind.RemoteOwner,
selected: false,
ownerName: dbConfig.databases.remote.owners[1],
});
});
@@ -155,25 +170,171 @@ describe("db tree creator", () => {
expect(dbTreeRoot).toBeTruthy();
expect(dbTreeRoot.kind).toBe(DbItemKind.RootRemote);
const repoNodes = dbTreeRoot.children.filter(
(child) => child.kind === DbItemKind.RemoteRepo,
);
const repoNodes = dbTreeRoot.children.filter(isRemoteRepoDbItem);
expect(repoNodes.length).toBe(3);
expect(repoNodes[0]).toEqual({
kind: DbItemKind.RemoteRepo,
selected: false,
repoFullName: dbConfig.databases.remote.repositories[0],
});
expect(repoNodes[1]).toEqual({
kind: DbItemKind.RemoteRepo,
selected: false,
repoFullName: dbConfig.databases.remote.repositories[1],
});
expect(repoNodes[2]).toEqual({
kind: DbItemKind.RemoteRepo,
selected: false,
repoFullName: dbConfig.databases.remote.repositories[2],
});
});
describe("selected db item", () => {
it("should allow selecting a remote user defined list node", () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [
{
name: "my-list-1",
repositories: [
"owner1/repo1",
"owner1/repo2",
"owner2/repo1",
],
},
],
owners: [],
repositories: [],
},
local: {
lists: [],
databases: [],
},
},
selected: {
kind: SelectedDbItemKind.RemoteUserDefinedList,
listName: "my-list-1",
},
};
const dbTreeRoot = createRemoteTree(dbConfig);
expect(dbTreeRoot).toBeTruthy();
expect(dbTreeRoot.kind).toBe(DbItemKind.RootRemote);
const repositoryListNodes = dbTreeRoot.children.filter(
(child) => child.kind === DbItemKind.RemoteUserDefinedList,
);
expect(repositoryListNodes.length).toBe(1);
expect(repositoryListNodes[0].selected).toEqual(true);
});
it("should allow selecting a remote owner node", () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [],
owners: ["owner1", "owner2"],
repositories: [],
},
local: {
lists: [],
databases: [],
},
},
selected: {
kind: SelectedDbItemKind.RemoteOwner,
ownerName: "owner1",
},
};
const dbTreeRoot = createRemoteTree(dbConfig);
expect(dbTreeRoot).toBeTruthy();
expect(dbTreeRoot.kind).toBe(DbItemKind.RootRemote);
const ownerNodes = dbTreeRoot.children.filter(
(child) => child.kind === DbItemKind.RemoteOwner,
);
expect(ownerNodes.length).toBe(2);
expect(ownerNodes[0].selected).toEqual(true);
expect(ownerNodes[1].selected).toEqual(false);
});
it("should allow selecting a remote repo node", () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [],
owners: [],
repositories: ["owner1/repo1", "owner1/repo2"],
},
local: {
lists: [],
databases: [],
},
},
selected: {
kind: SelectedDbItemKind.RemoteRepository,
repositoryName: "owner1/repo2",
},
};
const dbTreeRoot = createRemoteTree(dbConfig);
expect(dbTreeRoot).toBeTruthy();
expect(dbTreeRoot.kind).toBe(DbItemKind.RootRemote);
const repoNodes = dbTreeRoot.children.filter(isRemoteRepoDbItem);
expect(repoNodes.length).toBe(2);
expect(repoNodes[0].selected).toEqual(false);
expect(repoNodes[1].selected).toEqual(true);
});
it("should allow selecting a remote repo in a list", () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1"],
},
],
owners: [],
repositories: ["owner1/repo2"],
},
local: {
lists: [],
databases: [],
},
},
selected: {
kind: SelectedDbItemKind.RemoteRepository,
listName: "my-list-1",
repositoryName: "owner1/repo1",
},
};
const dbTreeRoot = createRemoteTree(dbConfig);
expect(dbTreeRoot).toBeTruthy();
const listNodes = dbTreeRoot.children.filter(
isRemoteUserDefinedListDbItem,
);
expect(listNodes.length).toBe(1);
expect(listNodes[0].selected).toEqual(false);
expect(listNodes[0].repos.length).toBe(1);
expect(listNodes[0].repos[0].repoFullName).toBe("owner1/repo1");
expect(listNodes[0].repos[0].selected).toBe(true);
});
});
});
describe("createLocalTree", () => {
it("should build root node", () => {
const dbConfig: DbConfig = {
@@ -252,9 +413,11 @@ describe("db tree creator", () => {
expect(localListNodes.length).toBe(2);
expect(localListNodes[0]).toEqual({
kind: DbItemKind.LocalList,
selected: false,
listName: dbConfig.databases.local.lists[0].name,
databases: dbConfig.databases.local.lists[0].databases.map((db) => ({
kind: DbItemKind.LocalDatabase,
selected: false,
databaseName: db.name,
dateAdded: db.dateAdded,
language: db.language,
@@ -263,9 +426,11 @@ describe("db tree creator", () => {
});
expect(localListNodes[1]).toEqual({
kind: DbItemKind.LocalList,
selected: false,
listName: dbConfig.databases.local.lists[1].name,
databases: dbConfig.databases.local.lists[1].databases.map((db) => ({
kind: DbItemKind.LocalDatabase,
selected: false,
databaseName: db.name,
dateAdded: db.dateAdded,
language: db.language,
@@ -313,6 +478,7 @@ describe("db tree creator", () => {
expect(localDatabaseNodes.length).toBe(2);
expect(localDatabaseNodes[0]).toEqual({
kind: DbItemKind.LocalDatabase,
selected: false,
databaseName: dbConfig.databases.local.databases[0].name,
dateAdded: dbConfig.databases.local.databases[0].dateAdded,
language: dbConfig.databases.local.databases[0].language,
@@ -320,6 +486,7 @@ describe("db tree creator", () => {
});
expect(localDatabaseNodes[1]).toEqual({
kind: DbItemKind.LocalDatabase,
selected: false,
databaseName: dbConfig.databases.local.databases[1].name,
dateAdded: dbConfig.databases.local.databases[1].dateAdded,
language: dbConfig.databases.local.databases[1].language,

View File

@@ -123,17 +123,92 @@
},
"selected": {
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": ["configDefined", "remoteSystemDefinedList"]
"oneOf": [
{
"properties": {
"kind": {
"type": "string",
"enum": ["localUserDefinedList"]
},
"listName": {
"type": "string"
}
},
"required": ["kind", "listName"],
"additionalProperties": false
},
"value": {
"type": "string"
{
"properties": {
"kind": {
"type": "string",
"enum": ["localDatabase"]
},
"databaseName": {
"type": "string"
},
"listName": {
"type": "string"
}
},
"required": ["kind", "databaseName"],
"additionalProperties": false
},
{
"properties": {
"kind": {
"type": "string",
"enum": ["remoteSystemDefinedList"]
},
"listName": {
"type": "string"
}
},
"required": ["kind", "listName"],
"additionalProperties": false
},
{
"properties": {
"kind": {
"type": "string",
"enum": ["remoteUserDefinedList"]
},
"listName": {
"type": "string"
}
},
"required": ["kind", "listName"],
"additionalProperties": false
},
{
"properties": {
"kind": {
"type": "string",
"enum": ["remoteOwner"]
},
"ownerName": {
"type": "string"
}
},
"required": ["kind", "ownerName"],
"additionalProperties": false
},
{
"properties": {
"kind": {
"type": "string",
"enum": ["remoteRepository"]
},
"repositoryName": {
"type": "string"
},
"listName": {
"type": "string"
}
},
"required": ["kind", "repositoryName"],
"additionalProperties": false
}
},
"required": ["kind", "value"],
"additionalProperties": false
]
}
},
"required": ["databases"],