Merge pull request #2359 from github/yer-a-ternary-choice-query
Add configuration option to turn off skeleton pack generation
This commit is contained in:
@@ -371,11 +371,23 @@
|
||||
"default": false,
|
||||
"description": "Allow database to be downloaded via HTTP. Warning: enabling this option will allow downloading from insecure servers."
|
||||
},
|
||||
"codeQL.createQuery.folder": {
|
||||
"codeQL.createQuery.qlPackLocation": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"patternErrorMessage": "Please enter a valid folder",
|
||||
"markdownDescription": "The name of the folder where we want to create queries and query packs via the \"CodeQL: Create Query\" command. The folder should exist."
|
||||
"markdownDescription": "The name of the folder where we want to create queries and QL packs via the \"CodeQL: Create Query\" command. The folder should exist."
|
||||
},
|
||||
"codeQL.createQuery.autogenerateQlPacks": {
|
||||
"type": "string",
|
||||
"default": "ask",
|
||||
"enum": [
|
||||
"ask",
|
||||
"never"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Ask to create a QL pack when a new CodeQL database is added.",
|
||||
"Never create a QL pack when a new CodeQL database is added."
|
||||
],
|
||||
"description": "Ask the user to generate a QL pack when a new CodeQL database is downloaded."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -666,17 +666,39 @@ export function allowHttp(): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the folder where we want to create skeleton wizard QL packs.
|
||||
* Parent setting for all settings related to the "Create Query" command.
|
||||
*/
|
||||
const CREATE_QUERY_COMMAND = new Setting("createQuery", ROOT_SETTING);
|
||||
|
||||
/**
|
||||
* The name of the folder where we want to create QL packs.
|
||||
**/
|
||||
const SKELETON_WIZARD_FOLDER = new Setting(
|
||||
"folder",
|
||||
new Setting("createQuery", ROOT_SETTING),
|
||||
const QL_PACK_LOCATION = new Setting("qlPackLocation", CREATE_QUERY_COMMAND);
|
||||
|
||||
export function getQlPackLocation(): string | undefined {
|
||||
return QL_PACK_LOCATION.getValue<string>() || undefined;
|
||||
}
|
||||
|
||||
export async function setQlPackLocation(folder: string | undefined) {
|
||||
await QL_PACK_LOCATION.updateValue(folder, ConfigurationTarget.Global);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to ask the user to autogenerate a QL pack. The options are "ask" and "never".
|
||||
**/
|
||||
const AUTOGENERATE_QL_PACKS = new Setting(
|
||||
"autogenerateQlPacks",
|
||||
CREATE_QUERY_COMMAND,
|
||||
);
|
||||
|
||||
export function getSkeletonWizardFolder(): string | undefined {
|
||||
return SKELETON_WIZARD_FOLDER.getValue<string>() || undefined;
|
||||
const AutogenerateQLPacksValues = ["ask", "never"] as const;
|
||||
type AutogenerateQLPacks = typeof AutogenerateQLPacksValues[number];
|
||||
|
||||
export function getAutogenerateQlPacks(): AutogenerateQLPacks {
|
||||
const value = AUTOGENERATE_QL_PACKS.getValue<AutogenerateQLPacks>();
|
||||
return AutogenerateQLPacksValues.includes(value) ? value : "ask";
|
||||
}
|
||||
|
||||
export async function setSkeletonWizardFolder(folder: string | undefined) {
|
||||
await SKELETON_WIZARD_FOLDER.updateValue(folder, ConfigurationTarget.Global);
|
||||
export async function setAutogenerateQlPacks(choice: AutogenerateQLPacks) {
|
||||
await AUTOGENERATE_QL_PACKS.updateValue(choice, ConfigurationTarget.Global);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
isLikelyDatabaseRoot,
|
||||
showAndLogExceptionWithTelemetry,
|
||||
isFolderAlreadyInWorkspace,
|
||||
showBinaryChoiceDialog,
|
||||
getFirstWorkspaceFolder,
|
||||
showNeverAskAgainDialog,
|
||||
} from "../helpers";
|
||||
import { ProgressCallback, withProgress } from "../common/vscode/progress";
|
||||
import {
|
||||
@@ -26,7 +26,11 @@ import { asError, getErrorMessage } from "../pure/helpers-pure";
|
||||
import { QueryRunner } from "../query-server";
|
||||
import { pathsEqual } from "../pure/files";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { isCodespacesTemplate } from "../config";
|
||||
import {
|
||||
getAutogenerateQlPacks,
|
||||
isCodespacesTemplate,
|
||||
setAutogenerateQlPacks,
|
||||
} from "../config";
|
||||
import { QlPackGenerator } from "../qlpack-generator";
|
||||
import { QueryLanguage } from "../common/query-language";
|
||||
import { App } from "../common/app";
|
||||
@@ -745,11 +749,20 @@ export class DatabaseManager extends DisposableObject {
|
||||
return;
|
||||
}
|
||||
|
||||
const answer = await showBinaryChoiceDialog(
|
||||
if (getAutogenerateQlPacks() === "never") {
|
||||
return;
|
||||
}
|
||||
|
||||
const answer = await showNeverAskAgainDialog(
|
||||
`We've noticed you don't have a CodeQL pack available to analyze this database. Can we set up a query pack for you?`,
|
||||
);
|
||||
|
||||
if (!answer) {
|
||||
if (answer === "No") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (answer === "No, and never ask me again") {
|
||||
await setAutogenerateQlPacks("never");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -256,6 +256,46 @@ export function isWorkspaceFolderOnDisk(
|
||||
return workspaceFolder.uri.scheme === "file";
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a modal dialog for the user to make a choice between yes/no/never be asked again.
|
||||
*
|
||||
* @param message The message to show.
|
||||
* @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can
|
||||
* be closed even if the user does not make a choice.
|
||||
* @param yesTitle The text in the box indicating the affirmative choice.
|
||||
* @param noTitle The text in the box indicating the negative choice.
|
||||
* @param neverTitle The text in the box indicating the opt out choice.
|
||||
*
|
||||
* @return
|
||||
* `Yes` if the user clicks 'Yes',
|
||||
* `No` if the user clicks 'No' or cancels the dialog,
|
||||
* `No, and never ask me again` if the user clicks 'No, and never ask me again',
|
||||
* `undefined` if the dialog is closed without the user making a choice.
|
||||
*/
|
||||
export async function showNeverAskAgainDialog(
|
||||
message: string,
|
||||
modal = true,
|
||||
yesTitle = "Yes",
|
||||
noTitle = "No",
|
||||
neverAskAgainTitle = "No, and never ask me again",
|
||||
): Promise<string | undefined> {
|
||||
const yesItem = { title: yesTitle, isCloseAffordance: true };
|
||||
const noItem = { title: noTitle, isCloseAffordance: false };
|
||||
const neverAskAgainItem = {
|
||||
title: neverAskAgainTitle,
|
||||
isCloseAffordance: false,
|
||||
};
|
||||
const chosenItem = await Window.showInformationMessage(
|
||||
message,
|
||||
{ modal },
|
||||
yesItem,
|
||||
noItem,
|
||||
neverAskAgainItem,
|
||||
);
|
||||
|
||||
return chosenItem?.title;
|
||||
}
|
||||
|
||||
/** Gets all active workspace folders that are on the filesystem. */
|
||||
export function getOnDiskWorkspaceFoldersObjects() {
|
||||
const workspaceFolders = workspace.workspaceFolders ?? [];
|
||||
|
||||
@@ -21,9 +21,9 @@ import {
|
||||
downloadGitHubDatabase,
|
||||
} from "./databases/database-fetcher";
|
||||
import {
|
||||
getSkeletonWizardFolder,
|
||||
getQlPackLocation,
|
||||
isCodespacesTemplate,
|
||||
setSkeletonWizardFolder,
|
||||
setQlPackLocation,
|
||||
} from "./config";
|
||||
import { existsSync } from "fs-extra";
|
||||
|
||||
@@ -115,7 +115,7 @@ export class SkeletonQueryWizard {
|
||||
return firstStorageFolder;
|
||||
}
|
||||
|
||||
let storageFolder = getSkeletonWizardFolder();
|
||||
let storageFolder = getQlPackLocation();
|
||||
|
||||
if (storageFolder === undefined || !existsSync(storageFolder)) {
|
||||
storageFolder = await Window.showInputBox({
|
||||
@@ -136,7 +136,7 @@ export class SkeletonQueryWizard {
|
||||
);
|
||||
}
|
||||
|
||||
await setSkeletonWizardFolder(storageFolder);
|
||||
await setQlPackLocation(storageFolder);
|
||||
return storageFolder;
|
||||
}
|
||||
|
||||
|
||||
@@ -412,7 +412,7 @@ describe("SkeletonQueryWizard", () => {
|
||||
|
||||
originalValue = workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.get("folder");
|
||||
.get("qlPackLocation");
|
||||
|
||||
// Set isCodespacesTemplate to true to indicate we are in the codespace template
|
||||
await workspace
|
||||
@@ -421,9 +421,13 @@ describe("SkeletonQueryWizard", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.update("qlPackLocation", originalValue);
|
||||
|
||||
await workspace
|
||||
.getConfiguration("codeQL")
|
||||
.update("codespacesTemplate", originalValue);
|
||||
.update("codespacesTemplate", false);
|
||||
});
|
||||
|
||||
it("should not prompt the user", async () => {
|
||||
@@ -445,16 +449,16 @@ describe("SkeletonQueryWizard", () => {
|
||||
|
||||
originalValue = workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.get("folder");
|
||||
.get("qlPackLocation");
|
||||
await workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.update("folder", storedPath);
|
||||
.update("qlPackLocation", storedPath);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.update("folder", originalValue);
|
||||
.update("qlPackLocation", originalValue);
|
||||
});
|
||||
|
||||
it("should return it and not prompt the user", async () => {
|
||||
@@ -474,16 +478,16 @@ describe("SkeletonQueryWizard", () => {
|
||||
|
||||
originalValue = workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.get("folder");
|
||||
.get("qlPackLocation");
|
||||
await workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.update("folder", storedPath);
|
||||
.update("qlPackLocation", storedPath);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.update("folder", originalValue);
|
||||
.update("qlPackLocation", originalValue);
|
||||
});
|
||||
|
||||
it("should prompt the user for to provide a new folder name", async () => {
|
||||
|
||||
@@ -44,8 +44,8 @@ describe("local databases", () => {
|
||||
let packAddSpy: jest.Mock<any, []>;
|
||||
let logSpy: jest.Mock<any, []>;
|
||||
|
||||
let showBinaryChoiceDialogSpy: jest.SpiedFunction<
|
||||
typeof helpers.showBinaryChoiceDialog
|
||||
let showNeverAskAgainDialogSpy: jest.SpiedFunction<
|
||||
typeof helpers.showNeverAskAgainDialog
|
||||
>;
|
||||
|
||||
let dir: tmp.DirResult;
|
||||
@@ -63,9 +63,9 @@ describe("local databases", () => {
|
||||
/* */
|
||||
});
|
||||
|
||||
showBinaryChoiceDialogSpy = jest
|
||||
.spyOn(helpers, "showBinaryChoiceDialog")
|
||||
.mockResolvedValue(true);
|
||||
showNeverAskAgainDialogSpy = jest
|
||||
.spyOn(helpers, "showNeverAskAgainDialog")
|
||||
.mockResolvedValue("Yes");
|
||||
|
||||
extensionContextStoragePath = dir.name;
|
||||
|
||||
@@ -649,19 +649,31 @@ describe("local databases", () => {
|
||||
it("should offer the user to set up a skeleton QL pack", async () => {
|
||||
await (databaseManager as any).createSkeletonPacks(mockDbItem);
|
||||
|
||||
expect(showBinaryChoiceDialogSpy).toBeCalledTimes(1);
|
||||
expect(showNeverAskAgainDialogSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should return early if the user refuses help", async () => {
|
||||
showBinaryChoiceDialogSpy = jest
|
||||
.spyOn(helpers, "showBinaryChoiceDialog")
|
||||
.mockResolvedValue(false);
|
||||
showNeverAskAgainDialogSpy = jest
|
||||
.spyOn(helpers, "showNeverAskAgainDialog")
|
||||
.mockResolvedValue("No");
|
||||
|
||||
await (databaseManager as any).createSkeletonPacks(mockDbItem);
|
||||
|
||||
expect(generateSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should return early and write choice to settings if user wants to never be asked again", async () => {
|
||||
showNeverAskAgainDialogSpy = jest
|
||||
.spyOn(helpers, "showNeverAskAgainDialog")
|
||||
.mockResolvedValue("No, and never ask me again");
|
||||
const updateValueSpy = jest.spyOn(Setting.prototype, "updateValue");
|
||||
|
||||
await (databaseManager as any).createSkeletonPacks(mockDbItem);
|
||||
|
||||
expect(generateSpy).not.toBeCalled();
|
||||
expect(updateValueSpy).toHaveBeenCalledWith("never", 1);
|
||||
});
|
||||
|
||||
it("should create the skeleton QL pack for the user", async () => {
|
||||
await (databaseManager as any).createSkeletonPacks(mockDbItem);
|
||||
|
||||
@@ -694,9 +706,9 @@ describe("local databases", () => {
|
||||
});
|
||||
|
||||
it("should exit early", async () => {
|
||||
showBinaryChoiceDialogSpy = jest
|
||||
.spyOn(helpers, "showBinaryChoiceDialog")
|
||||
.mockResolvedValue(false);
|
||||
showNeverAskAgainDialogSpy = jest
|
||||
.spyOn(helpers, "showNeverAskAgainDialog")
|
||||
.mockResolvedValue("No");
|
||||
|
||||
await (databaseManager as any).createSkeletonPacks(mockDbItem);
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
showBinaryChoiceDialog,
|
||||
showBinaryChoiceWithUrlDialog,
|
||||
showInformationMessageWithAction,
|
||||
showNeverAskAgainDialog,
|
||||
walkDirectory,
|
||||
} from "../../../src/helpers";
|
||||
import { reportStreamProgress } from "../../../src/common/vscode/progress";
|
||||
@@ -416,7 +417,7 @@ describe("helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("open dialog", () => {
|
||||
describe("showBinaryChoiceDialog", () => {
|
||||
let showInformationMessageSpy: jest.SpiedFunction<
|
||||
typeof window.showInformationMessage
|
||||
>;
|
||||
@@ -445,6 +446,23 @@ describe("helpers", () => {
|
||||
const val = await showBinaryChoiceDialog("xxx");
|
||||
expect(val).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("showInformationMessageWithAction", () => {
|
||||
let showInformationMessageSpy: jest.SpiedFunction<
|
||||
typeof window.showInformationMessage
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
showInformationMessageSpy = jest
|
||||
.spyOn(window, "showInformationMessage")
|
||||
.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
const resolveArg =
|
||||
(index: number) =>
|
||||
(...args: any[]) =>
|
||||
Promise.resolve(args[index]);
|
||||
|
||||
it("should show an info dialog and confirm the action", async () => {
|
||||
// pretend user chooses to run action
|
||||
@@ -459,6 +477,23 @@ describe("helpers", () => {
|
||||
const val = await showInformationMessageWithAction("xxx", "yyy");
|
||||
expect(val).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("showBinaryChoiceWithUrlDialog", () => {
|
||||
let showInformationMessageSpy: jest.SpiedFunction<
|
||||
typeof window.showInformationMessage
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
showInformationMessageSpy = jest
|
||||
.spyOn(window, "showInformationMessage")
|
||||
.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
const resolveArg =
|
||||
(index: number) =>
|
||||
(...args: any[]) =>
|
||||
Promise.resolve(args[index]);
|
||||
|
||||
it("should show a binary choice dialog with a url and return `yes`", async () => {
|
||||
// pretend user clicks on the url twice and then clicks 'yes'
|
||||
@@ -494,6 +529,53 @@ describe("helpers", () => {
|
||||
expect(showInformationMessageSpy).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("showNeverAskAgainDialog", () => {
|
||||
let showInformationMessageSpy: jest.SpiedFunction<
|
||||
typeof window.showInformationMessage
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
showInformationMessageSpy = jest
|
||||
.spyOn(window, "showInformationMessage")
|
||||
.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
const resolveArg =
|
||||
(index: number) =>
|
||||
(...args: any[]) =>
|
||||
Promise.resolve(args[index]);
|
||||
|
||||
const title =
|
||||
"We've noticed you don't have a CodeQL pack available to analyze this database. Can we set up a query pack for you?";
|
||||
|
||||
it("should show a ternary choice dialog and return `Yes`", async () => {
|
||||
// pretend user chooses 'Yes'
|
||||
const yesItem = resolveArg(2);
|
||||
showInformationMessageSpy.mockImplementationOnce(yesItem);
|
||||
|
||||
const answer = await showNeverAskAgainDialog(title);
|
||||
expect(answer).toBe("Yes");
|
||||
});
|
||||
|
||||
it("should show a ternary choice dialog and return `No`", async () => {
|
||||
// pretend user chooses 'No'
|
||||
const noItem = resolveArg(3);
|
||||
showInformationMessageSpy.mockImplementationOnce(noItem);
|
||||
|
||||
const answer = await showNeverAskAgainDialog(title);
|
||||
expect(answer).toBe("No");
|
||||
});
|
||||
|
||||
it("should show a ternary choice dialog and return `No, and never ask me again`", async () => {
|
||||
// pretend user chooses 'No, and never ask me again'
|
||||
const neverAskAgainItem = resolveArg(4);
|
||||
showInformationMessageSpy.mockImplementationOnce(neverAskAgainItem);
|
||||
|
||||
const answer = await showNeverAskAgainDialog(title);
|
||||
expect(answer).toBe("No, and never ask me again");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("walkDirectory", () => {
|
||||
|
||||
Reference in New Issue
Block a user