Merge pull request #2023 from github/koesie10/db-panel-controller-repo-welcome

Show welcome view when controller repo is not setup
This commit is contained in:
Koen Vlaswinkel
2023-01-31 17:01:00 +01:00
committed by GitHub
11 changed files with 397 additions and 244 deletions

View File

@@ -1315,6 +1315,11 @@
{
"view": "codeQLEvalLogViewer",
"contents": "Run the 'Show Evaluator Log (UI)' command on a CodeQL query run in the Query History view."
},
{
"view": "codeQLVariantAnalysisRepositories",
"contents": "Set up a controller repository to start using variant analysis.\n[Set up controller repository](command:codeQLVariantAnalysisRepositories.setupControllerRepository)",
"when": "!config.codeQL.variantAnalysis.controllerRepo"
}
]
},

View File

@@ -539,6 +539,27 @@ export async function setRemoteControllerRepo(repo: string | undefined) {
await REMOTE_CONTROLLER_REPO.updateValue(repo, ConfigurationTarget.Global);
}
export interface VariantAnalysisConfig {
controllerRepo: string | undefined;
onDidChangeConfiguration?: Event<void>;
}
export class VariantAnalysisConfigListener
extends ConfigListener
implements VariantAnalysisConfig
{
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
this.handleDidChangeConfigurationForRelevantSettings(
[VARIANT_ANALYSIS_SETTING],
e,
);
}
public get controllerRepo(): string | undefined {
return getRemoteControllerRepo();
}
}
/**
* The branch of "github/codeql-variant-analysis-action" to use with the "Run Variant Analysis" command.
* Default value is "main".

View File

@@ -24,7 +24,7 @@ export class DbModule extends DisposableObject {
const dbModule = new DbModule(app);
app.subscriptions.push(dbModule);
await dbModule.initialize();
await dbModule.initialize(app);
return dbModule;
}
@@ -39,12 +39,12 @@ export class DbModule extends DisposableObject {
return isCanary() && isVariantAnalysisReposPanelEnabled();
}
private async initialize(): Promise<void> {
private async initialize(app: App): Promise<void> {
void extLogger.log("Initializing database module");
await this.dbConfigStore.initialize();
const dbPanel = new DbPanel(this.dbManager);
const dbPanel = new DbPanel(this.dbManager, app.credentials);
await dbPanel.initialize();
this.push(dbPanel);

View File

@@ -29,6 +29,9 @@ import { DbManager } from "../db-manager";
import { DbTreeDataProvider } from "./db-tree-data-provider";
import { DbTreeViewItem } from "./db-tree-view-item";
import { getGitHubUrl } from "./db-tree-view-item-action";
import { getControllerRepo } from "../../remote-queries/run-remote-query";
import { getErrorMessage } from "../../pure/helpers-pure";
import { Credentials } from "../../common/authentication";
export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
kind: string;
@@ -42,7 +45,10 @@ export class DbPanel extends DisposableObject {
private readonly dataProvider: DbTreeDataProvider;
private readonly treeView: TreeView<DbTreeViewItem>;
public constructor(private readonly dbManager: DbManager) {
public constructor(
private readonly dbManager: DbManager,
private readonly credentials: Credentials,
) {
super();
this.dataProvider = new DbTreeDataProvider(dbManager);
@@ -112,6 +118,12 @@ export class DbPanel extends DisposableObject {
(treeViewItem: DbTreeViewItem) => this.removeItem(treeViewItem),
),
);
this.push(
commandRunner(
"codeQLVariantAnalysisRepositories.setupControllerRepository",
() => this.setupControllerRepository(),
),
);
}
private async openConfigFile(): Promise<void> {
@@ -383,4 +395,21 @@ export class DbPanel extends DisposableObject {
await commands.executeCommand("vscode.open", Uri.parse(githubUrl));
}
private async setupControllerRepository(): Promise<void> {
try {
// This will also validate that the controller repository is valid
await getControllerRepo(this.credentials);
} catch (e: unknown) {
if (e instanceof UserCancellationException) {
return;
}
void showAndLogErrorMessage(
`An error occurred while setting up the controller repository: ${getErrorMessage(
e,
)}`,
);
}
}
}

View File

@@ -13,6 +13,7 @@ import {
DbConfigValidationError,
DbConfigValidationErrorKind,
} from "../db-validation-errors";
import { VariantAnalysisConfigListener } from "../../config";
export class DbTreeDataProvider
extends DisposableObject
@@ -27,8 +28,17 @@ export class DbTreeDataProvider
);
private dbTreeItems: DbTreeViewItem[];
private variantAnalysisConfig: VariantAnalysisConfigListener;
public constructor(private readonly dbManager: DbManager) {
super();
this.variantAnalysisConfig = this.push(new VariantAnalysisConfigListener());
this.variantAnalysisConfig.onDidChangeConfiguration(() => {
this.dbTreeItems = this.createTree();
this._onDidChangeTreeData.fire(undefined);
});
this.dbTreeItems = this.createTree();
this.onDidChangeTreeData = this._onDidChangeTreeData.event;
@@ -62,6 +72,11 @@ export class DbTreeDataProvider
}
private createTree(): DbTreeViewItem[] {
// Returning an empty tree here will show the welcome view
if (!this.variantAnalysisConfig.controllerRepo) {
return [];
}
const dbItemsResult = this.dbManager.getDbItems();
if (dbItemsResult.isFailure) {

View File

@@ -376,10 +376,10 @@ export async function getControllerRepo(
);
controllerRepoNwo = await window.showInputBox({
title:
"Controller repository in which to run the GitHub Actions workflow for this variant analysis",
"Controller repository in which to run GitHub Actions workflows for variant analyses",
placeHolder: "<owner>/<repo>",
prompt:
"Enter the name of a GitHub repository in the format <owner>/<repo>",
"Enter the name of a GitHub repository in the format <owner>/<repo>. You can change this in the extension settings.",
ignoreFocusOut: true,
});
if (!controllerRepoNwo) {

View File

@@ -12,6 +12,7 @@ import {
import { CodeQLExtensionInterface } from "../../../../src/extension";
import { MockGitHubApiServer } from "../../../../src/mocks/mock-gh-api-server";
import { mockConfiguration } from "../../utils/configuration-helpers";
jest.setTimeout(30_000);
@@ -35,50 +36,17 @@ describe("Variant Analysis Submission Integration", () => {
let showErrorMessageSpy: jest.SpiedFunction<typeof window.showErrorMessage>;
beforeEach(async () => {
const originalGetConfiguration = workspace.getConfiguration;
jest
.spyOn(workspace, "getConfiguration")
.mockImplementation((section, scope) => {
const configuration = originalGetConfiguration(section, scope);
return {
get(key: string, defaultValue?: unknown) {
if (section === "codeQL.variantAnalysis" && key === "liveResults") {
return true;
}
if (section === "codeQL" && key == "canary") {
return true;
}
if (
section === "codeQL.variantAnalysis" &&
key === "controllerRepo"
) {
return "github/vscode-codeql";
}
return configuration.get(key, defaultValue);
},
has(key: string) {
return configuration.has(key);
},
inspect(key: string) {
return configuration.inspect(key);
},
update(
key: string,
value: unknown,
configurationTarget?: boolean,
overrideInLanguage?: boolean,
) {
return configuration.update(
key,
value,
configurationTarget,
overrideInLanguage,
);
},
};
});
mockConfiguration({
values: {
codeQL: {
canary: true,
},
"codeQL.variantAnalysis": {
liveResults: true,
controllerRepo: "github/vscode-codeql",
},
},
});
jest.spyOn(authentication, "getSession").mockResolvedValue({
id: "test",

View File

@@ -13,6 +13,7 @@ import { DbTreeViewItem } from "../../../../src/databases/ui/db-tree-view-item";
import { ExtensionApp } from "../../../../src/common/vscode/vscode-app";
import { createMockExtensionContext } from "../../../factories/extension-context";
import { createDbConfig } from "../../../factories/db-config-factories";
import { mockConfiguration } from "../../utils/configuration-helpers";
describe("db panel rendering nodes", () => {
const workspaceStoragePath = join(__dirname, "test-workspace-storage");
@@ -48,238 +49,281 @@ describe("db panel rendering nodes", () => {
await remove(workspaceStoragePath);
});
it("should render default remote nodes when the config is empty", async () => {
const dbConfig: DbConfig = createDbConfig();
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
const items = dbTreeItems!;
expect(items.length).toBe(3);
checkRemoteSystemDefinedListItem(items[0], 10);
checkRemoteSystemDefinedListItem(items[1], 100);
checkRemoteSystemDefinedListItem(items[2], 1000);
});
it("should render remote repository list nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
describe("when controller repo is not set", () => {
mockConfiguration({
values: {
"codeQL.variantAnalysis": {
controllerRepo: undefined,
},
{
name: "my-list-2",
repositories: ["owner1/repo1", "owner2/repo1", "owner2/repo2"],
},
],
},
});
await saveDbConfig(dbConfig);
it("should not have any items", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "my-list-2",
repositories: ["owner2/repo1", "owner2/repo2"],
},
],
});
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
await saveDbConfig(dbConfig);
const systemDefinedListItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteSystemDefinedList,
);
expect(systemDefinedListItems.length).toBe(3);
const dbTreeItems = await dbTreeDataProvider.getChildren();
const userDefinedListItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteUserDefinedList,
);
expect(userDefinedListItems.length).toBe(2);
checkUserDefinedListItem(userDefinedListItems[0], "my-list-1", [
"owner1/repo1",
"owner1/repo2",
]);
checkUserDefinedListItem(userDefinedListItems[1], "my-list-2", [
"owner1/repo1",
"owner2/repo1",
"owner2/repo2",
]);
expect(dbTreeItems).toHaveLength(0);
});
});
it("should render owner list nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteOwners: ["owner1", "owner2"],
describe("when controller repo is set", () => {
beforeEach(() => {
mockConfiguration({
values: {
"codeQL.variantAnalysis": {
controllerRepo: "github/codeql",
},
},
});
});
await saveDbConfig(dbConfig);
it("should render default remote nodes when the config is empty", async () => {
const dbConfig: DbConfig = createDbConfig();
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
expect(dbTreeItems?.length).toBe(5);
await saveDbConfig(dbConfig);
const ownerListItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteOwner,
);
expect(ownerListItems.length).toBe(2);
checkOwnerItem(ownerListItems[0], "owner1");
checkOwnerItem(ownerListItems[1], "owner2");
});
const dbTreeItems = await dbTreeDataProvider.getChildren();
it("should render repository nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteRepos: ["owner1/repo1", "owner1/repo2"],
expect(dbTreeItems).toBeTruthy();
const items = dbTreeItems!;
expect(items.length).toBe(3);
checkRemoteSystemDefinedListItem(items[0], 10);
checkRemoteSystemDefinedListItem(items[1], 100);
checkRemoteSystemDefinedListItem(items[2], 1000);
});
await saveDbConfig(dbConfig);
it("should render remote repository list nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "my-list-2",
repositories: ["owner1/repo1", "owner2/repo1", "owner2/repo2"],
},
],
});
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
expect(dbTreeItems!.length).toBe(5);
await saveDbConfig(dbConfig);
const repoItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteRepo,
);
expect(repoItems.length).toBe(2);
checkRemoteRepoItem(repoItems[0], "owner1/repo1");
checkRemoteRepoItem(repoItems[1], "owner1/repo2");
});
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
it.skip("should render local list nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
localLists: [
const systemDefinedListItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteSystemDefinedList,
);
expect(systemDefinedListItems.length).toBe(3);
const userDefinedListItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteUserDefinedList,
);
expect(userDefinedListItems.length).toBe(2);
checkUserDefinedListItem(userDefinedListItems[0], "my-list-1", [
"owner1/repo1",
"owner1/repo2",
]);
checkUserDefinedListItem(userDefinedListItems[1], "my-list-2", [
"owner1/repo1",
"owner2/repo1",
"owner2/repo2",
]);
});
it("should render owner list nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteOwners: ["owner1", "owner2"],
});
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
expect(dbTreeItems?.length).toBe(5);
const ownerListItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteOwner,
);
expect(ownerListItems.length).toBe(2);
checkOwnerItem(ownerListItems[0], "owner1");
checkOwnerItem(ownerListItems[1], "owner2");
});
it("should render repository nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteRepos: ["owner1/repo1", "owner1/repo2"],
});
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
expect(dbTreeItems!.length).toBe(5);
const repoItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteRepo,
);
expect(repoItems.length).toBe(2);
checkRemoteRepoItem(repoItems[0], "owner1/repo1");
checkRemoteRepoItem(repoItems[1], "owner1/repo2");
});
it.skip("should render local list nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
localLists: [
{
name: "my-list-1",
databases: [
{
name: "db1",
dateAdded: 1668428293677,
language: "cpp",
storagePath: "/path/to/db1/",
},
{
name: "db2",
dateAdded: 1668428472731,
language: "cpp",
storagePath: "/path/to/db2/",
},
],
},
{
name: "my-list-2",
databases: [
{
name: "db3",
dateAdded: 1668428472731,
language: "ruby",
storagePath: "/path/to/db3/",
},
],
},
],
});
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
const localRootNode = dbTreeItems?.find(
(i) => i.dbItem?.kind === DbItemKind.RootLocal,
);
expect(localRootNode).toBeTruthy();
expect(localRootNode!.dbItem).toBeTruthy();
expect(localRootNode!.collapsibleState).toBe(
TreeItemCollapsibleState.Collapsed,
);
expect(localRootNode!.children).toBeTruthy();
expect(localRootNode!.children.length).toBe(2);
const localListItems = localRootNode!.children.filter(
(item) => item.dbItem?.kind === DbItemKind.LocalList,
);
expect(localListItems.length).toBe(2);
checkLocalListItem(localListItems[0], "my-list-1", [
{
name: "my-list-1",
databases: [
{
name: "db1",
dateAdded: 1668428293677,
language: "cpp",
storagePath: "/path/to/db1/",
},
{
name: "db2",
dateAdded: 1668428472731,
language: "cpp",
storagePath: "/path/to/db2/",
},
],
kind: DbItemKind.LocalDatabase,
databaseName: "db1",
dateAdded: 1668428293677,
language: "cpp",
storagePath: "/path/to/db1/",
selected: false,
},
{
name: "my-list-2",
databases: [
{
name: "db3",
dateAdded: 1668428472731,
language: "ruby",
storagePath: "/path/to/db3/",
},
],
kind: DbItemKind.LocalDatabase,
databaseName: "db2",
dateAdded: 1668428472731,
language: "cpp",
storagePath: "/path/to/db2/",
selected: false,
},
],
]);
checkLocalListItem(localListItems[1], "my-list-2", [
{
kind: DbItemKind.LocalDatabase,
databaseName: "db3",
dateAdded: 1668428472731,
language: "ruby",
storagePath: "/path/to/db3/",
selected: false,
},
]);
});
await saveDbConfig(dbConfig);
it.skip("should render local database nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
localDbs: [
{
name: "db1",
dateAdded: 1668428293677,
language: "csharp",
storagePath: "/path/to/db1/",
},
{
name: "db2",
dateAdded: 1668428472731,
language: "go",
storagePath: "/path/to/db2/",
},
],
});
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
await saveDbConfig(dbConfig);
const localRootNode = dbTreeItems?.find(
(i) => i.dbItem?.kind === DbItemKind.RootLocal,
);
expect(localRootNode).toBeTruthy();
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(localRootNode!.dbItem).toBeTruthy();
expect(localRootNode!.collapsibleState).toBe(
TreeItemCollapsibleState.Collapsed,
);
expect(localRootNode!.children).toBeTruthy();
expect(localRootNode!.children.length).toBe(2);
expect(dbTreeItems).toBeTruthy();
const localRootNode = dbTreeItems?.find(
(i) => i.dbItem?.kind === DbItemKind.RootLocal,
);
expect(localRootNode).toBeTruthy();
const localListItems = localRootNode!.children.filter(
(item) => item.dbItem?.kind === DbItemKind.LocalList,
);
expect(localListItems.length).toBe(2);
checkLocalListItem(localListItems[0], "my-list-1", [
{
expect(localRootNode!.dbItem).toBeTruthy();
expect(localRootNode!.collapsibleState).toBe(
TreeItemCollapsibleState.Collapsed,
);
expect(localRootNode!.children).toBeTruthy();
expect(localRootNode!.children.length).toBe(2);
const localDatabaseItems = localRootNode!.children.filter(
(item) => item.dbItem?.kind === DbItemKind.LocalDatabase,
);
expect(localDatabaseItems.length).toBe(2);
checkLocalDatabaseItem(localDatabaseItems[0], {
kind: DbItemKind.LocalDatabase,
databaseName: "db1",
dateAdded: 1668428293677,
language: "cpp",
language: "csharp",
storagePath: "/path/to/db1/",
selected: false,
},
{
});
checkLocalDatabaseItem(localDatabaseItems[1], {
kind: DbItemKind.LocalDatabase,
databaseName: "db2",
dateAdded: 1668428472731,
language: "cpp",
language: "go",
storagePath: "/path/to/db2/",
selected: false,
},
]);
checkLocalListItem(localListItems[1], "my-list-2", [
{
kind: DbItemKind.LocalDatabase,
databaseName: "db3",
dateAdded: 1668428472731,
language: "ruby",
storagePath: "/path/to/db3/",
selected: false,
},
]);
});
it.skip("should render local database nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
localDbs: [
{
name: "db1",
dateAdded: 1668428293677,
language: "csharp",
storagePath: "/path/to/db1/",
},
{
name: "db2",
dateAdded: 1668428472731,
language: "go",
storagePath: "/path/to/db2/",
},
],
});
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
const localRootNode = dbTreeItems?.find(
(i) => i.dbItem?.kind === DbItemKind.RootLocal,
);
expect(localRootNode).toBeTruthy();
expect(localRootNode!.dbItem).toBeTruthy();
expect(localRootNode!.collapsibleState).toBe(
TreeItemCollapsibleState.Collapsed,
);
expect(localRootNode!.children).toBeTruthy();
expect(localRootNode!.children.length).toBe(2);
const localDatabaseItems = localRootNode!.children.filter(
(item) => item.dbItem?.kind === DbItemKind.LocalDatabase,
);
expect(localDatabaseItems.length).toBe(2);
checkLocalDatabaseItem(localDatabaseItems[0], {
kind: DbItemKind.LocalDatabase,
databaseName: "db1",
dateAdded: 1668428293677,
language: "csharp",
storagePath: "/path/to/db1/",
selected: false,
});
checkLocalDatabaseItem(localDatabaseItems[1], {
kind: DbItemKind.LocalDatabase,
databaseName: "db2",
dateAdded: 1668428472731,
language: "go",
storagePath: "/path/to/db2/",
selected: false,
});
});
});

View File

@@ -9,6 +9,7 @@ import { DbTreeViewItem } from "../../../../src/databases/ui/db-tree-view-item";
import { ExtensionApp } from "../../../../src/common/vscode/vscode-app";
import { createMockExtensionContext } from "../../../factories/extension-context";
import { createDbConfig } from "../../../factories/db-config-factories";
import { mockConfiguration } from "../../utils/configuration-helpers";
describe("db panel", () => {
const workspaceStoragePath = join(__dirname, "test-workspace-storage");
@@ -38,6 +39,14 @@ describe("db panel", () => {
beforeEach(async () => {
await ensureDir(workspaceStoragePath);
mockConfiguration({
values: {
"codeQL.variantAnalysis": {
controllerRepo: "github/codeql",
},
},
});
});
afterEach(async () => {

View File

@@ -15,6 +15,7 @@ import {
import { ExtensionApp } from "../../../../src/common/vscode/vscode-app";
import { createMockExtensionContext } from "../../../factories/extension-context";
import { createDbConfig } from "../../../factories/db-config-factories";
import { mockConfiguration } from "../../utils/configuration-helpers";
describe("db panel selection", () => {
const workspaceStoragePath = join(__dirname, "test-workspace-storage");
@@ -44,6 +45,14 @@ describe("db panel selection", () => {
beforeEach(async () => {
await ensureDir(workspaceStoragePath);
mockConfiguration({
values: {
"codeQL.variantAnalysis": {
controllerRepo: "github/codeql",
},
},
});
});
afterEach(async () => {

View File

@@ -0,0 +1,53 @@
import { workspace } from "vscode";
type MockConfigurationConfig = {
values: {
[section: string]: {
[scope: string]: any | (() => any);
};
};
};
export function mockConfiguration(config: MockConfigurationConfig) {
const originalGetConfiguration = workspace.getConfiguration;
jest
.spyOn(workspace, "getConfiguration")
.mockImplementation((section, scope) => {
const configuration = originalGetConfiguration(section, scope);
return {
get(key: string, defaultValue?: unknown) {
if (
section &&
config.values[section] &&
config.values[section][key]
) {
const value = config.values[section][key];
return typeof value === "function" ? value() : value;
}
return configuration.get(key, defaultValue);
},
has(key: string) {
return configuration.has(key);
},
inspect(key: string) {
return configuration.inspect(key);
},
update(
key: string,
value: unknown,
configurationTarget?: boolean,
overrideInLanguage?: boolean,
) {
return configuration.update(
key,
value,
configurationTarget,
overrideInLanguage,
);
},
};
});
}