diff --git a/extensions/ql-vscode/.storybook/preview.ts b/extensions/ql-vscode/.storybook/preview.ts index 7182025d149..7616a6202dc 100644 --- a/extensions/ql-vscode/.storybook/preview.ts +++ b/extensions/ql-vscode/.storybook/preview.ts @@ -33,5 +33,6 @@ export const parameters = { }; (window as any).acquireVsCodeApi = () => ({ - postMessage: action('post-vscode-message') + postMessage: action('post-vscode-message'), + setState: action('set-vscode-state'), }); diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index 70d9209611f..4080fedd54d 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -63,6 +63,7 @@ "onCommand:codeQL.quickQuery", "onCommand:codeQL.restartQueryServer", "onWebviewPanel:resultsView", + "onWebviewPanel:codeQL.variantAnalysis", "onFileSystem:codeql-zip-archive" ], "main": "./out/extension", diff --git a/extensions/ql-vscode/src/abstract-webview.ts b/extensions/ql-vscode/src/abstract-webview.ts index b72d82cbde7..4206f3303e7 100644 --- a/extensions/ql-vscode/src/abstract-webview.ts +++ b/extensions/ql-vscode/src/abstract-webview.ts @@ -33,6 +33,11 @@ export abstract class AbstractWebview { + this.panel = panel; + this.setupPanel(panel); + } + protected get isShowingPanel() { return !!this.panel; } @@ -59,37 +64,43 @@ export abstract class AbstractWebview { - this.panel = undefined; - this.panelLoaded = false; - this.onPanelDispose(); - }, - null, - ctx.subscriptions - ) - ); - - this.panel.webview.html = getHtmlForWebview( - ctx, - this.panel.webview, - config.view, - { - allowInlineStyles: true, - } - ); - this.push( - this.panel.webview.onDidReceiveMessage( - async (e) => this.onMessage(e), - undefined, - ctx.subscriptions - ) - ); + this.setupPanel(this.panel); } return this.panel; } + protected setupPanel(panel: WebviewPanel): void { + const config = this.getPanelConfig(); + + this.push( + panel.onDidDispose( + () => { + this.panel = undefined; + this.panelLoaded = false; + this.onPanelDispose(); + }, + null, + this.ctx.subscriptions + ) + ); + + panel.webview.html = getHtmlForWebview( + this.ctx, + panel.webview, + config.view, + { + allowInlineStyles: true, + } + ); + this.push( + panel.webview.onDidReceiveMessage( + async (e) => this.onMessage(e), + undefined, + this.ctx.subscriptions + ) + ); + } + protected abstract getPanelConfig(): WebviewPanelConfig; protected abstract onPanelDispose(): void; diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 4d24b37cd51..a8d93be1929 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -104,6 +104,8 @@ import { LogScannerService } from './log-insights/log-scanner-service'; import { createInitialQueryInfo } from './run-queries-shared'; import { LegacyQueryRunner } from './legacy-query-server/legacyRunner'; import { QueryRunner } from './queryRunner'; +import { VariantAnalysisView } from './remote-queries/variant-analysis-view'; +import { VariantAnalysisViewSerializer } from './remote-queries/variant-analysis-view-serializer'; import { VariantAnalysis } from './remote-queries/shared/variant-analysis'; import { VariantAnalysis as VariantAnalysisApiResponse, @@ -176,6 +178,7 @@ export interface CodeQLExtensionInterface { readonly distributionManager: DistributionManager; readonly databaseManager: DatabaseManager; readonly databaseUI: DatabaseUI; + readonly variantAnalysisManager: VariantAnalysisManager; readonly dispose: () => void; } @@ -386,7 +389,10 @@ export async function activate(ctx: ExtensionContext): Promise { - await variantAnalysisManager.showView(1); + // Generate a random variant analysis ID for testing + const variantAnalysisId: number = Math.floor(Math.random() * 1000000); + + await variantAnalysisManager.showView(variantAnalysisId); }) ); @@ -1159,6 +1172,7 @@ async function activateWithInstalledDistribution( distributionManager, databaseManager: dbm, databaseUI, + variantAnalysisManager, dispose: () => { ctx.subscriptions.forEach(d => d.dispose()); } diff --git a/extensions/ql-vscode/src/pure/interface-types.ts b/extensions/ql-vscode/src/pure/interface-types.ts index 7d6024999fb..c636872606b 100644 --- a/extensions/ql-vscode/src/pure/interface-types.ts +++ b/extensions/ql-vscode/src/pure/interface-types.ts @@ -440,6 +440,15 @@ export interface SetVariantAnalysisMessage { variantAnalysis: VariantAnalysis; } +export type StopVariantAnalysisMessage = { + t: 'stopVariantAnalysis'; + variantAnalysisId: number; +} + +export type VariantAnalysisState = { + variantAnalysisId: number; +} + export interface SetRepoResultsMessage { t: 'setRepoResults'; repoResults: VariantAnalysisScannedRepositoryResult[]; @@ -456,4 +465,5 @@ export type ToVariantAnalysisMessage = | SetRepoStatesMessage; export type FromVariantAnalysisMessage = - | ViewLoadedMsg; + | ViewLoadedMsg + | StopVariantAnalysisMessage; diff --git a/extensions/ql-vscode/src/remote-queries/variant-analysis-view-serializer.ts b/extensions/ql-vscode/src/remote-queries/variant-analysis-view-serializer.ts new file mode 100644 index 00000000000..d0a881fa115 --- /dev/null +++ b/extensions/ql-vscode/src/remote-queries/variant-analysis-view-serializer.ts @@ -0,0 +1,48 @@ +import { ExtensionContext, WebviewPanel, WebviewPanelSerializer } from 'vscode'; +import { VariantAnalysisView } from './variant-analysis-view'; +import { VariantAnalysisState } from '../pure/interface-types'; +import { VariantAnalysisViewManager } from './variant-analysis-view-manager'; + +export class VariantAnalysisViewSerializer implements WebviewPanelSerializer { + private resolvePromises: ((value: VariantAnalysisViewManager) => void)[] = []; + + private manager?: VariantAnalysisViewManager; + + public constructor( + private readonly ctx: ExtensionContext, + ) { } + + onExtensionLoaded(manager: VariantAnalysisViewManager): void { + this.manager = manager; + + this.resolvePromises.forEach((resolve) => resolve(manager)); + this.resolvePromises = []; + } + + async deserializeWebviewPanel(webviewPanel: WebviewPanel, state: unknown): Promise { + if (!state || typeof state !== 'object') { + return; + } + + if (!('variantAnalysisId' in state)) { + return; + } + + const variantAnalysisState: VariantAnalysisState = state as VariantAnalysisState; + + const manager = await this.waitForExtensionFullyLoaded(); + + const view = new VariantAnalysisView(this.ctx, variantAnalysisState.variantAnalysisId, manager); + await view.restoreView(webviewPanel); + } + + private waitForExtensionFullyLoaded(): Promise> { + if (this.manager) { + return Promise.resolve(this.manager); + } + + return new Promise>((resolve) => { + this.resolvePromises.push(resolve); + }); + } +} diff --git a/extensions/ql-vscode/src/remote-queries/variant-analysis-view.ts b/extensions/ql-vscode/src/remote-queries/variant-analysis-view.ts index 43b14db377d..03c6e78be05 100644 --- a/extensions/ql-vscode/src/remote-queries/variant-analysis-view.ts +++ b/extensions/ql-vscode/src/remote-queries/variant-analysis-view.ts @@ -1,12 +1,20 @@ import { ExtensionContext, ViewColumn } from 'vscode'; import { AbstractWebview, WebviewPanelConfig } from '../abstract-webview'; -import { WebviewMessage } from '../interface-utils'; import { logger } from '../logging'; -import { VariantAnalysisViewInterface, VariantAnalysisViewManager } from './variant-analysis-view-manager'; -import { VariantAnalysis, VariantAnalysisScannedRepositoryState } from './shared/variant-analysis'; import { FromVariantAnalysisMessage, ToVariantAnalysisMessage } from '../pure/interface-types'; +import { assertNever } from '../pure/helpers-pure'; +import { + VariantAnalysis, + VariantAnalysisQueryLanguage, + VariantAnalysisRepoStatus, + VariantAnalysisScannedRepositoryState, + VariantAnalysisStatus +} from './shared/variant-analysis'; +import { VariantAnalysisViewInterface, VariantAnalysisViewManager } from './variant-analysis-view-manager'; export class VariantAnalysisView extends AbstractWebview implements VariantAnalysisViewInterface { + public static readonly viewType = 'codeQL.variantAnalysis'; + public constructor( ctx: ExtensionContext, public readonly variantAnalysisId: number, @@ -19,6 +27,8 @@ export class VariantAnalysisView extends AbstractWebview { @@ -45,11 +55,11 @@ export class VariantAnalysisView extends AbstractWebview { - void logger.log('Received message on variant analysis view: ' + msg.t); + protected async onMessage(msg: FromVariantAnalysisMessage): Promise { + switch (msg.t) { + case 'viewLoaded': + this.onWebViewLoaded(); + + void logger.log('Variant analysis view loaded'); + + await this.postMessage({ + t: 'setVariantAnalysis', + variantAnalysis: this.getVariantAnalysis(), + }); + + break; + case 'stopVariantAnalysis': + void logger.log(`Stop variant analysis: ${msg.variantAnalysisId}`); + break; + default: + assertNever(msg); + } + } + + private getVariantAnalysis(): VariantAnalysis { + return { + id: this.variantAnalysisId, + controllerRepoId: 1, + actionsWorkflowRunId: 789263, + query: { + name: 'Example query', + filePath: 'example.ql', + language: VariantAnalysisQueryLanguage.Javascript, + }, + databases: {}, + status: VariantAnalysisStatus.InProgress, + scannedRepos: [ + { + repository: { + id: 1, + fullName: 'octodemo/hello-world-1', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 2, + fullName: 'octodemo/hello-world-2', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 3, + fullName: 'octodemo/hello-world-3', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 4, + fullName: 'octodemo/hello-world-4', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 5, + fullName: 'octodemo/hello-world-5', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 6, + fullName: 'octodemo/hello-world-6', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 7, + fullName: 'octodemo/hello-world-7', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 8, + fullName: 'octodemo/hello-world-8', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 9, + fullName: 'octodemo/hello-world-9', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 10, + fullName: 'octodemo/hello-world-10', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + ], + skippedRepos: { + notFoundRepos: { + repositoryCount: 2, + repositories: [ + { + fullName: 'octodemo/hello-globe' + }, + { + fullName: 'octodemo/hello-planet' + } + ] + }, + noCodeqlDbRepos: { + repositoryCount: 4, + repositories: [ + { + id: 100, + fullName: 'octodemo/no-db-1' + }, + { + id: 101, + fullName: 'octodemo/no-db-2' + }, + { + id: 102, + fullName: 'octodemo/no-db-3' + }, + { + id: 103, + fullName: 'octodemo/no-db-4' + } + ] + }, + overLimitRepos: { + repositoryCount: 1, + repositories: [ + { + id: 201, + fullName: 'octodemo/over-limit-1' + } + ] + }, + accessMismatchRepos: { + repositoryCount: 1, + repositories: [ + { + id: 205, + fullName: 'octodemo/private' + } + ] + } + }, + }; } } diff --git a/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysis.stories.tsx b/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysis.stories.tsx index 80ec0b080e4..0496a44b9e6 100644 --- a/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysis.stories.tsx +++ b/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysis.stories.tsx @@ -3,14 +3,171 @@ import React from 'react'; import { ComponentStory, ComponentMeta } from '@storybook/react'; import { VariantAnalysis as VariantAnalysisComponent } from '../../view/variant-analysis/VariantAnalysis'; +import { + VariantAnalysis as VariantAnalysisDomainModel, + VariantAnalysisQueryLanguage, VariantAnalysisRepoStatus, VariantAnalysisStatus +} from '../../remote-queries/shared/variant-analysis'; export default { title: 'Variant Analysis/Variant Analysis', component: VariantAnalysisComponent, } as ComponentMeta; -const Template: ComponentStory = () => ( - +const Template: ComponentStory = (args) => ( + ); -export const VariantAnalysis = Template.bind({}); +const variantAnalysis: VariantAnalysisDomainModel = { + id: 1, + controllerRepoId: 1, + actionsWorkflowRunId: 789263, + query: { + name: 'Example query', + filePath: 'example.ql', + language: VariantAnalysisQueryLanguage.Javascript, + }, + databases: {}, + status: VariantAnalysisStatus.InProgress, + scannedRepos: [ + { + repository: { + id: 1, + fullName: 'octodemo/hello-world-1', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 2, + fullName: 'octodemo/hello-world-2', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 3, + fullName: 'octodemo/hello-world-3', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 4, + fullName: 'octodemo/hello-world-4', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 5, + fullName: 'octodemo/hello-world-5', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 6, + fullName: 'octodemo/hello-world-6', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 7, + fullName: 'octodemo/hello-world-7', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 8, + fullName: 'octodemo/hello-world-8', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 9, + fullName: 'octodemo/hello-world-9', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + { + repository: { + id: 10, + fullName: 'octodemo/hello-world-10', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + ], + skippedRepos: { + notFoundRepos: { + repositoryCount: 2, + repositories: [ + { + fullName: 'octodemo/hello-globe' + }, + { + fullName: 'octodemo/hello-planet' + } + ] + }, + noCodeqlDbRepos: { + repositoryCount: 4, + repositories: [ + { + id: 100, + fullName: 'octodemo/no-db-1' + }, + { + id: 101, + fullName: 'octodemo/no-db-2' + }, + { + id: 102, + fullName: 'octodemo/no-db-3' + }, + { + id: 103, + fullName: 'octodemo/no-db-4' + } + ] + }, + overLimitRepos: { + repositoryCount: 1, + repositories: [ + { + id: 201, + fullName: 'octodemo/over-limit-1' + } + ] + }, + accessMismatchRepos: { + repositoryCount: 1, + repositories: [ + { + id: 205, + fullName: 'octodemo/private' + } + ] + } + }, +}; + +export const Loading = Template.bind({}); +Loading.args = {}; + +export const FullExample = Template.bind({}); +FullExample.args = { + variantAnalysis: variantAnalysis, +}; diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx index 7ab61be876c..3df58f3c8fa 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx @@ -1,167 +1,16 @@ import * as React from 'react'; import { useEffect, useState } from 'react'; -import { ToVariantAnalysisMessage } from '../../pure/interface-types'; import { VariantAnalysis as VariantAnalysisDomainModel, - VariantAnalysisQueryLanguage, - VariantAnalysisRepoStatus, VariantAnalysisScannedRepositoryResult, VariantAnalysisScannedRepositoryState, - VariantAnalysisStatus + VariantAnalysisScannedRepositoryResult, + VariantAnalysisScannedRepositoryState, } from '../../remote-queries/shared/variant-analysis'; import { VariantAnalysisHeader } from './VariantAnalysisHeader'; import { VariantAnalysisOutcomePanels } from './VariantAnalysisOutcomePanels'; import { VariantAnalysisLoading } from './VariantAnalysisLoading'; - -const variantAnalysis: VariantAnalysisDomainModel = { - id: 1, - controllerRepoId: 1, - actionsWorkflowRunId: 789263, - query: { - name: 'Example query', - filePath: 'example.ql', - language: VariantAnalysisQueryLanguage.Javascript, - }, - databases: {}, - status: VariantAnalysisStatus.InProgress, - scannedRepos: [ - { - repository: { - id: 1, - fullName: 'octodemo/hello-world-1', - private: false, - }, - analysisStatus: VariantAnalysisRepoStatus.Succeeded, - }, - { - repository: { - id: 2, - fullName: 'octodemo/hello-world-2', - private: false, - }, - analysisStatus: VariantAnalysisRepoStatus.Canceled, - }, - { - repository: { - id: 3, - fullName: 'octodemo/hello-world-3', - private: false, - }, - analysisStatus: VariantAnalysisRepoStatus.TimedOut, - }, - { - repository: { - id: 4, - fullName: 'octodemo/hello-world-4', - private: false, - }, - analysisStatus: VariantAnalysisRepoStatus.Failed, - }, - { - repository: { - id: 5, - fullName: 'octodemo/hello-world-5', - private: false, - }, - analysisStatus: VariantAnalysisRepoStatus.InProgress, - }, - { - repository: { - id: 6, - fullName: 'octodemo/hello-world-6', - private: false, - }, - analysisStatus: VariantAnalysisRepoStatus.InProgress, - }, - { - repository: { - id: 7, - fullName: 'octodemo/hello-world-7', - private: false, - }, - analysisStatus: VariantAnalysisRepoStatus.Pending, - }, - { - repository: { - id: 8, - fullName: 'octodemo/hello-world-8', - private: false, - }, - analysisStatus: VariantAnalysisRepoStatus.Pending, - }, - { - repository: { - id: 9, - fullName: 'octodemo/hello-world-9', - private: false, - }, - analysisStatus: VariantAnalysisRepoStatus.Pending, - }, - { - repository: { - id: 10, - fullName: 'octodemo/hello-world-10', - private: false, - }, - analysisStatus: VariantAnalysisRepoStatus.Pending, - }, - ], - skippedRepos: { - notFoundRepos: { - repositoryCount: 9999, - repositories: [ - { - fullName: 'octodemo/hello-globe' - }, - { - fullName: 'octodemo/hello-planet' - } - ] - }, - noCodeqlDbRepos: { - repositoryCount: 4, - repositories: [ - { - id: 100, - fullName: 'octodemo/no-db-1', - private: false, - }, - { - id: 101, - fullName: 'octodemo/no-db-2', - private: true, - }, - { - id: 102, - fullName: 'octodemo/no-db-3', - private: true, - }, - { - id: 103, - fullName: 'octodemo/no-db-4', - private: false, - } - ] - }, - overLimitRepos: { - repositoryCount: 1, - repositories: [ - { - id: 201, - fullName: 'octodemo/over-limit-1' - } - ] - }, - accessMismatchRepos: { - repositoryCount: 1, - repositories: [ - { - id: 205, - fullName: 'octodemo/private' - } - ] - } - }, -}; +import { ToVariantAnalysisMessage } from '../../pure/interface-types'; +import { vscode } from '../vscode-api'; const repositoryResults: VariantAnalysisScannedRepositoryResult[] = [ { @@ -206,11 +55,11 @@ type Props = { } export function VariantAnalysis({ - variantAnalysis: initialVariantAnalysis = variantAnalysis, + variantAnalysis: initialVariantAnalysis, repoStates: initialRepoStates = [], repoResults: initialRepoResults = repositoryResults, }: Props): JSX.Element { - const [variantAnalysis, setVariantAnalysis] = useState(initialVariantAnalysis); + const [variantAnalysis, setVariantAnalysis] = useState(); const [repoStates, setRepoStates] = useState(initialRepoStates); const [repoResults, setRepoResults] = useState(initialRepoResults); @@ -220,6 +69,9 @@ export function VariantAnalysis({ const msg: ToVariantAnalysisMessage = evt.data; if (msg.t === 'setVariantAnalysis') { setVariantAnalysis(msg.variantAnalysis); + vscode.setState({ + variantAnalysisId: msg.variantAnalysis.id, + }); } else if (msg.t === 'setRepoResults') { setRepoResults(oldRepoResults => { const newRepoIds = msg.repoResults.map(r => r.repositoryId); @@ -239,7 +91,7 @@ export function VariantAnalysis({ }); }); - if (variantAnalysis.actionsWorkflowRunId === undefined) { + if (variantAnalysis?.actionsWorkflowRunId === undefined) { return ; } diff --git a/extensions/ql-vscode/src/view/vscode-api.ts b/extensions/ql-vscode/src/view/vscode-api.ts index 61c4816a317..2e49409e588 100644 --- a/extensions/ql-vscode/src/view/vscode-api.ts +++ b/extensions/ql-vscode/src/view/vscode-api.ts @@ -1,10 +1,20 @@ -import { FromCompareViewMessage, FromRemoteQueriesMessage, FromResultsViewMsg } from '../pure/interface-types'; +import { + FromCompareViewMessage, + FromRemoteQueriesMessage, + FromResultsViewMsg, + FromVariantAnalysisMessage, VariantAnalysisState +} from '../pure/interface-types'; export interface VsCodeApi { /** * Post message back to vscode extension. */ - postMessage(msg: FromResultsViewMsg | FromCompareViewMessage | FromRemoteQueriesMessage): void; + postMessage(msg: FromResultsViewMsg | FromCompareViewMessage | FromRemoteQueriesMessage | FromVariantAnalysisMessage): void; + + /** + * Set state of the webview. + */ + setState(state: VariantAnalysisState): void; } declare const acquireVsCodeApi: () => VsCodeApi;