diff --git a/extensions/ql-vscode/src/common/commands.ts b/extensions/ql-vscode/src/common/commands.ts index d5e9a6a3011..daa94b7370a 100644 --- a/extensions/ql-vscode/src/common/commands.ts +++ b/extensions/ql-vscode/src/common/commands.ts @@ -323,6 +323,7 @@ export type PackagingCommands = { export type ModelEditorCommands = { "codeQL.openModelEditor": () => Promise; + "codeQL.openModelEditorFromModelingPanel": () => Promise; "codeQLModelEditor.jumpToUsageLocation": ( method: Method, usage: Usage, diff --git a/extensions/ql-vscode/src/common/interface-types.ts b/extensions/ql-vscode/src/common/interface-types.ts index b5444eb0cbb..0c3f412ec7d 100644 --- a/extensions/ql-vscode/src/common/interface-types.ts +++ b/extensions/ql-vscode/src/common/interface-types.ts @@ -610,10 +610,15 @@ interface RevealInEditorMessage { method: Method; } +interface StartModelingMessage { + t: "startModeling"; +} + export type FromMethodModelingMessage = | CommonFromViewMessages | SetModeledMethodMessage - | RevealInEditorMessage; + | RevealInEditorMessage + | StartModelingMessage; interface SetMethodMessage { t: "setMethod"; diff --git a/extensions/ql-vscode/src/common/vscode/abstract-webview-view-provider.ts b/extensions/ql-vscode/src/common/vscode/abstract-webview-view-provider.ts index 05bcab3082a..83b07d974cb 100644 --- a/extensions/ql-vscode/src/common/vscode/abstract-webview-view-provider.ts +++ b/extensions/ql-vscode/src/common/vscode/abstract-webview-view-provider.ts @@ -13,7 +13,7 @@ export abstract class AbstractWebviewViewProvider< private disposables: Disposable[] = []; constructor( - private readonly app: App, + protected readonly app: App, private readonly webviewKind: WebviewKind, ) {} diff --git a/extensions/ql-vscode/src/model-editor/method-modeling/method-modeling-view-provider.ts b/extensions/ql-vscode/src/model-editor/method-modeling/method-modeling-view-provider.ts index 8b04153d601..7ce2376795f 100644 --- a/extensions/ql-vscode/src/model-editor/method-modeling/method-modeling-view-provider.ts +++ b/extensions/ql-vscode/src/model-editor/method-modeling/method-modeling-view-provider.ts @@ -92,6 +92,12 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider< await this.revealInModelEditor(msg.method); break; + + case "startModeling": + await this.app.commands.execute( + "codeQL.openModelEditorFromModelingPanel", + ); + break; default: assertNever(msg); } diff --git a/extensions/ql-vscode/src/model-editor/model-editor-module.ts b/extensions/ql-vscode/src/model-editor/model-editor-module.ts index bb4ee6bb86a..c5645de2cad 100644 --- a/extensions/ql-vscode/src/model-editor/model-editor-module.ts +++ b/extensions/ql-vscode/src/model-editor/model-editor-module.ts @@ -73,115 +73,9 @@ export class ModelEditorModule extends DisposableObject { public getCommands(): ModelEditorCommands { return { - "codeQL.openModelEditor": async () => { - const db = this.databaseManager.currentDatabaseItem; - if (!db) { - void showAndLogErrorMessage(this.app.logger, "No database selected"); - return; - } - - const language = db.language; - if ( - !SUPPORTED_LANGUAGES.includes(language) || - !isQueryLanguage(language) - ) { - void showAndLogErrorMessage( - this.app.logger, - `The CodeQL Model Editor is not supported for ${language} databases.`, - ); - return; - } - - return withProgress( - async (progress) => { - const maxStep = 4; - - if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) { - void showAndLogErrorMessage( - this.app.logger, - `This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`, - ); - return; - } - - if ( - !(await this.cliServer.cliConstraints.supportsResolveExtensions()) - ) { - void showAndLogErrorMessage( - this.app.logger, - `This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_EXTENSIONS.format()} or later.`, - ); - return; - } - - const modelFile = await pickExtensionPack( - this.cliServer, - db, - this.app.logger, - progress, - maxStep, - ); - if (!modelFile) { - return; - } - - progress({ - message: "Installing dependencies...", - step: 3, - maxStep, - }); - - // Create new temporary directory for query files and pack dependencies - const { path: queryDir, cleanup: cleanupQueryDir } = await dir({ - unsafeCleanup: true, - }); - - const success = await setUpPack(this.cliServer, queryDir, language); - if (!success) { - await cleanupQueryDir(); - return; - } - - progress({ - message: "Opening editor...", - step: 4, - maxStep, - }); - - const view = new ModelEditorView( - this.app, - this.modelingStore, - this.editorViewTracker, - this.databaseManager, - this.cliServer, - this.queryRunner, - this.queryStorageDir, - queryDir, - db, - modelFile, - Mode.Application, - ); - - this.modelingStore.onDbClosed(async (dbUri) => { - if (dbUri === db.databaseUri.toString()) { - await cleanupQueryDir(); - } - }); - - this.push(view); - this.push({ - dispose(): void { - void cleanupQueryDir(); - }, - }); - - await view.openView(); - }, - { - title: "Opening CodeQL Model Editor", - }, - ); - }, + "codeQL.openModelEditor": this.openModelEditor.bind(this), + "codeQL.openModelEditorFromModelingPanel": + this.openModelEditor.bind(this), "codeQLModelEditor.jumpToUsageLocation": async ( method: Method, usage: Usage, @@ -213,4 +107,116 @@ export class ModelEditorModule extends DisposableObject { await this.methodModelingPanel.setMethod(method); await showResolvableLocation(usage.url, databaseItem, this.app.logger); } + + private async openModelEditor(): Promise { + { + const db = this.databaseManager.currentDatabaseItem; + if (!db) { + void showAndLogErrorMessage(this.app.logger, "No database selected"); + return; + } + + const language = db.language; + if ( + !SUPPORTED_LANGUAGES.includes(language) || + !isQueryLanguage(language) + ) { + void showAndLogErrorMessage( + this.app.logger, + `The CodeQL Model Editor is not supported for ${language} databases.`, + ); + return; + } + + return withProgress( + async (progress) => { + const maxStep = 4; + + if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) { + void showAndLogErrorMessage( + this.app.logger, + `This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`, + ); + return; + } + + if ( + !(await this.cliServer.cliConstraints.supportsResolveExtensions()) + ) { + void showAndLogErrorMessage( + this.app.logger, + `This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_EXTENSIONS.format()} or later.`, + ); + return; + } + + const modelFile = await pickExtensionPack( + this.cliServer, + db, + this.app.logger, + progress, + maxStep, + ); + if (!modelFile) { + return; + } + + progress({ + message: "Installing dependencies...", + step: 3, + maxStep, + }); + + // Create new temporary directory for query files and pack dependencies + const { path: queryDir, cleanup: cleanupQueryDir } = await dir({ + unsafeCleanup: true, + }); + + const success = await setUpPack(this.cliServer, queryDir, language); + if (!success) { + await cleanupQueryDir(); + return; + } + + progress({ + message: "Opening editor...", + step: 4, + maxStep, + }); + + const view = new ModelEditorView( + this.app, + this.modelingStore, + this.editorViewTracker, + this.databaseManager, + this.cliServer, + this.queryRunner, + this.queryStorageDir, + queryDir, + db, + modelFile, + Mode.Application, + ); + + this.modelingStore.onDbClosed(async (dbUri) => { + if (dbUri === db.databaseUri.toString()) { + await cleanupQueryDir(); + } + }); + + this.push(view); + this.push({ + dispose(): void { + void cleanupQueryDir(); + }, + }); + + await view.openView(); + }, + { + title: "Opening CodeQL Model Editor", + }, + ); + } + } } diff --git a/extensions/ql-vscode/src/stories/common/ResponsiveContainer.stories.tsx b/extensions/ql-vscode/src/stories/common/ResponsiveContainer.stories.tsx new file mode 100644 index 00000000000..5639d147b97 --- /dev/null +++ b/extensions/ql-vscode/src/stories/common/ResponsiveContainer.stories.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; + +import { Meta, StoryFn } from "@storybook/react"; + +import { ResponsiveContainer as ResponsiveContainerComponent } from "../../view/common/ResponsiveContainer"; + +export default { + title: "Responsive Container", + component: ResponsiveContainerComponent, +} as Meta; + +const Template: StoryFn = (args) => ( + + Hello + +); + +export const ResponsiveContainer = Template.bind({}); diff --git a/extensions/ql-vscode/src/stories/method-modeling/NoMethodSelected.stories.tsx b/extensions/ql-vscode/src/stories/method-modeling/NoMethodSelected.stories.tsx new file mode 100644 index 00000000000..4881702035c --- /dev/null +++ b/extensions/ql-vscode/src/stories/method-modeling/NoMethodSelected.stories.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; + +import { Meta, StoryFn } from "@storybook/react"; + +import { NoMethodSelected as NoMethodSelectedComponent } from "../../view/method-modeling/NoMethodSelected"; + +export default { + title: "Method Modeling/No Method Selected", + component: NoMethodSelectedComponent, +} as Meta; + +const Template: StoryFn = () => ( + +); + +export const NoMethodSelected = Template.bind({}); diff --git a/extensions/ql-vscode/src/stories/method-modeling/NotInModelingMode.stories.tsx b/extensions/ql-vscode/src/stories/method-modeling/NotInModelingMode.stories.tsx new file mode 100644 index 00000000000..654bbdb129b --- /dev/null +++ b/extensions/ql-vscode/src/stories/method-modeling/NotInModelingMode.stories.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; + +import { Meta, StoryFn } from "@storybook/react"; + +import { NotInModelingMode as NotInModelingModeComponent } from "../../view/method-modeling/NotInModelingMode"; + +export default { + title: "Method Modeling/Not In Modeling Mode", + component: NotInModelingModeComponent, +} as Meta; + +const Template: StoryFn = () => ( + +); + +export const NotInModelingMode = Template.bind({}); diff --git a/extensions/ql-vscode/src/view/common/ResponsiveContainer.tsx b/extensions/ql-vscode/src/view/common/ResponsiveContainer.tsx new file mode 100644 index 00000000000..5c73f582ecc --- /dev/null +++ b/extensions/ql-vscode/src/view/common/ResponsiveContainer.tsx @@ -0,0 +1,15 @@ +import { styled } from "styled-components"; + +export const ResponsiveContainer = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + height: 100vh; + + @media (min-height: 300px) { + align-items: center; + justify-content: center; + text-align: center; + } +`; diff --git a/extensions/ql-vscode/src/view/method-modeling/NoMethodSelected.tsx b/extensions/ql-vscode/src/view/method-modeling/NoMethodSelected.tsx new file mode 100644 index 00000000000..bd4866d59ff --- /dev/null +++ b/extensions/ql-vscode/src/view/method-modeling/NoMethodSelected.tsx @@ -0,0 +1,8 @@ +import * as React from "react"; +import { ResponsiveContainer } from "../common/ResponsiveContainer"; + +export const NoMethodSelected = () => { + return ( + Select an API or method to model + ); +}; diff --git a/extensions/ql-vscode/src/view/method-modeling/NotInModelingMode.tsx b/extensions/ql-vscode/src/view/method-modeling/NotInModelingMode.tsx new file mode 100644 index 00000000000..fa120c8a15a --- /dev/null +++ b/extensions/ql-vscode/src/view/method-modeling/NotInModelingMode.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; +import { useCallback } from "react"; +import { vscode } from "../vscode-api"; +import { styled } from "styled-components"; +import TextButton from "../common/TextButton"; +import { ResponsiveContainer } from "../common/ResponsiveContainer"; + +const Button = styled(TextButton)` + margin-top: 0.2rem; +`; + +export const NotInModelingMode = () => { + const handleClick = useCallback(() => { + vscode.postMessage({ + t: "startModeling", + }); + }, []); + + return ( + + Not in modeling mode + + + ); +};