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; local: LocalDbConfig;
} }
export interface SelectedDbItem { export type SelectedDbItem =
kind: SelectedDbItemKind; | SelectedLocalUserDefinedList
value: string; | SelectedLocalDatabase
} | SelectedRemoteSystemDefinedList
| SelectedRemoteUserDefinedList
| SelectedRemoteOwner
| SelectedRemoteRepository;
export enum SelectedDbItemKind { export enum SelectedDbItemKind {
ConfigDefined = "configDefined", LocalUserDefinedList = "localUserDefinedList",
LocalDatabase = "localDatabase",
RemoteSystemDefinedList = "remoteSystemDefinedList", 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 { export interface RemoteDbConfig {
@@ -70,10 +109,44 @@ export function cloneDbConfig(config: DbConfig): DbConfig {
}, },
}, },
selected: config.selected selected: config.selected
? { ? cloneDbConfigSelectedItem(config.selected)
kind: config.selected.kind,
value: config.selected.value,
}
: undefined, : 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 { export interface LocalListDbItem {
kind: DbItemKind.LocalList; kind: DbItemKind.LocalList;
selected: boolean;
listName: string; listName: string;
databases: LocalDatabaseDbItem[]; databases: LocalDatabaseDbItem[];
} }
export interface LocalDatabaseDbItem { export interface LocalDatabaseDbItem {
kind: DbItemKind.LocalDatabase; kind: DbItemKind.LocalDatabase;
selected: boolean;
databaseName: string; databaseName: string;
dateAdded: number; dateAdded: number;
language: string; language: string;
@@ -51,6 +53,7 @@ export type RemoteDbItem =
export interface RemoteSystemDefinedListDbItem { export interface RemoteSystemDefinedListDbItem {
kind: DbItemKind.RemoteSystemDefinedList; kind: DbItemKind.RemoteSystemDefinedList;
selected: boolean;
listName: string; listName: string;
listDisplayName: string; listDisplayName: string;
listDescription: string; listDescription: string;
@@ -58,16 +61,66 @@ export interface RemoteSystemDefinedListDbItem {
export interface RemoteUserDefinedListDbItem { export interface RemoteUserDefinedListDbItem {
kind: DbItemKind.RemoteUserDefinedList; kind: DbItemKind.RemoteUserDefinedList;
selected: boolean;
listName: string; listName: string;
repos: RemoteRepoDbItem[]; repos: RemoteRepoDbItem[];
} }
export interface RemoteOwnerDbItem { export interface RemoteOwnerDbItem {
kind: DbItemKind.RemoteOwner; kind: DbItemKind.RemoteOwner;
selected: boolean;
ownerName: string; ownerName: string;
} }
export interface RemoteRepoDbItem { export interface RemoteRepoDbItem {
kind: DbItemKind.RemoteRepo; kind: DbItemKind.RemoteRepo;
selected: boolean;
repoFullName: string; 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 { App, AppMode } from "../common/app";
import { isCanary, isNewQueryRunExperienceEnabled } from "../config"; import { isCanary, isNewQueryRunExperienceEnabled } from "../config";
import { extLogger } from "../common"; import { extLogger } from "../common";
@@ -5,6 +6,7 @@ import { DisposableObject } from "../pure/disposable-object";
import { DbConfigStore } from "./config/db-config-store"; import { DbConfigStore } from "./config/db-config-store";
import { DbManager } from "./db-manager"; import { DbManager } from "./db-manager";
import { DbPanel } from "./ui/db-panel"; import { DbPanel } from "./ui/db-panel";
import { DbSelectionDecorationProvider } from "./ui/db-selection-decoration-provider";
export class DbModule extends DisposableObject { export class DbModule extends DisposableObject {
public async initialize(app: App): Promise<void> { public async initialize(app: App): Promise<void> {
@@ -30,6 +32,10 @@ export class DbModule extends DisposableObject {
this.push(dbPanel); this.push(dbPanel);
this.push(dbConfigStore); this.push(dbConfigStore);
const dbSelectionDecorationProvider = new DbSelectionDecorationProvider();
window.registerFileDecorationProvider(dbSelectionDecorationProvider);
} }
} }

View File

@@ -3,6 +3,7 @@ import {
LocalDatabase, LocalDatabase,
LocalList, LocalList,
RemoteRepositoryList, RemoteRepositoryList,
SelectedDbItemKind,
} from "./config/db-config"; } from "./config/db-config";
import { import {
DbItemKind, DbItemKind,
@@ -18,16 +19,20 @@ import {
export function createRemoteTree(dbConfig: DbConfig): RootRemoteDbItem { export function createRemoteTree(dbConfig: DbConfig): RootRemoteDbItem {
const systemDefinedLists = [ const systemDefinedLists = [
createSystemDefinedList(10), createSystemDefinedList(10, dbConfig),
createSystemDefinedList(100), createSystemDefinedList(100, dbConfig),
createSystemDefinedList(1000), createSystemDefinedList(1000, dbConfig),
]; ];
const userDefinedRepoLists = dbConfig.databases.remote.repositoryLists.map( 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 { return {
kind: DbItemKind.RootRemote, kind: DbItemKind.RootRemote,
@@ -41,8 +46,12 @@ export function createRemoteTree(dbConfig: DbConfig): RootRemoteDbItem {
} }
export function createLocalTree(dbConfig: DbConfig): RootLocalDbItem { export function createLocalTree(dbConfig: DbConfig): RootLocalDbItem {
const localLists = dbConfig.databases.local.lists.map(createLocalList); const localLists = dbConfig.databases.local.lists.map((l) =>
const localDbs = dbConfig.databases.local.databases.map(createLocalDb); createLocalList(l, dbConfig),
);
const localDbs = dbConfig.databases.local.databases.map((l) =>
createLocalDb(l, dbConfig),
);
return { return {
kind: DbItemKind.RootLocal, 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 { return {
kind: DbItemKind.RemoteSystemDefinedList, kind: DbItemKind.RemoteSystemDefinedList,
listName: `top_${n}`, listName,
listDisplayName: `Top ${n} repositories`, listDisplayName: `Top ${n} repositories`,
listDescription: `Top ${n} repositories of a language`, listDescription: `Top ${n} repositories of a language`,
selected: !!selected,
}; };
} }
function createUserDefinedList( function createRemoteUserDefinedList(
list: RemoteRepositoryList, list: RemoteRepositoryList,
dbConfig: DbConfig,
): RemoteUserDefinedListDbItem { ): RemoteUserDefinedListDbItem {
const selected =
dbConfig.selected &&
dbConfig.selected.kind === SelectedDbItemKind.RemoteUserDefinedList &&
dbConfig.selected.listName === list.name;
return { return {
kind: DbItemKind.RemoteUserDefinedList, kind: DbItemKind.RemoteUserDefinedList,
listName: list.name, 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 { return {
kind: DbItemKind.RemoteOwner, kind: DbItemKind.RemoteOwner,
ownerName: owner, 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 { return {
kind: DbItemKind.RemoteRepo, kind: DbItemKind.RemoteRepo,
repoFullName: repo, 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 { return {
kind: DbItemKind.LocalList, kind: DbItemKind.LocalList,
listName: list.name, 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 { return {
kind: DbItemKind.LocalDatabase, kind: DbItemKind.LocalDatabase,
databaseName: db.name, databaseName: db.name,
dateAdded: db.dateAdded, dateAdded: db.dateAdded,
language: db.language, language: db.language,
storagePath: db.storagePath, 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 * as vscode from "vscode";
import { import {
DbItem, DbItem,
isSelectableDbItem,
LocalDatabaseDbItem, LocalDatabaseDbItem,
LocalListDbItem, LocalListDbItem,
RemoteOwnerDbItem, RemoteOwnerDbItem,
@@ -28,6 +29,16 @@ export class DbTreeViewItem extends vscode.TreeItem {
public readonly children: DbTreeViewItem[], public readonly children: DbTreeViewItem[],
) { ) {
super(label, collapsibleState); 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 { ensureDir, writeFile } from "fs-extra";
import { import {
window,
commands, commands,
Uri,
ExtensionContext,
workspace,
ViewColumn,
CancellationToken, CancellationToken,
ExtensionContext,
Uri,
ViewColumn,
window,
workspace,
} from "vscode"; } from "vscode";
import { Credentials } from "../authentication"; import { Credentials } from "../authentication";
import { ProgressCallback, UserCancellationException } from "../commandRunner"; import { ProgressCallback, UserCancellationException } from "../commandRunner";
@@ -21,6 +21,7 @@ import {
generateMarkdown, generateMarkdown,
generateVariantAnalysisMarkdown, generateVariantAnalysisMarkdown,
MarkdownFile, MarkdownFile,
RepositorySummary,
} from "./remote-queries-markdown-generation"; } from "./remote-queries-markdown-generation";
import { RemoteQuery } from "./remote-query"; import { RemoteQuery } from "./remote-query";
import { AnalysisResults, sumAnalysesResults } from "./shared/analysis-result"; import { AnalysisResults, sumAnalysesResults } from "./shared/analysis-result";
@@ -30,6 +31,7 @@ import { assertNever } from "../pure/helpers-pure";
import { import {
VariantAnalysis, VariantAnalysis,
VariantAnalysisScannedRepository, VariantAnalysisScannedRepository,
VariantAnalysisScannedRepositoryDownloadStatus,
VariantAnalysisScannedRepositoryResult, VariantAnalysisScannedRepositoryResult,
} from "./shared/variant-analysis"; } from "./shared/variant-analysis";
import { import {
@@ -162,6 +164,10 @@ export async function exportVariantAnalysisResults(
throw new UserCancellationException("Cancelled"); throw new UserCancellationException("Cancelled");
} }
const repoStates = await variantAnalysisManager.getRepoStates(
variantAnalysisId,
);
void extLogger.log( void extLogger.log(
`Exporting variant analysis results for variant analysis with id ${variantAnalysis.id}`, `Exporting variant analysis results for variant analysis with id ${variantAnalysis.id}`,
); );
@@ -197,6 +203,18 @@ export async function exportVariantAnalysisResults(
} }
for (const repo of repositories) { 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) { if (repo.resultCount == 0) {
yield [ yield [
repo, repo,
@@ -268,11 +286,14 @@ export async function exportVariantAnalysisAnalysisResults(
message: "Generating Markdown files", message: "Generating Markdown files",
}); });
const description = buildVariantAnalysisGistDescription(variantAnalysis); const { markdownFiles, summaries } = await generateVariantAnalysisMarkdown(
const markdownFiles = await generateVariantAnalysisMarkdown(
variantAnalysis, variantAnalysis,
analysesResults, analysesResults,
"gist", exportFormat,
);
const description = buildVariantAnalysisGistDescription(
variantAnalysis,
summaries,
); );
await exportResults( await exportResults(
@@ -407,20 +428,16 @@ const buildGistDescription = (
*/ */
const buildVariantAnalysisGistDescription = ( const buildVariantAnalysisGistDescription = (
variantAnalysis: VariantAnalysis, variantAnalysis: VariantAnalysis,
summaries: RepositorySummary[],
) => { ) => {
const resultCount = const resultCount = summaries.reduce(
variantAnalysis.scannedRepos?.reduce( (acc, summary) => acc + (summary.resultCount ?? 0),
(acc, item) => acc + (item.resultCount ?? 0), 0,
0, );
) ?? 0;
const resultLabel = pluralize(resultCount, "result", "results"); const resultLabel = pluralize(resultCount, "result", "results");
const repositoryLabel = variantAnalysis.scannedRepos?.length const repositoryLabel = summaries.length
? `(${pluralize( ? `(${pluralize(summaries.length, "repository", "repositories")})`
variantAnalysis.scannedRepos.length,
"repository",
"repositories",
)})`
: ""; : "";
return `${variantAnalysis.query.name} (${variantAnalysis.query.language}) ${resultLabel} ${repositoryLabel}`; return `${variantAnalysis.query.name} (${variantAnalysis.query.language}) ${resultLabel} ${repositoryLabel}`;
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,10 +47,24 @@ InProgress.args = {
variantAnalysisStatus: VariantAnalysisStatus.InProgress, 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({}); export const Succeeded = Template.bind({});
Succeeded.args = { Succeeded.args = {
...InProgress.args, ...InProgress.args,
variantAnalysisStatus: VariantAnalysisStatus.Succeeded, variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
showResultActions: true,
}; };
export const Failed = Template.bind({}); export const Failed = Template.bind({});

View File

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

View File

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

View File

@@ -6,15 +6,25 @@ import {
getTotalResultCount, getTotalResultCount,
hasRepoScanCompleted, hasRepoScanCompleted,
VariantAnalysis, VariantAnalysis,
VariantAnalysisScannedRepositoryDownloadStatus,
VariantAnalysisScannedRepositoryState,
} from "../../remote-queries/shared/variant-analysis"; } from "../../remote-queries/shared/variant-analysis";
import { QueryDetails } from "./QueryDetails"; import { QueryDetails } from "./QueryDetails";
import { VariantAnalysisActions } from "./VariantAnalysisActions"; import { VariantAnalysisActions } from "./VariantAnalysisActions";
import { VariantAnalysisStats } from "./VariantAnalysisStats"; import { VariantAnalysisStats } from "./VariantAnalysisStats";
import { parseDate } from "../../pure/date"; import { parseDate } from "../../pure/date";
import { basename } from "../common/path"; import { basename } from "../common/path";
import {
defaultFilterSortState,
filterAndSortRepositoriesWithResults,
RepositoriesFilterSortState,
} from "../../pure/variant-analysis-filter-sort";
export type VariantAnalysisHeaderProps = { export type VariantAnalysisHeaderProps = {
variantAnalysis: VariantAnalysis; variantAnalysis: VariantAnalysis;
repositoryStates?: VariantAnalysisScannedRepositoryState[];
filterSortState?: RepositoriesFilterSortState;
selectedRepositoryIds?: number[];
onOpenQueryFileClick: () => void; onOpenQueryFileClick: () => void;
onViewQueryTextClick: () => void; onViewQueryTextClick: () => void;
@@ -40,6 +50,9 @@ const Row = styled.div`
export const VariantAnalysisHeader = ({ export const VariantAnalysisHeader = ({
variantAnalysis, variantAnalysis,
repositoryStates,
filterSortState,
selectedRepositoryIds,
onOpenQueryFileClick, onOpenQueryFileClick,
onViewQueryTextClick, onViewQueryTextClick,
onStopQueryClick, onStopQueryClick,
@@ -62,6 +75,36 @@ export const VariantAnalysisHeader = ({
const hasSkippedRepos = useMemo(() => { const hasSkippedRepos = useMemo(() => {
return getSkippedRepoCount(variantAnalysis.skippedRepos) > 0; return getSkippedRepoCount(variantAnalysis.skippedRepos) > 0;
}, [variantAnalysis.skippedRepos]); }, [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 ( return (
<Container> <Container>
@@ -74,10 +117,13 @@ export const VariantAnalysisHeader = ({
/> />
<VariantAnalysisActions <VariantAnalysisActions
variantAnalysisStatus={variantAnalysis.status} variantAnalysisStatus={variantAnalysis.status}
showResultActions={(resultCount ?? 0) > 0}
onStopQueryClick={onStopQueryClick} onStopQueryClick={onStopQueryClick}
onCopyRepositoryListClick={onCopyRepositoryListClick} onCopyRepositoryListClick={onCopyRepositoryListClick}
onExportResultsClick={onExportResultsClick} onExportResultsClick={onExportResultsClick}
stopQueryDisabled={!variantAnalysis.actionsWorkflowRunId} stopQueryDisabled={!variantAnalysis.actionsWorkflowRunId}
exportResultsDisabled={!hasDownloadedRepos}
copyRepositoryListDisabled={!hasReposWithResults}
/> />
</Row> </Row>
<VariantAnalysisStats <VariantAnalysisStats

View File

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

View File

@@ -1,7 +1,10 @@
import { TreeItemCollapsibleState, ThemeIcon } from "vscode"; import { TreeItemCollapsibleState, ThemeIcon } from "vscode";
import { join } from "path"; import { join } from "path";
import { ensureDir, remove, writeJson } from "fs-extra"; 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 { DbManager } from "../../../databases/db-manager";
import { DbConfigStore } from "../../../databases/config/db-config-store"; import { DbConfigStore } from "../../../databases/config/db-config-store";
import { DbTreeDataProvider } from "../../../databases/ui/db-tree-data-provider"; import { DbTreeDataProvider } from "../../../databases/ui/db-tree-data-provider";
@@ -307,6 +310,7 @@ describe("db panel", () => {
dateAdded: 1668428293677, dateAdded: 1668428293677,
language: "cpp", language: "cpp",
storagePath: "/path/to/db1/", storagePath: "/path/to/db1/",
selected: false,
}, },
{ {
kind: DbItemKind.LocalDatabase, kind: DbItemKind.LocalDatabase,
@@ -314,6 +318,7 @@ describe("db panel", () => {
dateAdded: 1668428472731, dateAdded: 1668428472731,
language: "cpp", language: "cpp",
storagePath: "/path/to/db2/", storagePath: "/path/to/db2/",
selected: false,
}, },
]); ]);
checkLocalListItem(localListItems[1], "my-list-2", [ checkLocalListItem(localListItems[1], "my-list-2", [
@@ -323,6 +328,7 @@ describe("db panel", () => {
dateAdded: 1668428472731, dateAdded: 1668428472731,
language: "ruby", language: "ruby",
storagePath: "/path/to/db3/", storagePath: "/path/to/db3/",
selected: false,
}, },
]); ]);
}); });
@@ -381,6 +387,7 @@ describe("db panel", () => {
dateAdded: 1668428293677, dateAdded: 1668428293677,
language: "csharp", language: "csharp",
storagePath: "/path/to/db1/", storagePath: "/path/to/db1/",
selected: false,
}); });
checkLocalDatabaseItem(localDatabaseItems[1], { checkLocalDatabaseItem(localDatabaseItems[1], {
kind: DbItemKind.LocalDatabase, kind: DbItemKind.LocalDatabase,
@@ -388,9 +395,134 @@ describe("db panel", () => {
dateAdded: 1668428472731, dateAdded: 1668428472731,
language: "go", language: "go",
storagePath: "/path/to/db2/", 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> { async function saveDbConfig(dbConfig: DbConfig): Promise<void> {
await writeJson(dbConfigFilePath, dbConfig); await writeJson(dbConfigFilePath, dbConfig);
@@ -471,4 +603,18 @@ describe("db panel", () => {
expect(item.iconPath).toEqual(new ThemeIcon("database")); expect(item.iconPath).toEqual(new ThemeIcon("database"));
expect(item.collapsibleState).toBe(TreeItemCollapsibleState.None); 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": { "selected": {
"kind": "configDefined", "kind": "remoteUserDefinedList",
"value": "path.to.database" "listName": "repoList1"
} }
} }

View File

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

View File

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

View File

@@ -123,17 +123,92 @@
}, },
"selected": { "selected": {
"type": "object", "type": "object",
"properties": { "oneOf": [
"kind": { {
"type": "string", "properties": {
"enum": ["configDefined", "remoteSystemDefinedList"] "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"], "required": ["databases"],