Merge remote-tracking branch 'origin/main' into koesie10/export-progress
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
const resultCount = summaries.reduce(
|
||||
(acc, summary) => acc + (summary.resultCount ?? 0),
|
||||
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}`;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
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,
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface VariantAnalysisViewManager<
|
||||
> {
|
||||
registerView(view: T): void;
|
||||
unregisterView(view: T): void;
|
||||
getView(variantAnalysisId: number): T | undefined;
|
||||
|
||||
getVariantAnalysis(
|
||||
variantAnalysisId: number,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -43,6 +43,7 @@ const variantAnalysis: VariantAnalysisDomainModel = {
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
resultCount: 100,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -144,6 +144,9 @@ export function VariantAnalysis({
|
||||
<>
|
||||
<VariantAnalysisHeader
|
||||
variantAnalysis={variantAnalysis}
|
||||
repositoryStates={repoStates}
|
||||
filterSortState={filterSortState}
|
||||
selectedRepositoryIds={selectedRepositoryIds}
|
||||
onOpenQueryFileClick={openQueryFile}
|
||||
onViewQueryTextClick={openQueryText}
|
||||
onStopQueryClick={stopQuery}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
}
|
||||
},
|
||||
"selected": {
|
||||
"kind": "configDefined",
|
||||
"value": "path.to.database"
|
||||
"kind": "remoteUserDefinedList",
|
||||
"listName": "repoList1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -123,17 +123,92 @@
|
||||
},
|
||||
"selected": {
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["configDefined", "remoteSystemDefinedList"]
|
||||
"enum": ["localUserDefinedList"]
|
||||
},
|
||||
"value": {
|
||||
"listName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["kind", "value"],
|
||||
"required": ["kind", "listName"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"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": ["databases"],
|
||||
|
||||
Reference in New Issue
Block a user