Merge branch 'main' into alexet/prepare-new-qs
This commit is contained in:
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
@@ -35,6 +35,9 @@
|
||||
"runtimeArgs": [
|
||||
"--inspect=9229"
|
||||
],
|
||||
"env": {
|
||||
"LANG": "en-US"
|
||||
},
|
||||
"args": [
|
||||
"--exit",
|
||||
"-u",
|
||||
@@ -43,6 +46,8 @@
|
||||
"--diff",
|
||||
"-r",
|
||||
"ts-node/register",
|
||||
"-r",
|
||||
"test/mocha.setup.js",
|
||||
"test/pure-tests/**/*.ts"
|
||||
],
|
||||
"stopOnEntry": false,
|
||||
@@ -50,6 +55,18 @@
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
},
|
||||
{
|
||||
"name": "Launch Unit Tests - React (vscode-codeql)",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/extensions/ql-vscode/node_modules/.bin/jest",
|
||||
"showAsyncStacks": true,
|
||||
"cwd": "${workspaceFolder}/extensions/ql-vscode",
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
},
|
||||
{
|
||||
"name": "Launch Integration Tests - No Workspace (vscode-codeql)",
|
||||
"type": "extensionHost",
|
||||
|
||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -30,12 +30,11 @@
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"eslint.options": {
|
||||
// This is necessary so that eslint can properly resolve its plugins
|
||||
"resolvePluginsRelativeTo": "./extensions/ql-vscode"
|
||||
},
|
||||
// This is necessary to ensure that ESLint can find the correct configuration files and plugins.
|
||||
"eslint.workingDirectories": ["./extensions/ql-vscode"],
|
||||
"editor.formatOnSave": false,
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
"javascript.preferences.quoteStyle": "single",
|
||||
"editor.wordWrapColumn": 100
|
||||
"editor.wordWrapColumn": 100,
|
||||
"jest.rootPath": "./extensions/ql-vscode"
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
**/* @github/codeql-vscode-reviewers
|
||||
/extensions/ql-vscode/src/remote-queries/ @github/code-scanning-secexp-reviewers
|
||||
/extensions/ql-vscode/src/view/remote-queries/ @github/code-scanning-secexp-reviewers
|
||||
/extensions/ql-vscode/src/view/variant-analysis/ @github/code-scanning-secexp-reviewers
|
||||
|
||||
@@ -160,6 +160,7 @@ From inside of VSCode, open the `launch.json` file and in the _Launch Integratio
|
||||
* **IMPORTANT** Make sure you are on the `main` branch and your local checkout is fully updated when you add the tag.
|
||||
* If you accidentally add the tag to the wrong ref, you can just force push it to the right one later.
|
||||
1. Monitor the status of the release build in the `Release` workflow in the Actions tab.
|
||||
* DO NOT approve the "publish" stages of the workflow yet.
|
||||
1. Download the VSIX from the draft GitHub release at the top of [the releases page](https://github.com/github/vscode-codeql/releases) that is created when the release build finishes.
|
||||
1. Unzip the `.vsix` and inspect its `package.json` to make sure the version is what you expect,
|
||||
or look at the source if there's any doubt the right code is being shipped.
|
||||
|
||||
@@ -10,7 +10,7 @@ module.exports = {
|
||||
node: true,
|
||||
es6: true,
|
||||
},
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:jest-dom/recommended"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-use-before-define": 0,
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
- Removed the ability to manually upgrade databases from the context menu on databases. Databases are non-destructively upgraded automatically so
|
||||
for most users this was not needed. For advanced users this is still available in the Command Palette. [#1501](https://github.com/github/vscode-codeql/pull/1501)
|
||||
## 1.7.0 - 20 September 2022
|
||||
|
||||
- Remove ability to download databases from LGTM. [#1467](https://github.com/github/vscode-codeql/pull/1467)
|
||||
- Removed the ability to manually upgrade databases from the context menu on databases. Databases are non-destructively upgraded automatically so for most users this was not needed. For advanced users this is still available in the Command Palette. [#1501](https://github.com/github/vscode-codeql/pull/1501)
|
||||
- Always restart the query server after a manual database upgrade. This avoids a bug in the query server where an invalid dbscheme was being retained in memory after an upgrade. [#1519](https://github.com/github/vscode-codeql/pull/1519)
|
||||
|
||||
## 1.6.12 - 1 September 2022
|
||||
|
||||
@@ -19,7 +22,7 @@ No user facing changes.
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 1.6.9 - 20 July 2022
|
||||
## 1.6.9 - 20 July 2022
|
||||
|
||||
No user facing changes.
|
||||
|
||||
|
||||
214
extensions/ql-vscode/jest.config.js
Normal file
214
extensions/ql-vscode/jest.config.js
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* For a detailed explanation regarding each configuration property and type check, visit:
|
||||
* https://jestjs.io/docs/configuration
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/6m/1394pht172qgd7dmw1fwjk100000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls, instances, contexts and results before every test
|
||||
// clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
// collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
// coverageDirectory: undefined,
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
coverageProvider: 'v8',
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// The default configuration for fake timers
|
||||
// fakeTimers: {
|
||||
// "enableGlobally": false
|
||||
// },
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
moduleFileExtensions: [
|
||||
'js',
|
||||
'mjs',
|
||||
'cjs',
|
||||
'jsx',
|
||||
'ts',
|
||||
'tsx',
|
||||
'json'
|
||||
],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
'moduleNameMapper': {
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/test/__mocks__/fileMock.ts',
|
||||
'\\.(css|less)$': '<rootDir>/test/__mocks__/styleMock.ts'
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
preset: 'ts-jest',
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state before every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state and implementation before every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
setupFilesAfterEnv: ['<rootDir>/test/jest.setup.ts'],
|
||||
|
||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||
// slowTestThreshold: 5,
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: 'jsdom',
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: [
|
||||
'**/__tests__/**/*.[jt]s?(x)'
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jest-circus/runner",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: 'src/view/tsconfig.spec.json',
|
||||
},
|
||||
],
|
||||
'node_modules': [
|
||||
'babel-jest',
|
||||
{
|
||||
presets: [
|
||||
'@babel/preset-env'
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-transform-modules-commonjs',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
'transformIgnorePatterns': [
|
||||
// These use ES modules, so need to be transformed
|
||||
'node_modules/(?!(?:@vscode/webview-ui-toolkit|@microsoft/.+|exenv-es6)/.*)'
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
||||
13581
extensions/ql-vscode/package-lock.json
generated
13581
extensions/ql-vscode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.6.13",
|
||||
"version": "1.7.1",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -313,6 +313,10 @@
|
||||
"command": "codeQL.exportVariantAnalysisResults",
|
||||
"title": "CodeQL: Export Variant Analysis Results"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.mockVariantAnalysisView",
|
||||
"title": "CodeQL: Open Variant Analysis Mock View"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runQueries",
|
||||
"title": "CodeQL: Run Queries in Selected Files"
|
||||
@@ -669,7 +673,7 @@
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseLgtm",
|
||||
"when": "view == codeQLDatabases",
|
||||
"when": "config.codeQL.canary && view == codeQLDatabases",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
@@ -893,6 +897,10 @@
|
||||
"command": "codeQL.exportVariantAnalysisResults",
|
||||
"when": "config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.mockVariantAnalysisView",
|
||||
"when": "config.codeQL.canary && config.codeQL.variantAnalysis.liveResults"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runQueries",
|
||||
"when": "false"
|
||||
@@ -921,6 +929,10 @@
|
||||
"command": "codeQL.viewCfg",
|
||||
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabaseLgtm",
|
||||
"when": "config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.setCurrentDatabase",
|
||||
"when": "false"
|
||||
@@ -1166,7 +1178,7 @@
|
||||
},
|
||||
{
|
||||
"view": "codeQLDatabases",
|
||||
"contents": "Add a CodeQL database:\n[From a folder](command:codeQLDatabases.chooseDatabaseFolder)\n[From an archive](command:codeQLDatabases.chooseDatabaseArchive)\n[From a URL (as a zip file)](command:codeQLDatabases.chooseDatabaseInternet)\n[From GitHub](command:codeQLDatabases.chooseDatabaseGithub)\n[From LGTM](command:codeQLDatabases.chooseDatabaseLgtm)"
|
||||
"contents": "Add a CodeQL database:\n[From a folder](command:codeQLDatabases.chooseDatabaseFolder)\n[From an archive](command:codeQLDatabases.chooseDatabaseArchive)\n[From a URL (as a zip file)](command:codeQLDatabases.chooseDatabaseInternet)\n[From GitHub](command:codeQLDatabases.chooseDatabaseGithub)"
|
||||
},
|
||||
{
|
||||
"view": "codeQLEvalLogViewer",
|
||||
@@ -1179,7 +1191,9 @@
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:extension": "tsc --watch",
|
||||
"watch:webpack": "gulp watchView",
|
||||
"test": "mocha --exit -r ts-node/register test/pure-tests/**/*.ts",
|
||||
"test": "npm-run-all -p test:*",
|
||||
"test:unit": "mocha --exit -r ts-node/register -r test/mocha.setup.js test/pure-tests/**/*.ts",
|
||||
"test:view": "jest",
|
||||
"preintegration": "rm -rf ./out/vscode-tests && gulp",
|
||||
"integration": "node ./out/vscode-tests/run-integration-tests.js no-workspace,minimal-workspace",
|
||||
"cli-integration": "npm run preintegration && node ./out/vscode-tests/run-integration-tests.js cli-integration",
|
||||
@@ -1231,6 +1245,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
|
||||
"@storybook/addon-actions": "^6.5.10",
|
||||
"@storybook/addon-essentials": "^6.5.10",
|
||||
"@storybook/addon-interactions": "^6.5.10",
|
||||
@@ -1239,6 +1254,9 @@
|
||||
"@storybook/manager-webpack5": "^6.5.10",
|
||||
"@storybook/react": "^6.5.10",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/chai-as-promised": "~7.1.2",
|
||||
"@types/child-process-promise": "^2.2.1",
|
||||
@@ -1252,6 +1270,7 @@
|
||||
"@types/gulp": "^4.0.9",
|
||||
"@types/gulp-replace": "^1.1.0",
|
||||
"@types/gulp-sourcemaps": "0.0.32",
|
||||
"@types/jest": "^29.0.2",
|
||||
"@types/js-yaml": "^3.12.5",
|
||||
"@types/jszip": "~3.1.6",
|
||||
"@types/mocha": "^9.0.0",
|
||||
@@ -1277,14 +1296,16 @@
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||
"@typescript-eslint/parser": "^4.26.0",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"applicationinsights": "^1.8.7",
|
||||
"applicationinsights": "^2.3.5",
|
||||
"babel-loader": "^8.2.5",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "~7.1.1",
|
||||
"css-loader": "~3.1.0",
|
||||
"del": "^6.0.0",
|
||||
"eslint": "~6.8.0",
|
||||
"eslint-plugin-jest-dom": "^4.0.2",
|
||||
"eslint-plugin-react": "~7.19.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-storybook": "^0.6.4",
|
||||
"file-loader": "^6.2.0",
|
||||
"glob": "^7.1.4",
|
||||
@@ -1293,6 +1314,8 @@
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-typescript": "^5.0.1",
|
||||
"husky": "~4.3.8",
|
||||
"jest": "^29.0.3",
|
||||
"jest-environment-jsdom": "^29.0.3",
|
||||
"lint-staged": "~10.2.2",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"mocha": "^10.0.0",
|
||||
@@ -1300,9 +1323,10 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "~2.0.5",
|
||||
"proxyquire": "~2.1.3",
|
||||
"sinon": "~13.0.1",
|
||||
"sinon": "~14.0.0",
|
||||
"sinon-chai": "~3.5.0",
|
||||
"through2": "^4.0.2",
|
||||
"ts-jest": "^29.0.1",
|
||||
"ts-loader": "^8.1.0",
|
||||
"ts-node": "^10.7.0",
|
||||
"ts-protoc-gen": "^0.9.0",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { DisposableObject } from './pure/disposable-object';
|
||||
import { tmpDir } from './helpers';
|
||||
import { getHtmlForWebview, WebviewMessage, WebviewView } from './interface-utils';
|
||||
|
||||
export type InterfacePanelConfig = {
|
||||
export type WebviewPanelConfig = {
|
||||
viewId: string;
|
||||
title: string;
|
||||
viewColumn: ViewColumn;
|
||||
@@ -22,7 +22,7 @@ export type InterfacePanelConfig = {
|
||||
additionalOptions?: WebviewPanelOptions & WebviewOptions;
|
||||
}
|
||||
|
||||
export abstract class AbstractInterfaceManager<ToMessage extends WebviewMessage, FromMessage extends WebviewMessage> extends DisposableObject {
|
||||
export abstract class AbstractWebview<ToMessage extends WebviewMessage, FromMessage extends WebviewMessage> extends DisposableObject {
|
||||
protected panel: WebviewPanel | undefined;
|
||||
protected panelLoaded = false;
|
||||
protected panelLoadedCallBacks: (() => void)[] = [];
|
||||
@@ -90,7 +90,7 @@ export abstract class AbstractInterfaceManager<ToMessage extends WebviewMessage,
|
||||
return this.panel;
|
||||
}
|
||||
|
||||
protected abstract getPanelConfig(): InterfacePanelConfig;
|
||||
protected abstract getPanelConfig(): WebviewPanelConfig;
|
||||
|
||||
protected abstract onPanelDispose(): void;
|
||||
|
||||
@@ -17,14 +17,14 @@ import resultsDiff from './resultsDiff';
|
||||
import { CompletedLocalQueryInfo } from '../query-results';
|
||||
import { getErrorMessage } from '../pure/helpers-pure';
|
||||
import { HistoryItemLabelProvider } from '../history-item-label-provider';
|
||||
import { AbstractInterfaceManager, InterfacePanelConfig } from '../abstract-interface-manager';
|
||||
import { AbstractWebview, WebviewPanelConfig } from '../abstract-webview';
|
||||
|
||||
interface ComparePair {
|
||||
from: CompletedLocalQueryInfo;
|
||||
to: CompletedLocalQueryInfo;
|
||||
}
|
||||
|
||||
export class CompareInterfaceManager extends AbstractInterfaceManager<ToCompareViewMessage, FromCompareViewMessage> {
|
||||
export class CompareView extends AbstractWebview<ToCompareViewMessage, FromCompareViewMessage> {
|
||||
private comparePair: ComparePair | undefined;
|
||||
|
||||
constructor(
|
||||
@@ -95,7 +95,7 @@ export class CompareInterfaceManager extends AbstractInterfaceManager<ToCompareV
|
||||
}
|
||||
}
|
||||
|
||||
protected getPanelConfig(): InterfacePanelConfig {
|
||||
protected getPanelConfig(): WebviewPanelConfig {
|
||||
return {
|
||||
viewId: 'compareView',
|
||||
title: 'Compare CodeQL Query Results',
|
||||
@@ -111,7 +111,7 @@ export class CompareInterfaceManager extends AbstractInterfaceManager<ToCompareV
|
||||
|
||||
protected async onMessage(msg: FromCompareViewMessage): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case 'compareViewLoaded':
|
||||
case 'viewLoaded':
|
||||
this.onWebViewLoaded();
|
||||
break;
|
||||
|
||||
@@ -387,3 +387,13 @@ export function getActionBranch(): string {
|
||||
export function isIntegrationTestMode() {
|
||||
return process.env.INTEGRATION_TEST_MODE === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* A flag indicating whether to enable the experimental "live results" feature
|
||||
* for multi-repo variant analyses.
|
||||
*/
|
||||
const LIVE_RESULTS = new Setting('liveResults', REMOTE_QUERIES_SETTING);
|
||||
|
||||
export function isVariantAnalysisLiveResultsEnabled(): boolean {
|
||||
return !!LIVE_RESULTS.getValue<boolean>();
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ import {
|
||||
} from './helpers';
|
||||
import { asError, assertNever, getErrorMessage } from './pure/helpers-pure';
|
||||
import { spawnIdeServer } from './ide-server';
|
||||
import { InterfaceManager } from './interface';
|
||||
import { ResultsView } from './interface';
|
||||
import { WebviewReveal } from './interface-utils';
|
||||
import { ideServerLogger, logger, ProgressReporter, queryServerLogger } from './logging';
|
||||
import { QueryHistoryManager } from './query-history';
|
||||
@@ -77,7 +77,7 @@ import * as qsClient from './legacy-query-server/queryserver-client';
|
||||
import { displayQuickQuery } from './quick-query';
|
||||
import { QLTestAdapterFactory } from './test-adapter';
|
||||
import { TestUIService } from './test-ui';
|
||||
import { CompareInterfaceManager } from './compare/compare-interface';
|
||||
import { CompareView } from './compare/compare-view';
|
||||
import { gatherQlFiles } from './pure/files';
|
||||
import { initializeTelemetry } from './telemetry';
|
||||
import {
|
||||
@@ -104,6 +104,7 @@ 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';
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -448,8 +449,8 @@ async function activateWithInstalledDistribution(
|
||||
const labelProvider = new HistoryItemLabelProvider(queryHistoryConfigurationListener);
|
||||
|
||||
void logger.log('Initializing results panel interface.');
|
||||
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger, labelProvider);
|
||||
ctx.subscriptions.push(intm);
|
||||
const localQueryResultsView = new ResultsView(ctx, dbm, cliServer, queryServerLogger, labelProvider);
|
||||
ctx.subscriptions.push(localQueryResultsView);
|
||||
|
||||
void logger.log('Initializing variant analysis manager.');
|
||||
const rqm = new RemoteQueriesManager(ctx, cliServer, queryStorageDir, logger);
|
||||
@@ -459,7 +460,7 @@ async function activateWithInstalledDistribution(
|
||||
const qhm = new QueryHistoryManager(
|
||||
qs,
|
||||
dbm,
|
||||
intm,
|
||||
localQueryResultsView,
|
||||
rqm,
|
||||
evalLogViewer,
|
||||
queryStorageDir,
|
||||
@@ -481,8 +482,8 @@ async function activateWithInstalledDistribution(
|
||||
void logger.log('Reading query history');
|
||||
await qhm.readQueryHistory();
|
||||
|
||||
void logger.log('Initializing compare panel interface.');
|
||||
const cmpm = new CompareInterfaceManager(
|
||||
void logger.log('Initializing compare view.');
|
||||
const compareView = new CompareView(
|
||||
ctx,
|
||||
dbm,
|
||||
cliServer,
|
||||
@@ -490,7 +491,7 @@ async function activateWithInstalledDistribution(
|
||||
labelProvider,
|
||||
showResults
|
||||
);
|
||||
ctx.subscriptions.push(cmpm);
|
||||
ctx.subscriptions.push(compareView);
|
||||
|
||||
void logger.log('Initializing source archive filesystem provider.');
|
||||
archiveFilesystemProvider.activate(ctx);
|
||||
@@ -500,7 +501,7 @@ async function activateWithInstalledDistribution(
|
||||
to: CompletedLocalQueryInfo
|
||||
): Promise<void> {
|
||||
try {
|
||||
await cmpm.showResults(from, to);
|
||||
await compareView.showResults(from, to);
|
||||
} catch (e) {
|
||||
void showAndLogErrorMessage(getErrorMessage(e));
|
||||
}
|
||||
@@ -510,7 +511,7 @@ async function activateWithInstalledDistribution(
|
||||
query: CompletedLocalQueryInfo,
|
||||
forceReveal: WebviewReveal
|
||||
): Promise<void> {
|
||||
await intm.showResults(query, forceReveal, false);
|
||||
await localQueryResultsView.showResults(query, forceReveal, false);
|
||||
}
|
||||
|
||||
async function compileAndRunQuery(
|
||||
@@ -901,8 +902,15 @@ async function activateWithInstalledDistribution(
|
||||
}));
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.exportVariantAnalysisResults', async () => {
|
||||
await exportRemoteQueryResults(qhm, rqm, ctx);
|
||||
commandRunner('codeQL.exportVariantAnalysisResults', async (queryId?: string) => {
|
||||
await exportRemoteQueryResults(qhm, rqm, ctx, queryId);
|
||||
})
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.mockVariantAnalysisView', async () => {
|
||||
const variantAnalysisView = new VariantAnalysisView(ctx);
|
||||
variantAnalysisView.openView();
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ export function tryResolveLocation(
|
||||
}
|
||||
}
|
||||
|
||||
export type WebviewView = 'results' | 'compare' | 'remote-queries';
|
||||
export type WebviewView = 'results' | 'compare' | 'remote-queries' | 'variant-analysis';
|
||||
|
||||
export interface WebviewMessage {
|
||||
t: string;
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
} from './interface-utils';
|
||||
import { getDefaultResultSetName, ParsedResultSets } from './pure/interface-types';
|
||||
import { RawResultSet, transformBqrsResultSet, ResultSetSchema } from './pure/bqrs-cli-types';
|
||||
import { AbstractInterfaceManager, InterfacePanelConfig } from './abstract-interface-manager';
|
||||
import { AbstractWebview, WebviewPanelConfig } from './abstract-webview';
|
||||
import { PAGE_SIZE } from './config';
|
||||
import { CompletedLocalQueryInfo } from './query-results';
|
||||
import { HistoryItemLabelProvider } from './history-item-label-provider';
|
||||
@@ -119,7 +119,7 @@ function numInterpretedPages(interpretation: Interpretation | undefined): number
|
||||
return Math.ceil(n / pageSize);
|
||||
}
|
||||
|
||||
export class InterfaceManager extends AbstractInterfaceManager<IntoResultsViewMsg, FromResultsViewMsg> {
|
||||
export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResultsViewMsg> {
|
||||
private _displayedQuery?: CompletedLocalQueryInfo;
|
||||
private _interpretation?: Interpretation;
|
||||
|
||||
@@ -173,7 +173,7 @@ export class InterfaceManager extends AbstractInterfaceManager<IntoResultsViewMs
|
||||
await this.postMessage({ t: 'navigatePath', direction });
|
||||
}
|
||||
|
||||
protected getPanelConfig(): InterfacePanelConfig {
|
||||
protected getPanelConfig(): WebviewPanelConfig {
|
||||
return {
|
||||
viewId: 'resultsView',
|
||||
title: 'CodeQL Query Results',
|
||||
@@ -190,7 +190,7 @@ export class InterfaceManager extends AbstractInterfaceManager<IntoResultsViewMs
|
||||
protected async onMessage(msg: FromResultsViewMsg): Promise<void> {
|
||||
try {
|
||||
switch (msg.t) {
|
||||
case 'resultViewLoaded':
|
||||
case 'viewLoaded':
|
||||
this.onWebViewLoaded();
|
||||
break;
|
||||
case 'viewSourceFile': {
|
||||
|
||||
@@ -193,7 +193,14 @@ export async function upgradeDatabaseExplicit(
|
||||
void qs.logger.log('Running the following database upgrade:');
|
||||
|
||||
getUpgradeDescriptions(compileUpgradeResult.compiledUpgrades).map(s => s.description).join('\n');
|
||||
return await runDatabaseUpgrade(qs, dbItem, compileUpgradeResult.compiledUpgrades, progress, token);
|
||||
const result = await runDatabaseUpgrade(qs, dbItem, compileUpgradeResult.compiledUpgrades, progress, token);
|
||||
|
||||
// TODO Can remove the next lines when https://github.com/github/codeql-team/issues/1241 is fixed
|
||||
// restart the query server to avoid a bug in the CLI where the upgrade is applied, but the old dbscheme
|
||||
// is still cached in memory.
|
||||
|
||||
await qs.restartQueryServer(progress, token);
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
void showAndLogErrorMessage(`Database upgrade failed: ${e}`);
|
||||
|
||||
26
extensions/ql-vscode/src/pure/date.ts
Normal file
26
extensions/ql-vscode/src/pure/date.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Contains an assortment of helper constants and functions for working with dates.
|
||||
*/
|
||||
|
||||
const dateWithoutYearFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
});
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
});
|
||||
|
||||
export function formatDate(value: Date): string {
|
||||
if (value.getFullYear() === new Date().getFullYear()) {
|
||||
return dateWithoutYearFormatter.format(value);
|
||||
}
|
||||
|
||||
return dateFormatter.format(value);
|
||||
}
|
||||
@@ -174,7 +174,7 @@ export type FromResultsViewMsg =
|
||||
| ToggleDiagnostics
|
||||
| ChangeRawResultsSortMsg
|
||||
| ChangeInterpretedResultsSortMsg
|
||||
| ResultViewLoaded
|
||||
| ViewLoadedMsg
|
||||
| ChangePage
|
||||
| OpenFileMsg;
|
||||
|
||||
@@ -216,11 +216,11 @@ interface ToggleDiagnostics {
|
||||
}
|
||||
|
||||
/**
|
||||
* Message from the results view to signal that loading the results
|
||||
* is complete.
|
||||
* Message from a view signal that loading is complete.
|
||||
*/
|
||||
interface ResultViewLoaded {
|
||||
t: 'resultViewLoaded';
|
||||
interface ViewLoadedMsg {
|
||||
t: 'viewLoaded';
|
||||
viewName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,18 +279,11 @@ interface ChangeInterpretedResultsSortMsg {
|
||||
* Message from the compare view to the extension.
|
||||
*/
|
||||
export type FromCompareViewMessage =
|
||||
| CompareViewLoadedMessage
|
||||
| ViewLoadedMsg
|
||||
| ChangeCompareMessage
|
||||
| ViewSourceFileMsg
|
||||
| OpenQueryMessage;
|
||||
|
||||
/**
|
||||
* Message from the compare view to signal the completion of loading results.
|
||||
*/
|
||||
interface CompareViewLoadedMessage {
|
||||
t: 'compareViewLoaded';
|
||||
}
|
||||
|
||||
/**
|
||||
* Message from the compare view to request opening a query.
|
||||
*/
|
||||
@@ -389,7 +382,7 @@ export interface ParsedResultSets {
|
||||
}
|
||||
|
||||
export type FromRemoteQueriesMessage =
|
||||
| RemoteQueryLoadedMessage
|
||||
| ViewLoadedMsg
|
||||
| RemoteQueryErrorMessage
|
||||
| OpenFileMsg
|
||||
| OpenVirtualFileMsg
|
||||
@@ -402,10 +395,6 @@ export type ToRemoteQueriesMessage =
|
||||
| SetRemoteQueryResultMessage
|
||||
| SetAnalysesResultsMessage;
|
||||
|
||||
export interface RemoteQueryLoadedMessage {
|
||||
t: 'remoteQueryLoaded';
|
||||
}
|
||||
|
||||
export interface SetRemoteQueryResultMessage {
|
||||
t: 'setRemoteQueryResult';
|
||||
queryResult: RemoteQueryResult
|
||||
@@ -433,6 +422,7 @@ export interface RemoteQueryDownloadAllAnalysesResultsMessage {
|
||||
|
||||
export interface RemoteQueryExportResultsMessage {
|
||||
t: 'remoteQueryExportResults';
|
||||
queryId: string;
|
||||
}
|
||||
|
||||
export interface CopyRepoListMessage {
|
||||
|
||||
15
extensions/ql-vscode/src/pure/number.ts
Normal file
15
extensions/ql-vscode/src/pure/number.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Contains an assortment of helper constants and functions for working with numbers.
|
||||
*/
|
||||
|
||||
const numberFormatter = new Intl.NumberFormat('en-US');
|
||||
|
||||
/**
|
||||
* Formats a number to be human-readable with decimal places and thousands separators.
|
||||
*
|
||||
* @param value The number to format.
|
||||
* @returns The formatted number. For example, "10,000", "1,000,000", or "1,000,000,000".
|
||||
*/
|
||||
export function formatDecimal(value: number): string {
|
||||
return numberFormatter.format(value);
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
* Contains an assortment of helper constants and functions for working with time, dates, and durations.
|
||||
*/
|
||||
|
||||
export const ONE_MINUTE_IN_MS = 1000 * 60;
|
||||
export const ONE_SECOND_IN_MS = 1000;
|
||||
export const ONE_MINUTE_IN_MS = ONE_SECOND_IN_MS * 60;
|
||||
export const ONE_HOUR_IN_MS = ONE_MINUTE_IN_MS * 60;
|
||||
export const TWO_HOURS_IN_MS = ONE_HOUR_IN_MS * 2;
|
||||
export const THREE_HOURS_IN_MS = ONE_HOUR_IN_MS * 3;
|
||||
@@ -43,20 +44,23 @@ export function humanizeRelativeTime(relativeTimeMillis?: number) {
|
||||
|
||||
/**
|
||||
* Converts a number of milliseconds into a human-readable string with units, indicating an amount of time.
|
||||
* Negative numbers have no meaning and are considered to be "Less than a minute".
|
||||
* Negative numbers have no meaning and are considered to be "Less than a second".
|
||||
*
|
||||
* @param millis The number of milliseconds to convert.
|
||||
* @returns A humanized duration. For example, "2 minutes", "2 hours", "2 days", or "2 months".
|
||||
* @returns A humanized duration. For example, "2 seconds", "2 minutes", "2 hours", "2 days", or "2 months".
|
||||
*/
|
||||
export function humanizeUnit(millis?: number): string {
|
||||
// assume a blank or empty string is a zero
|
||||
// assume anything less than 0 is a zero
|
||||
if (!millis || millis < ONE_MINUTE_IN_MS) {
|
||||
return 'Less than a minute';
|
||||
if (!millis || millis < ONE_SECOND_IN_MS) {
|
||||
return 'Less than a second';
|
||||
}
|
||||
let unit: string;
|
||||
let unitDiff: number;
|
||||
if (millis < ONE_HOUR_IN_MS) {
|
||||
if (millis < ONE_MINUTE_IN_MS) {
|
||||
unit = 'second';
|
||||
unitDiff = Math.floor(millis / ONE_SECOND_IN_MS);
|
||||
} else if (millis < ONE_HOUR_IN_MS) {
|
||||
unit = 'minute';
|
||||
unitDiff = Math.floor(millis / ONE_MINUTE_IN_MS);
|
||||
} else if (millis < ONE_DAY_IN_MS) {
|
||||
|
||||
11
extensions/ql-vscode/src/pure/zip.ts
Normal file
11
extensions/ql-vscode/src/pure/zip.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as unzipper from 'unzipper';
|
||||
|
||||
/**
|
||||
* Unzips a zip file to a directory.
|
||||
* @param sourcePath The path to the zip file.
|
||||
* @param destinationPath The path to the directory to unzip to.
|
||||
*/
|
||||
export async function unzipFile(sourcePath: string, destinationPath: string) {
|
||||
const file = await unzipper.Open.file(sourcePath);
|
||||
await file.extract({ path: destinationPath });
|
||||
}
|
||||
@@ -39,10 +39,10 @@ import * as fs from 'fs-extra';
|
||||
import { CliVersionConstraint } from './cli';
|
||||
import { HistoryItemLabelProvider } from './history-item-label-provider';
|
||||
import { Credentials } from './authentication';
|
||||
import { cancelRemoteQuery } from './remote-queries/gh-actions-api-client';
|
||||
import { cancelRemoteQuery } from './remote-queries/gh-api/gh-actions-api-client';
|
||||
import { RemoteQueriesManager } from './remote-queries/remote-queries-manager';
|
||||
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
|
||||
import { InterfaceManager } from './interface';
|
||||
import { ResultsView } from './interface';
|
||||
import { WebviewReveal } from './interface-utils';
|
||||
import { EvalLogViewer } from './eval-log-viewer';
|
||||
import EvalLogTreeBuilder from './eval-log-tree-builder';
|
||||
@@ -331,7 +331,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
constructor(
|
||||
private readonly qs: QueryRunner,
|
||||
private readonly dbm: DatabaseManager,
|
||||
private readonly localQueriesInterfaceManager: InterfaceManager,
|
||||
private readonly localQueriesResultsView: ResultsView,
|
||||
private readonly remoteQueriesManager: RemoteQueriesManager,
|
||||
private readonly evalLogViewer: EvalLogViewer,
|
||||
private readonly queryStorageDir: string,
|
||||
@@ -680,6 +680,10 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
return this.treeDataProvider.getCurrent();
|
||||
}
|
||||
|
||||
getRemoteQueryById(queryId: string): RemoteQueryHistoryItem | undefined {
|
||||
return this.treeDataProvider.allHistory.find(i => i.t === 'remote' && i.queryId === queryId) as RemoteQueryHistoryItem;
|
||||
}
|
||||
|
||||
async removeDeletedQueries() {
|
||||
await Promise.all(this.treeDataProvider.allHistory.map(async (item) => {
|
||||
if (item.t == 'local' && item.completedQuery && !(await fs.pathExists(item.completedQuery?.query.querySaveDir))) {
|
||||
@@ -1360,7 +1364,7 @@ the file in the file explorer and dragging it into the workspace.`
|
||||
|
||||
private async openQueryResults(item: QueryHistoryInfo) {
|
||||
if (item.t === 'local') {
|
||||
await this.localQueriesInterfaceManager.showResults(item as CompletedLocalQueryInfo, WebviewReveal.Forced, false);
|
||||
await this.localQueriesResultsView.showResults(item as CompletedLocalQueryInfo, WebviewReveal.Forced, false);
|
||||
}
|
||||
else if (item.t === 'remote') {
|
||||
await this.remoteQueriesManager.openRemoteQueryResults(item.queryId);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CancellationToken, ExtensionContext } from 'vscode';
|
||||
|
||||
import { Credentials } from '../authentication';
|
||||
import { Logger } from '../logging';
|
||||
import { downloadArtifactFromLink } from './gh-actions-api-client';
|
||||
import { downloadArtifactFromLink } from './gh-api/gh-actions-api-client';
|
||||
import { AnalysisSummary } from './shared/remote-query-result';
|
||||
import { AnalysisResults, AnalysisAlert, AnalysisRawResults } from './shared/analysis-result';
|
||||
import { UserCancellationException } from '../commandRunner';
|
||||
|
||||
@@ -10,31 +10,46 @@ import {
|
||||
} from '../helpers';
|
||||
import { logger } from '../logging';
|
||||
import { QueryHistoryManager } from '../query-history';
|
||||
import { createGist } from './gh-actions-api-client';
|
||||
import { createGist } from './gh-api/gh-actions-api-client';
|
||||
import { RemoteQueriesManager } from './remote-queries-manager';
|
||||
import { generateMarkdown } from './remote-queries-markdown-generation';
|
||||
import { RemoteQuery } from './remote-query';
|
||||
import { AnalysisResults, sumAnalysesResults } from './shared/analysis-result';
|
||||
import { RemoteQueryHistoryItem } from './remote-query-history-item';
|
||||
|
||||
/**
|
||||
* Exports the results of the currently-selected remote query.
|
||||
* Exports the results of the given or currently-selected remote query.
|
||||
* The user is prompted to select the export format.
|
||||
*/
|
||||
export async function exportRemoteQueryResults(
|
||||
queryHistoryManager: QueryHistoryManager,
|
||||
remoteQueriesManager: RemoteQueriesManager,
|
||||
ctx: ExtensionContext,
|
||||
queryId?: string,
|
||||
): Promise<void> {
|
||||
const queryHistoryItem = queryHistoryManager.getCurrentQueryHistoryItem();
|
||||
if (!queryHistoryItem || queryHistoryItem.t !== 'remote') {
|
||||
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
|
||||
} else if (!queryHistoryItem.completed) {
|
||||
let queryHistoryItem: RemoteQueryHistoryItem;
|
||||
if (queryId) {
|
||||
const query = queryHistoryManager.getRemoteQueryById(queryId);
|
||||
if (!query) {
|
||||
void logger.log(`Could not find query with id ${queryId}`);
|
||||
throw new Error('There was an error when trying to retrieve variant analysis information');
|
||||
}
|
||||
queryHistoryItem = query;
|
||||
} else {
|
||||
const query = queryHistoryManager.getCurrentQueryHistoryItem();
|
||||
if (!query || query.t !== 'remote') {
|
||||
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
|
||||
}
|
||||
queryHistoryItem = query;
|
||||
}
|
||||
|
||||
if (!queryHistoryItem.completed) {
|
||||
throw new Error('Variant analysis results are not yet available.');
|
||||
}
|
||||
const queryId = queryHistoryItem.queryId;
|
||||
void logger.log(`Exporting variant analysis results for query: ${queryId}`);
|
||||
|
||||
void logger.log(`Exporting variant analysis results for query: ${queryHistoryItem.queryId}`);
|
||||
const query = queryHistoryItem.remoteQuery;
|
||||
const analysesResults = remoteQueriesManager.getAnalysesResults(queryId);
|
||||
const analysesResults = remoteQueriesManager.getAnalysesResults(queryHistoryItem.queryId);
|
||||
|
||||
const gistOption = {
|
||||
label: '$(ports-open-browser-icon) Create Gist (GitHub)',
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import * as unzipper from 'unzipper';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import { showAndLogErrorMessage, showAndLogWarningMessage, tmpDir } from '../helpers';
|
||||
import { Credentials } from '../authentication';
|
||||
import { logger } from '../logging';
|
||||
import { RemoteQueryWorkflowResult } from './remote-query-workflow-result';
|
||||
import { DownloadLink, createDownloadPath } from './download-link';
|
||||
import { RemoteQuery } from './remote-query';
|
||||
import { RemoteQueryFailureIndexItem, RemoteQueryResultIndex, RemoteQuerySuccessIndexItem } from './remote-query-result-index';
|
||||
import { getErrorMessage } from '../pure/helpers-pure';
|
||||
import { showAndLogErrorMessage, showAndLogWarningMessage, tmpDir } from '../../helpers';
|
||||
import { Credentials } from '../../authentication';
|
||||
import { logger } from '../../logging';
|
||||
import { RemoteQueryWorkflowResult } from '../remote-query-workflow-result';
|
||||
import { DownloadLink, createDownloadPath } from '../download-link';
|
||||
import { RemoteQuery } from '../remote-query';
|
||||
import { RemoteQueryFailureIndexItem, RemoteQueryResultIndex, RemoteQuerySuccessIndexItem } from '../remote-query-result-index';
|
||||
import { getErrorMessage } from '../../pure/helpers-pure';
|
||||
import { unzipFile } from '../../pure/zip';
|
||||
|
||||
export const RESULT_INDEX_ARTIFACT_NAME = 'result-index';
|
||||
|
||||
@@ -110,10 +110,8 @@ export async function downloadArtifactFromLink(
|
||||
const response = await octokit.request(`GET ${downloadLink.urlPath}/zip`, {});
|
||||
|
||||
const zipFilePath = createDownloadPath(storagePath, downloadLink, 'zip');
|
||||
await saveFile(`${zipFilePath}`, response.data as ArrayBuffer);
|
||||
|
||||
// Extract the zipped artifact.
|
||||
await unzipFile(zipFilePath, extractedPath);
|
||||
await unzipBuffer(response.data as ArrayBuffer, zipFilePath, extractedPath);
|
||||
}
|
||||
return path.join(extractedPath, downloadLink.innerFilePath || '');
|
||||
}
|
||||
@@ -300,20 +298,16 @@ async function downloadArtifact(
|
||||
archive_format: 'zip',
|
||||
});
|
||||
const artifactPath = path.join(tmpDir.name, `${artifactId}`);
|
||||
await saveFile(`${artifactPath}.zip`, response.data as ArrayBuffer);
|
||||
await unzipFile(`${artifactPath}.zip`, artifactPath);
|
||||
await unzipBuffer(response.data as ArrayBuffer, `${artifactPath}.zip`, artifactPath);
|
||||
return artifactPath;
|
||||
}
|
||||
|
||||
async function saveFile(filePath: string, data: ArrayBuffer): Promise<void> {
|
||||
async function unzipBuffer(data: ArrayBuffer, filePath: string, destinationPath: string): Promise<void> {
|
||||
void logger.log(`Saving file to ${filePath}`);
|
||||
await fs.writeFile(filePath, Buffer.from(data));
|
||||
}
|
||||
|
||||
async function unzipFile(sourcePath: string, destinationPath: string) {
|
||||
void logger.log(`Unzipping file to ${destinationPath}`);
|
||||
const file = await unzipper.Open.file(sourcePath);
|
||||
await file.extract({ path: destinationPath });
|
||||
await unzipFile(filePath, destinationPath);
|
||||
}
|
||||
|
||||
function getWorkflowError(conclusion: string | null): string {
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Credentials } from '../../authentication';
|
||||
import { OctokitResponse } from '@octokit/types/dist-types';
|
||||
import { VariantAnalysisSubmission } from '../shared/variant-analysis';
|
||||
import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisRepoTask,
|
||||
VariantAnalysisSubmissionRequest
|
||||
} from './variant-analysis';
|
||||
|
||||
export async function submitVariantAnalysis(
|
||||
credentials: Credentials,
|
||||
submissionDetails: VariantAnalysisSubmission
|
||||
): Promise<VariantAnalysis> {
|
||||
const octokit = await credentials.getOctokit();
|
||||
|
||||
const { actionRepoRef, query, databases, controllerRepoId } = submissionDetails;
|
||||
|
||||
const data: VariantAnalysisSubmissionRequest = {
|
||||
action_repo_ref: actionRepoRef,
|
||||
language: query.language,
|
||||
query_pack: query.pack,
|
||||
repositories: databases.repositories,
|
||||
repository_lists: databases.repositoryLists,
|
||||
repository_owners: databases.repositoryOwners,
|
||||
};
|
||||
|
||||
const response: OctokitResponse<VariantAnalysis> = await octokit.request(
|
||||
'POST /repositories/:controllerRepoId/code-scanning/codeql/variant-analyses',
|
||||
{
|
||||
controllerRepoId,
|
||||
data
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getVariantAnalysis(
|
||||
credentials: Credentials,
|
||||
controllerRepoId: number,
|
||||
variantAnalysisId: number
|
||||
): Promise<VariantAnalysis> {
|
||||
const octokit = await credentials.getOctokit();
|
||||
|
||||
const response: OctokitResponse<VariantAnalysis> = await octokit.request(
|
||||
'GET /repositories/:controllerRepoId/code-scanning/codeql/variant-analyses/:variantAnalysisId',
|
||||
{
|
||||
controllerRepoId,
|
||||
variantAnalysisId
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getVariantAnalysisRepo(
|
||||
credentials: Credentials,
|
||||
controllerRepoId: number,
|
||||
variantAnalysisId: number,
|
||||
repoId: number
|
||||
): Promise<VariantAnalysisRepoTask> {
|
||||
const octokit = await credentials.getOctokit();
|
||||
|
||||
const response: OctokitResponse<VariantAnalysisRepoTask> = await octokit.request(
|
||||
'GET /repositories/:controllerRepoId/code-scanning/codeql/variant-analyses/:variantAnalysisId/repositories/:repoId',
|
||||
{
|
||||
controllerRepoId,
|
||||
variantAnalysisId,
|
||||
repoId
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getRepositoryIdFromNwo(
|
||||
credentials: Credentials,
|
||||
owner: string,
|
||||
repo: string
|
||||
): Promise<number> {
|
||||
const octokit = await credentials.getOctokit();
|
||||
|
||||
const response = await octokit.rest.repos.get({ owner, repo });
|
||||
return response.data.id;
|
||||
}
|
||||
13
extensions/ql-vscode/src/remote-queries/gh-api/repository.ts
Normal file
13
extensions/ql-vscode/src/remote-queries/gh-api/repository.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Defines basic information about a repository.
|
||||
*
|
||||
* Different parts of the API may return different subsets of information
|
||||
* about a repository, but this model represents the very basic information
|
||||
* that will always be available.
|
||||
*/
|
||||
export interface Repository {
|
||||
id: number,
|
||||
name: string,
|
||||
full_name: string,
|
||||
private: boolean,
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Repository } from './repository';
|
||||
|
||||
export interface VariantAnalysisSubmissionRequest {
|
||||
action_repo_ref: string,
|
||||
language: VariantAnalysisQueryLanguage,
|
||||
query_pack: string,
|
||||
repositories?: string[],
|
||||
repository_lists?: string[],
|
||||
repository_owners?: string[]
|
||||
}
|
||||
|
||||
export type VariantAnalysisQueryLanguage =
|
||||
| 'csharp'
|
||||
| 'cpp'
|
||||
| 'go'
|
||||
| 'java'
|
||||
| 'javascript'
|
||||
| 'python'
|
||||
| 'ruby';
|
||||
|
||||
export interface VariantAnalysis {
|
||||
id: number,
|
||||
controller_repo: Repository,
|
||||
actor_id: number,
|
||||
query_language: VariantAnalysisQueryLanguage,
|
||||
query_pack_url: string,
|
||||
status: VariantAnalysisStatus,
|
||||
actions_workflow_run_id?: number,
|
||||
failure_reason?: VariantAnalysisFailureReason,
|
||||
scanned_repositories?: VariantAnalysisScannedRepository[],
|
||||
skipped_repositories?: VariantAnalysisSkippedRepositories
|
||||
}
|
||||
|
||||
export type VariantAnalysisStatus =
|
||||
| 'in_progress'
|
||||
| 'completed';
|
||||
|
||||
export type VariantAnalysisFailureReason =
|
||||
| 'no_repos_queried'
|
||||
| 'internal_error';
|
||||
|
||||
export type VariantAnalysisRepoStatus =
|
||||
| 'pending'
|
||||
| 'in_progress'
|
||||
| 'succeeded'
|
||||
| 'failed'
|
||||
| 'canceled'
|
||||
| 'timed_out';
|
||||
|
||||
export interface VariantAnalysisScannedRepository {
|
||||
repository: Repository,
|
||||
analysis_status: VariantAnalysisRepoStatus,
|
||||
result_count?: number,
|
||||
artifact_size_in_bytes?: number,
|
||||
failure_message?: string
|
||||
}
|
||||
|
||||
export interface VariantAnalysisSkippedRepositoryGroup {
|
||||
repository_count: number,
|
||||
repositories: Repository[]
|
||||
}
|
||||
|
||||
export interface VariantAnalysisNotFoundRepositoryGroup {
|
||||
repository_count: number,
|
||||
repository_nwos: string[]
|
||||
}
|
||||
export interface VariantAnalysisRepoTask {
|
||||
repository: Repository,
|
||||
analysis_status: VariantAnalysisRepoStatus,
|
||||
artifact_size_in_bytes?: number,
|
||||
result_count?: number,
|
||||
failure_message?: string,
|
||||
database_commit_sha?: string,
|
||||
source_location_prefix?: string,
|
||||
artifact_url?: string
|
||||
}
|
||||
|
||||
export interface VariantAnalysisSkippedRepositories {
|
||||
access_mismatch_repos: VariantAnalysisSkippedRepositoryGroup,
|
||||
not_found_repo_nwos: VariantAnalysisNotFoundRepositoryGroup,
|
||||
no_codeql_db_repos: VariantAnalysisSkippedRepositoryGroup,
|
||||
over_limit_repos: VariantAnalysisSkippedRepositoryGroup
|
||||
}
|
||||
@@ -10,10 +10,10 @@ import { ProgressCallback } from '../commandRunner';
|
||||
import { createTimestampFile, showAndLogErrorMessage, showAndLogInformationMessage, showInformationMessageWithAction } from '../helpers';
|
||||
import { Logger } from '../logging';
|
||||
import { runRemoteQuery } from './run-remote-query';
|
||||
import { RemoteQueriesInterfaceManager } from './remote-queries-interface';
|
||||
import { RemoteQueriesView } from './remote-queries-view';
|
||||
import { RemoteQuery } from './remote-query';
|
||||
import { RemoteQueriesMonitor } from './remote-queries-monitor';
|
||||
import { getRemoteQueryIndex, getRepositoriesMetadata, RepositoriesMetadata } from './gh-actions-api-client';
|
||||
import { getRemoteQueryIndex, getRepositoriesMetadata, RepositoriesMetadata } from './gh-api/gh-actions-api-client';
|
||||
import { RemoteQueryResultIndex } from './remote-query-result-index';
|
||||
import { RemoteQueryResult, sumAnalysisSummariesResults } from './remote-query-result';
|
||||
import { DownloadLink } from './download-link';
|
||||
@@ -56,7 +56,7 @@ export class RemoteQueriesManager extends DisposableObject {
|
||||
|
||||
private readonly remoteQueriesMonitor: RemoteQueriesMonitor;
|
||||
private readonly analysesResultsManager: AnalysesResultsManager;
|
||||
private readonly interfaceManager: RemoteQueriesInterfaceManager;
|
||||
private readonly view: RemoteQueriesView;
|
||||
|
||||
constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
@@ -66,7 +66,7 @@ export class RemoteQueriesManager extends DisposableObject {
|
||||
) {
|
||||
super();
|
||||
this.analysesResultsManager = new AnalysesResultsManager(ctx, cliServer, storagePath, logger);
|
||||
this.interfaceManager = new RemoteQueriesInterfaceManager(ctx, logger, this.analysesResultsManager);
|
||||
this.view = new RemoteQueriesView(ctx, logger, this.analysesResultsManager);
|
||||
this.remoteQueriesMonitor = new RemoteQueriesMonitor(ctx, logger);
|
||||
|
||||
this.remoteQueryAddedEventEmitter = this.push(new EventEmitter<NewQueryEvent>());
|
||||
@@ -76,7 +76,7 @@ export class RemoteQueriesManager extends DisposableObject {
|
||||
this.onRemoteQueryRemoved = this.remoteQueryRemovedEventEmitter.event;
|
||||
this.onRemoteQueryStatusUpdate = this.remoteQueryStatusUpdateEventEmitter.event;
|
||||
|
||||
this.push(this.interfaceManager);
|
||||
this.push(this.view);
|
||||
}
|
||||
|
||||
public async rehydrateRemoteQuery(queryId: string, query: RemoteQuery, status: QueryStatus) {
|
||||
@@ -192,7 +192,7 @@ export class RemoteQueriesManager extends DisposableObject {
|
||||
await this.analysesResultsManager.loadAnalysesResults(
|
||||
analysesToDownload,
|
||||
token,
|
||||
results => this.interfaceManager.setAnalysisResults(results, queryResult.queryId));
|
||||
results => this.view.setAnalysisResults(results, queryResult.queryId));
|
||||
}
|
||||
|
||||
public async copyRemoteQueryRepoListToClipboard(queryId: string) {
|
||||
@@ -248,7 +248,7 @@ export class RemoteQueriesManager extends DisposableObject {
|
||||
}
|
||||
|
||||
public async openResults(query: RemoteQuery, queryResult: RemoteQueryResult) {
|
||||
await this.interfaceManager.showResults(query, queryResult);
|
||||
await this.view.showResults(query, queryResult);
|
||||
}
|
||||
|
||||
private async askToOpenResults(query: RemoteQuery, queryResult: RemoteQueryResult): Promise<void> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Credentials } from '../authentication';
|
||||
import { Logger } from '../logging';
|
||||
import { getWorkflowStatus, isArtifactAvailable, RESULT_INDEX_ARTIFACT_NAME } from './gh-actions-api-client';
|
||||
import { getWorkflowStatus, isArtifactAvailable, RESULT_INDEX_ARTIFACT_NAME } from './gh-api/gh-actions-api-client';
|
||||
import { RemoteQuery } from './remote-query';
|
||||
import { RemoteQueryWorkflowResult } from './remote-query-workflow-result';
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ import { SHOW_QUERY_TEXT_MSG } from '../query-history';
|
||||
import { AnalysesResultsManager } from './analyses-results-manager';
|
||||
import { AnalysisResults } from './shared/analysis-result';
|
||||
import { humanizeUnit } from '../pure/time';
|
||||
import { AbstractInterfaceManager, InterfacePanelConfig } from '../abstract-interface-manager';
|
||||
import { AbstractWebview, WebviewPanelConfig } from '../abstract-webview';
|
||||
|
||||
export class RemoteQueriesInterfaceManager extends AbstractInterfaceManager<ToRemoteQueriesMessage, FromRemoteQueriesMessage> {
|
||||
export class RemoteQueriesView extends AbstractWebview<ToRemoteQueriesMessage, FromRemoteQueriesMessage> {
|
||||
private currentQueryId: string | undefined;
|
||||
|
||||
constructor(
|
||||
@@ -100,7 +100,7 @@ export class RemoteQueriesInterfaceManager extends AbstractInterfaceManager<ToRe
|
||||
};
|
||||
}
|
||||
|
||||
protected getPanelConfig(): InterfacePanelConfig {
|
||||
protected getPanelConfig(): WebviewPanelConfig {
|
||||
return {
|
||||
viewId: 'remoteQueriesView',
|
||||
title: 'CodeQL Query Results',
|
||||
@@ -121,7 +121,7 @@ export class RemoteQueriesInterfaceManager extends AbstractInterfaceManager<ToRe
|
||||
|
||||
protected async onMessage(msg: FromRemoteQueriesMessage): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case 'remoteQueryLoaded':
|
||||
case 'viewLoaded':
|
||||
this.onWebViewLoaded();
|
||||
break;
|
||||
case 'remoteQueryError':
|
||||
@@ -145,7 +145,7 @@ export class RemoteQueriesInterfaceManager extends AbstractInterfaceManager<ToRe
|
||||
await this.downloadAllAnalysesResults(msg);
|
||||
break;
|
||||
case 'remoteQueryExportResults':
|
||||
await commands.executeCommand('codeQL.exportVariantAnalysisResults');
|
||||
await commands.executeCommand('codeQL.exportVariantAnalysisResults', msg.queryId);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface Repository {
|
||||
id: number,
|
||||
fullName: string,
|
||||
private: boolean,
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Repository } from './repository';
|
||||
|
||||
export interface VariantAnalysis {
|
||||
id: number,
|
||||
controllerRepoId: number,
|
||||
query: {
|
||||
name: string,
|
||||
filePath: string,
|
||||
language: VariantAnalysisQueryLanguage
|
||||
},
|
||||
databases: {
|
||||
repositories?: string[],
|
||||
repositoryLists?: string[],
|
||||
repositoryOwners?: string[],
|
||||
},
|
||||
status: VariantAnalysisStatus,
|
||||
actionsWorkflowRunId?: number,
|
||||
failureReason?: VariantAnalysisFailureReason,
|
||||
scannedRepos?: VariantAnalysisScannedRepository[],
|
||||
skippedRepos?: VariantAnalysisSkippedRepositories
|
||||
}
|
||||
|
||||
export enum VariantAnalysisQueryLanguage {
|
||||
CSharp = 'csharp',
|
||||
Cpp = 'cpp',
|
||||
Go = 'go',
|
||||
Java = 'java',
|
||||
Javascript = 'javascript',
|
||||
Python = 'python',
|
||||
Ruby = 'ruby'
|
||||
}
|
||||
|
||||
export enum VariantAnalysisStatus {
|
||||
InProgress = 'inProgress',
|
||||
Succeeded = 'succeeded',
|
||||
Failed = 'failed',
|
||||
Canceled = 'canceled',
|
||||
}
|
||||
|
||||
export enum VariantAnalysisFailureReason {
|
||||
NoReposQueried = 'noReposQueried',
|
||||
InternalError = 'internalError',
|
||||
}
|
||||
|
||||
export enum VariantAnalysisRepoStatus {
|
||||
Pending = 'pending',
|
||||
InProgress = 'inProgress',
|
||||
Succeeded = 'succeeded',
|
||||
Failed = 'failed',
|
||||
Canceled = 'canceled',
|
||||
TimedOut = 'timedOut',
|
||||
}
|
||||
|
||||
export interface VariantAnalysisScannedRepository {
|
||||
repository: Repository,
|
||||
analysisStatus: VariantAnalysisRepoStatus,
|
||||
resultCount?: number,
|
||||
artifactSizeInBytes?: number,
|
||||
failureMessage?: string
|
||||
}
|
||||
|
||||
export interface VariantAnalysisSkippedRepositories {
|
||||
accessMismatchRepos?: VariantAnalysisSkippedRepositoryGroup,
|
||||
notFoundRepos?: VariantAnalysisSkippedRepositoryGroup,
|
||||
noCodeqlDbRepos?: VariantAnalysisSkippedRepositoryGroup,
|
||||
overLimitRepos?: VariantAnalysisSkippedRepositoryGroup
|
||||
}
|
||||
|
||||
export interface VariantAnalysisSkippedRepositoryGroup {
|
||||
repositoryCount: number,
|
||||
repositories: Array<{
|
||||
id?: number,
|
||||
fullName: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures information needed to submit a variant
|
||||
* analysis for processing.
|
||||
*/
|
||||
export interface VariantAnalysisSubmission {
|
||||
startTime: number,
|
||||
controllerRepoId: number,
|
||||
actionRepoRef: string,
|
||||
query: {
|
||||
name: string,
|
||||
filePath: string,
|
||||
language: VariantAnalysisQueryLanguage,
|
||||
|
||||
// Base64 encoded query pack.
|
||||
pack: string,
|
||||
},
|
||||
databases: {
|
||||
repositories?: string[],
|
||||
repositoryLists?: string[],
|
||||
repositoryOwners?: string[],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param repo
|
||||
* @returns whether the repo scan is in a completed state, i.e. it cannot normally change state anymore
|
||||
*/
|
||||
export function hasRepoScanCompleted(repo: VariantAnalysisScannedRepository): boolean {
|
||||
return [
|
||||
// All states that indicates the repository has been scanned and cannot
|
||||
// change status anymore.
|
||||
VariantAnalysisRepoStatus.Succeeded, VariantAnalysisRepoStatus.Failed,
|
||||
VariantAnalysisRepoStatus.Canceled, VariantAnalysisRepoStatus.TimedOut,
|
||||
].includes(repo.analysisStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param repos
|
||||
* @returns the total number of results. Will be `undefined` when there are no repos with results.
|
||||
*/
|
||||
export function getTotalResultCount(repos: VariantAnalysisScannedRepository[] | undefined): number | undefined {
|
||||
const reposWithResultCounts = repos?.filter(repo => repo.resultCount !== undefined);
|
||||
if (reposWithResultCounts === undefined || reposWithResultCounts.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return reposWithResultCounts.reduce((acc, repo) => acc + (repo.resultCount ?? 0), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param skippedRepos
|
||||
* @returns the total number of skipped repositories.
|
||||
*/
|
||||
export function getSkippedRepoCount(skippedRepos: VariantAnalysisSkippedRepositories | undefined): number {
|
||||
if (!skippedRepos) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Object.values(skippedRepos).reduce((acc, group) => acc + group.repositoryCount, 0);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ViewColumn } from 'vscode';
|
||||
import { AbstractWebview, WebviewPanelConfig } from '../abstract-webview';
|
||||
import { WebviewMessage } from '../interface-utils';
|
||||
import { logger } from '../logging';
|
||||
|
||||
export class VariantAnalysisView extends AbstractWebview<WebviewMessage, WebviewMessage> {
|
||||
public openView() {
|
||||
this.getPanel().reveal(undefined, true);
|
||||
}
|
||||
|
||||
protected getPanelConfig(): WebviewPanelConfig {
|
||||
return {
|
||||
viewId: 'variantAnalysisView',
|
||||
title: 'CodeQL Query Results',
|
||||
viewColumn: ViewColumn.Active,
|
||||
preserveFocus: true,
|
||||
view: 'variant-analysis'
|
||||
};
|
||||
}
|
||||
|
||||
protected onPanelDispose(): void {
|
||||
// Nothing to dispose currently.
|
||||
}
|
||||
|
||||
protected async onMessage(msg: WebviewMessage): Promise<void> {
|
||||
void logger.log('Received message on variant analysis view: ' + msg.t);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ module.exports = {
|
||||
},
|
||||
extends: [
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:storybook/recommended",
|
||||
],
|
||||
settings: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { ThemeProvider } from '@primer/react';
|
||||
|
||||
import CodePaths from '../../view/remote-queries/CodePaths';
|
||||
import { CodePaths } from '../../view/common';
|
||||
import type { CodeFlow } from '../../remote-queries/shared/analysis-result';
|
||||
|
||||
export default {
|
||||
@@ -112,8 +112,8 @@ PowerShell.args = {
|
||||
message: {
|
||||
tokens: [
|
||||
{
|
||||
type: 'text',
|
||||
t: 'This zip file may have a dangerous path'
|
||||
t: 'text',
|
||||
text: 'This zip file may have a dangerous path'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import FileCodeSnippet from '../../view/remote-queries/FileCodeSnippet';
|
||||
import { FileCodeSnippet } from '../../view/common';
|
||||
|
||||
export default {
|
||||
title: 'File Code Snippet',
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { CodePaths, Codicon as CodiconComponent } from '../../../view/common';
|
||||
|
||||
// To regenerate the icons, use the following command from the `extensions/ql-vscode` directory:
|
||||
// jq -R '[inputs | [splits(", *")] as $row | $row[0]]' < node_modules/@vscode/codicons/dist/codicon.csv > src/stories/common/icon/vscode-icons.json
|
||||
import icons from './vscode-icons.json';
|
||||
|
||||
export default {
|
||||
title: 'Icon/Codicon',
|
||||
component: CodiconComponent,
|
||||
argTypes: {
|
||||
name: {
|
||||
control: 'select',
|
||||
options: icons,
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof CodePaths>;
|
||||
|
||||
const Template: ComponentStory<typeof CodiconComponent> = (args) => (
|
||||
<CodiconComponent {...args} />
|
||||
);
|
||||
|
||||
export const Codicon = Template.bind({});
|
||||
Codicon.args = {
|
||||
name: 'account',
|
||||
label: 'Account'
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { CodePaths, ErrorIcon as ErrorIconComponent } from '../../../view/common';
|
||||
|
||||
export default {
|
||||
title: 'Icon/Error Icon',
|
||||
component: ErrorIconComponent,
|
||||
} as ComponentMeta<typeof CodePaths>;
|
||||
|
||||
const Template: ComponentStory<typeof ErrorIconComponent> = (args) => (
|
||||
<ErrorIconComponent {...args} />
|
||||
);
|
||||
|
||||
export const ErrorIcon = Template.bind({});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { CodePaths, SuccessIcon as SuccessIconComponent } from '../../../view/common';
|
||||
|
||||
export default {
|
||||
title: 'Icon/Success Icon',
|
||||
component: SuccessIconComponent,
|
||||
} as ComponentMeta<typeof CodePaths>;
|
||||
|
||||
const Template: ComponentStory<typeof SuccessIconComponent> = (args) => (
|
||||
<SuccessIconComponent {...args} />
|
||||
);
|
||||
|
||||
export const SuccessIcon = Template.bind({});
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { CodePaths, WarningIcon as WarningIconComponent } from '../../../view/common';
|
||||
|
||||
export default {
|
||||
title: 'Icon/Warning Icon',
|
||||
component: WarningIconComponent,
|
||||
} as ComponentMeta<typeof CodePaths>;
|
||||
|
||||
const Template: ComponentStory<typeof WarningIconComponent> = (args) => (
|
||||
<WarningIconComponent {...args} />
|
||||
);
|
||||
|
||||
export const WarningIcon = Template.bind({});
|
||||
413
extensions/ql-vscode/src/stories/common/icon/vscode-icons.json
Normal file
413
extensions/ql-vscode/src/stories/common/icon/vscode-icons.json
Normal file
@@ -0,0 +1,413 @@
|
||||
[
|
||||
"account",
|
||||
"activate-breakpoints",
|
||||
"add",
|
||||
"archive",
|
||||
"arrow-both",
|
||||
"arrow-circle-down",
|
||||
"arrow-circle-left",
|
||||
"arrow-circle-right",
|
||||
"arrow-circle-up",
|
||||
"arrow-down",
|
||||
"arrow-left",
|
||||
"arrow-right",
|
||||
"arrow-small-down",
|
||||
"arrow-small-left",
|
||||
"arrow-small-right",
|
||||
"arrow-small-up",
|
||||
"arrow-swap",
|
||||
"arrow-up",
|
||||
"azure-devops",
|
||||
"azure",
|
||||
"beaker-stop",
|
||||
"beaker",
|
||||
"bell-dot",
|
||||
"bell",
|
||||
"blank",
|
||||
"bold",
|
||||
"book",
|
||||
"bookmark",
|
||||
"bracket-dot",
|
||||
"bracket-error",
|
||||
"briefcase",
|
||||
"broadcast",
|
||||
"browser",
|
||||
"bug",
|
||||
"calendar",
|
||||
"call-incoming",
|
||||
"call-outgoing",
|
||||
"case-sensitive",
|
||||
"check-all",
|
||||
"check",
|
||||
"checklist",
|
||||
"chevron-down",
|
||||
"chevron-left",
|
||||
"chevron-right",
|
||||
"chevron-up",
|
||||
"chrome-close",
|
||||
"chrome-maximize",
|
||||
"chrome-minimize",
|
||||
"chrome-restore",
|
||||
"circle-filled",
|
||||
"circle-large-filled",
|
||||
"circle-large-outline",
|
||||
"circle-outline",
|
||||
"circle-slash",
|
||||
"circuit-board",
|
||||
"clear-all",
|
||||
"clippy",
|
||||
"close-all",
|
||||
"close",
|
||||
"cloud-download",
|
||||
"cloud-upload",
|
||||
"cloud",
|
||||
"code",
|
||||
"collapse-all",
|
||||
"color-mode",
|
||||
"combine",
|
||||
"comment-discussion",
|
||||
"comment",
|
||||
"compass-active",
|
||||
"compass-dot",
|
||||
"compass",
|
||||
"copy",
|
||||
"credit-card",
|
||||
"dash",
|
||||
"dashboard",
|
||||
"database",
|
||||
"debug-all",
|
||||
"debug-alt-small",
|
||||
"debug-alt",
|
||||
"debug-breakpoint-conditional-unverified",
|
||||
"debug-breakpoint-conditional",
|
||||
"debug-breakpoint-data-unverified",
|
||||
"debug-breakpoint-data",
|
||||
"debug-breakpoint-function-unverified",
|
||||
"debug-breakpoint-function",
|
||||
"debug-breakpoint-log-unverified",
|
||||
"debug-breakpoint-log",
|
||||
"debug-breakpoint-unsupported",
|
||||
"debug-console",
|
||||
"debug-continue-small",
|
||||
"debug-continue",
|
||||
"debug-coverage",
|
||||
"debug-disconnect",
|
||||
"debug-line-by-line",
|
||||
"debug-pause",
|
||||
"debug-rerun",
|
||||
"debug-restart-frame",
|
||||
"debug-restart",
|
||||
"debug-reverse-continue",
|
||||
"debug-stackframe-active",
|
||||
"debug-stackframe-dot",
|
||||
"debug-stackframe",
|
||||
"debug-start",
|
||||
"debug-step-back",
|
||||
"debug-step-into",
|
||||
"debug-step-out",
|
||||
"debug-step-over",
|
||||
"debug-stop",
|
||||
"debug",
|
||||
"desktop-download",
|
||||
"device-camera-video",
|
||||
"device-camera",
|
||||
"device-mobile",
|
||||
"diff-added",
|
||||
"diff-ignored",
|
||||
"diff-modified",
|
||||
"diff-removed",
|
||||
"diff-renamed",
|
||||
"diff",
|
||||
"discard",
|
||||
"edit",
|
||||
"editor-layout",
|
||||
"ellipsis",
|
||||
"empty-window",
|
||||
"error-small",
|
||||
"error",
|
||||
"exclude",
|
||||
"expand-all",
|
||||
"export",
|
||||
"extensions",
|
||||
"eye-closed",
|
||||
"eye",
|
||||
"feedback",
|
||||
"file-binary",
|
||||
"file-code",
|
||||
"file-media",
|
||||
"file-pdf",
|
||||
"file-submodule",
|
||||
"file-symlink-directory",
|
||||
"file-symlink-file",
|
||||
"file-zip",
|
||||
"file",
|
||||
"files",
|
||||
"filter-filled",
|
||||
"filter",
|
||||
"flame",
|
||||
"fold-down",
|
||||
"fold-up",
|
||||
"fold",
|
||||
"folder-active",
|
||||
"folder-library",
|
||||
"folder-opened",
|
||||
"folder",
|
||||
"gear",
|
||||
"gift",
|
||||
"gist-secret",
|
||||
"git-commit",
|
||||
"git-compare",
|
||||
"git-merge",
|
||||
"git-pull-request-closed",
|
||||
"git-pull-request-create",
|
||||
"git-pull-request-draft",
|
||||
"git-pull-request",
|
||||
"github-action",
|
||||
"github-alt",
|
||||
"github-inverted",
|
||||
"github",
|
||||
"globe",
|
||||
"go-to-file",
|
||||
"grabber",
|
||||
"graph-left",
|
||||
"graph-line",
|
||||
"graph-scatter",
|
||||
"graph",
|
||||
"gripper",
|
||||
"group-by-ref-type",
|
||||
"heart",
|
||||
"history",
|
||||
"home",
|
||||
"horizontal-rule",
|
||||
"hubot",
|
||||
"inbox",
|
||||
"indent",
|
||||
"info",
|
||||
"inspect",
|
||||
"issue-draft",
|
||||
"issue-reopened",
|
||||
"issues",
|
||||
"italic",
|
||||
"jersey",
|
||||
"json",
|
||||
"kebab-vertical",
|
||||
"key",
|
||||
"law",
|
||||
"layers-active",
|
||||
"layers-dot",
|
||||
"layers",
|
||||
"layout-activitybar-left",
|
||||
"layout-activitybar-right",
|
||||
"layout-centered",
|
||||
"layout-menubar",
|
||||
"layout-panel-center",
|
||||
"layout-panel-justify",
|
||||
"layout-panel-left",
|
||||
"layout-panel-off",
|
||||
"layout-panel-right",
|
||||
"layout-panel",
|
||||
"layout-sidebar-left-off",
|
||||
"layout-sidebar-left",
|
||||
"layout-sidebar-right-off",
|
||||
"layout-sidebar-right",
|
||||
"layout-statusbar",
|
||||
"layout",
|
||||
"library",
|
||||
"lightbulb-autofix",
|
||||
"lightbulb",
|
||||
"link-external",
|
||||
"link",
|
||||
"list-filter",
|
||||
"list-flat",
|
||||
"list-ordered",
|
||||
"list-selection",
|
||||
"list-tree",
|
||||
"list-unordered",
|
||||
"live-share",
|
||||
"loading",
|
||||
"location",
|
||||
"lock-small",
|
||||
"lock",
|
||||
"magnet",
|
||||
"mail-read",
|
||||
"mail",
|
||||
"markdown",
|
||||
"megaphone",
|
||||
"mention",
|
||||
"menu",
|
||||
"merge",
|
||||
"milestone",
|
||||
"mirror",
|
||||
"mortar-board",
|
||||
"move",
|
||||
"multiple-windows",
|
||||
"mute",
|
||||
"new-file",
|
||||
"new-folder",
|
||||
"newline",
|
||||
"no-newline",
|
||||
"note",
|
||||
"notebook-template",
|
||||
"notebook",
|
||||
"octoface",
|
||||
"open-preview",
|
||||
"organization",
|
||||
"output",
|
||||
"package",
|
||||
"paintcan",
|
||||
"pass-filled",
|
||||
"pass",
|
||||
"person-add",
|
||||
"person",
|
||||
"pie-chart",
|
||||
"pin",
|
||||
"pinned-dirty",
|
||||
"pinned",
|
||||
"play-circle",
|
||||
"play",
|
||||
"plug",
|
||||
"preserve-case",
|
||||
"preview",
|
||||
"primitive-square",
|
||||
"project",
|
||||
"pulse",
|
||||
"question",
|
||||
"quote",
|
||||
"radio-tower",
|
||||
"reactions",
|
||||
"record-keys",
|
||||
"record-small",
|
||||
"record",
|
||||
"redo",
|
||||
"references",
|
||||
"refresh",
|
||||
"regex",
|
||||
"remote-explorer",
|
||||
"remote",
|
||||
"remove",
|
||||
"replace-all",
|
||||
"replace",
|
||||
"reply",
|
||||
"repo-clone",
|
||||
"repo-force-push",
|
||||
"repo-forked",
|
||||
"repo-pull",
|
||||
"repo-push",
|
||||
"repo",
|
||||
"report",
|
||||
"request-changes",
|
||||
"rocket",
|
||||
"root-folder-opened",
|
||||
"root-folder",
|
||||
"rss",
|
||||
"ruby",
|
||||
"run-above",
|
||||
"run-all",
|
||||
"run-below",
|
||||
"run-errors",
|
||||
"save-all",
|
||||
"save-as",
|
||||
"save",
|
||||
"screen-full",
|
||||
"screen-normal",
|
||||
"search-stop",
|
||||
"search",
|
||||
"server-environment",
|
||||
"server-process",
|
||||
"server",
|
||||
"settings-gear",
|
||||
"settings",
|
||||
"shield",
|
||||
"sign-in",
|
||||
"sign-out",
|
||||
"smiley",
|
||||
"sort-precedence",
|
||||
"source-control",
|
||||
"split-horizontal",
|
||||
"split-vertical",
|
||||
"squirrel",
|
||||
"star-empty",
|
||||
"star-full",
|
||||
"star-half",
|
||||
"stop-circle",
|
||||
"symbol-array",
|
||||
"symbol-boolean",
|
||||
"symbol-class",
|
||||
"symbol-color",
|
||||
"symbol-constant",
|
||||
"symbol-enum-member",
|
||||
"symbol-enum",
|
||||
"symbol-event",
|
||||
"symbol-field",
|
||||
"symbol-file",
|
||||
"symbol-interface",
|
||||
"symbol-key",
|
||||
"symbol-keyword",
|
||||
"symbol-method",
|
||||
"symbol-misc",
|
||||
"symbol-namespace",
|
||||
"symbol-numeric",
|
||||
"symbol-operator",
|
||||
"symbol-parameter",
|
||||
"symbol-property",
|
||||
"symbol-ruler",
|
||||
"symbol-snippet",
|
||||
"symbol-string",
|
||||
"symbol-structure",
|
||||
"symbol-variable",
|
||||
"sync-ignored",
|
||||
"sync",
|
||||
"table",
|
||||
"tag",
|
||||
"target",
|
||||
"tasklist",
|
||||
"telescope",
|
||||
"terminal-bash",
|
||||
"terminal-cmd",
|
||||
"terminal-debian",
|
||||
"terminal-linux",
|
||||
"terminal-powershell",
|
||||
"terminal-tmux",
|
||||
"terminal-ubuntu",
|
||||
"terminal",
|
||||
"text-size",
|
||||
"three-bars",
|
||||
"thumbsdown",
|
||||
"thumbsup",
|
||||
"tools",
|
||||
"trash",
|
||||
"triangle-down",
|
||||
"triangle-left",
|
||||
"triangle-right",
|
||||
"triangle-up",
|
||||
"twitter",
|
||||
"type-hierarchy-sub",
|
||||
"type-hierarchy-super",
|
||||
"type-hierarchy",
|
||||
"unfold",
|
||||
"ungroup-by-ref-type",
|
||||
"unlock",
|
||||
"unmute",
|
||||
"unverified",
|
||||
"variable-group",
|
||||
"verified-filled",
|
||||
"verified",
|
||||
"versions",
|
||||
"vm-active",
|
||||
"vm-connect",
|
||||
"vm-outline",
|
||||
"vm-running",
|
||||
"vm",
|
||||
"wand",
|
||||
"warning",
|
||||
"watch",
|
||||
"whitespace",
|
||||
"whole-word",
|
||||
"window",
|
||||
"word-wrap",
|
||||
"workspace-trusted",
|
||||
"workspace-unknown",
|
||||
"workspace-untrusted",
|
||||
"zoom-in",
|
||||
"zoom-out"
|
||||
]
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
|
||||
import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer';
|
||||
import { QueryDetails as QueryDetailsComponent } from '../../view/variant-analysis/QueryDetails';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Query Details',
|
||||
component: QueryDetailsComponent,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<VariantAnalysisContainer>
|
||||
<Story />
|
||||
</VariantAnalysisContainer>
|
||||
)
|
||||
],
|
||||
argTypes: {
|
||||
onOpenQueryFileClick: {
|
||||
action: 'open-query-file-clicked',
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
onViewQueryTextClick: {
|
||||
action: 'view-query-text-clicked',
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
} as ComponentMeta<typeof QueryDetailsComponent>;
|
||||
|
||||
const Template: ComponentStory<typeof QueryDetailsComponent> = (args) => (
|
||||
<QueryDetailsComponent {...args} />
|
||||
);
|
||||
|
||||
export const QueryDetails = Template.bind({});
|
||||
QueryDetails.args = {
|
||||
queryName: 'Query name',
|
||||
queryFileName: 'example.ql',
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { VariantAnalysis as VariantAnalysisComponent } from '../../view/variant-analysis/VariantAnalysis';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Variant Analysis',
|
||||
component: VariantAnalysisComponent,
|
||||
} as ComponentMeta<typeof VariantAnalysisComponent>;
|
||||
|
||||
const Template: ComponentStory<typeof VariantAnalysisComponent> = () => (
|
||||
<VariantAnalysisComponent />
|
||||
);
|
||||
|
||||
export const VariantAnalysis = Template.bind({});
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
|
||||
import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer';
|
||||
import { VariantAnalysisStatus } from '../../remote-queries/shared/variant-analysis';
|
||||
import { VariantAnalysisActions } from '../../view/variant-analysis/VariantAnalysisActions';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Variant Analysis Actions',
|
||||
component: VariantAnalysisActions,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<VariantAnalysisContainer>
|
||||
<Story />
|
||||
</VariantAnalysisContainer>
|
||||
)
|
||||
],
|
||||
argTypes: {
|
||||
onStopQueryClick: {
|
||||
action: 'stop-query-clicked',
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
onCopyRepositoryListClick: {
|
||||
action: 'copy-repository-list-clicked',
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
onExportResultsClick: {
|
||||
action: 'export-results-clicked',
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
} as ComponentMeta<typeof VariantAnalysisActions>;
|
||||
|
||||
const Template: ComponentStory<typeof VariantAnalysisActions> = (args) => (
|
||||
<VariantAnalysisActions {...args} />
|
||||
);
|
||||
|
||||
export const InProgress = Template.bind({});
|
||||
InProgress.args = {
|
||||
variantAnalysisStatus: VariantAnalysisStatus.InProgress,
|
||||
};
|
||||
|
||||
export const Succeeded = Template.bind({});
|
||||
Succeeded.args = {
|
||||
...InProgress.args,
|
||||
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
|
||||
};
|
||||
|
||||
export const Failed = Template.bind({});
|
||||
Failed.args = {
|
||||
...InProgress.args,
|
||||
variantAnalysisStatus: VariantAnalysisStatus.Failed,
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
|
||||
import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer';
|
||||
import { VariantAnalysisHeader } from '../../view/variant-analysis/VariantAnalysisHeader';
|
||||
import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisQueryLanguage,
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisScannedRepository,
|
||||
VariantAnalysisStatus
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Variant Analysis Header',
|
||||
component: VariantAnalysisHeader,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<VariantAnalysisContainer>
|
||||
<Story />
|
||||
</VariantAnalysisContainer>
|
||||
)
|
||||
],
|
||||
argTypes: {
|
||||
onOpenQueryFileClick: {
|
||||
action: 'open-query-file-clicked',
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
onViewQueryTextClick: {
|
||||
action: 'view-query-text-clicked',
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
onStopQueryClick: {
|
||||
action: 'stop-query-clicked',
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
onCopyRepositoryListClick: {
|
||||
action: 'copy-repository-list-clicked',
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
onExportResultsClick: {
|
||||
action: 'export-results-clicked',
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
onViewLogsClick: {
|
||||
action: 'view-logs-clicked',
|
||||
table: {
|
||||
disable: true,
|
||||
}
|
||||
},
|
||||
}
|
||||
} as ComponentMeta<typeof VariantAnalysisHeader>;
|
||||
|
||||
const Template: ComponentStory<typeof VariantAnalysisHeader> = (args) => (
|
||||
<VariantAnalysisHeader {...args} />
|
||||
);
|
||||
|
||||
const buildVariantAnalysis = (data: Partial<VariantAnalysis>) => ({
|
||||
id: 1,
|
||||
controllerRepoId: 1,
|
||||
query: {
|
||||
name: 'Query name',
|
||||
filePath: 'example.ql',
|
||||
language: VariantAnalysisQueryLanguage.Javascript,
|
||||
},
|
||||
databases: {},
|
||||
status: VariantAnalysisStatus.InProgress,
|
||||
...data,
|
||||
});
|
||||
|
||||
const buildScannedRepo = (id: number, data?: Partial<VariantAnalysisScannedRepository>): VariantAnalysisScannedRepository => ({
|
||||
repository: {
|
||||
id: id,
|
||||
fullName: `octodemo/hello-world-${id}`,
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Pending,
|
||||
...data,
|
||||
});
|
||||
|
||||
export const InProgress = Template.bind({});
|
||||
InProgress.args = {
|
||||
variantAnalysis: buildVariantAnalysis({
|
||||
scannedRepos: [
|
||||
buildScannedRepo(1, {
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
resultCount: 99_999,
|
||||
}),
|
||||
buildScannedRepo(2, {
|
||||
analysisStatus: VariantAnalysisRepoStatus.Failed,
|
||||
}),
|
||||
buildScannedRepo(3, {
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
resultCount: 0,
|
||||
}),
|
||||
buildScannedRepo(4),
|
||||
buildScannedRepo(5),
|
||||
buildScannedRepo(6),
|
||||
buildScannedRepo(7),
|
||||
buildScannedRepo(8),
|
||||
buildScannedRepo(9),
|
||||
buildScannedRepo(10),
|
||||
]
|
||||
}),
|
||||
};
|
||||
|
||||
export const Succeeded = Template.bind({});
|
||||
Succeeded.args = {
|
||||
...InProgress.args,
|
||||
variantAnalysis: buildVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Succeeded,
|
||||
scannedRepos: Array.from({ length: 1000 }, (_, i) => buildScannedRepo(i + 1, {
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
resultCount: 100,
|
||||
}))
|
||||
}),
|
||||
duration: 720_000,
|
||||
completedAt: new Date(1661263446000),
|
||||
};
|
||||
|
||||
export const Failed = Template.bind({});
|
||||
Failed.args = {
|
||||
...InProgress.args,
|
||||
variantAnalysis: buildVariantAnalysis({
|
||||
status: VariantAnalysisStatus.Failed,
|
||||
}),
|
||||
duration: 10_000,
|
||||
completedAt: new Date(1661263446000),
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer';
|
||||
import { VariantAnalysisStats } from '../../view/variant-analysis/VariantAnalysisStats';
|
||||
import { VariantAnalysisStatus } from '../../remote-queries/shared/variant-analysis';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Variant Analysis Stats',
|
||||
component: VariantAnalysisStats,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<VariantAnalysisContainer>
|
||||
<Story />
|
||||
</VariantAnalysisContainer>
|
||||
)
|
||||
],
|
||||
argTypes: {
|
||||
onViewLogsClick: {
|
||||
action: 'view-logs-clicked',
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
} as ComponentMeta<typeof VariantAnalysisStats>;
|
||||
|
||||
const Template: ComponentStory<typeof VariantAnalysisStats> = (args) => (
|
||||
<VariantAnalysisStats {...args} />
|
||||
);
|
||||
|
||||
export const Starting = Template.bind({});
|
||||
Starting.args = {
|
||||
variantAnalysisStatus: VariantAnalysisStatus.InProgress,
|
||||
totalRepositoryCount: 10,
|
||||
};
|
||||
|
||||
export const Started = Template.bind({});
|
||||
Started.args = {
|
||||
...Starting.args,
|
||||
resultCount: 99_999,
|
||||
completedRepositoryCount: 2,
|
||||
};
|
||||
|
||||
export const StartedWithWarnings = Template.bind({});
|
||||
StartedWithWarnings.args = {
|
||||
...Starting.args,
|
||||
hasWarnings: true,
|
||||
};
|
||||
|
||||
export const Succeeded = Template.bind({});
|
||||
Succeeded.args = {
|
||||
...Started.args,
|
||||
totalRepositoryCount: 1000,
|
||||
completedRepositoryCount: 1000,
|
||||
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
|
||||
duration: 720_000,
|
||||
completedAt: new Date(1661263446000),
|
||||
};
|
||||
|
||||
export const SucceededWithWarnings = Template.bind({});
|
||||
SucceededWithWarnings.args = {
|
||||
...Succeeded.args,
|
||||
totalRepositoryCount: 10,
|
||||
completedRepositoryCount: 2,
|
||||
hasWarnings: true,
|
||||
};
|
||||
|
||||
export const Failed = Template.bind({});
|
||||
Failed.args = {
|
||||
...Starting.args,
|
||||
variantAnalysisStatus: VariantAnalysisStatus.Failed,
|
||||
duration: 10_000,
|
||||
completedAt: new Date(1661263446000),
|
||||
};
|
||||
|
||||
export const Stopped = Template.bind({});
|
||||
Stopped.args = {
|
||||
...SucceededWithWarnings.args,
|
||||
variantAnalysisStatus: VariantAnalysisStatus.Canceled,
|
||||
};
|
||||
@@ -3,7 +3,8 @@ module.exports = {
|
||||
browser: true
|
||||
},
|
||||
extends: [
|
||||
"plugin:react/recommended"
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import * as React from 'react';
|
||||
import { ChangeEvent, SetStateAction, useCallback } from 'react';
|
||||
import { VSCodeDropdown, VSCodeOption } from '@vscode/webview-ui-toolkit/react';
|
||||
|
||||
import { CodeFlow } from '../../../remote-queries/shared/analysis-result';
|
||||
|
||||
const getCodeFlowName = (codeFlow: CodeFlow) => {
|
||||
const filePath = codeFlow.threadFlows[codeFlow.threadFlows.length - 1].fileLink.filePath;
|
||||
return filePath.substring(filePath.lastIndexOf('/') + 1);
|
||||
};
|
||||
|
||||
type CodeFlowsDropdownProps = {
|
||||
codeFlows: CodeFlow[];
|
||||
setSelectedCodeFlow: (value: SetStateAction<CodeFlow>) => void;
|
||||
}
|
||||
|
||||
export const CodeFlowsDropdown = ({
|
||||
codeFlows,
|
||||
setSelectedCodeFlow
|
||||
}: CodeFlowsDropdownProps) => {
|
||||
const handleChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedOption = e.target;
|
||||
const selectedIndex = selectedOption.value as unknown as number;
|
||||
setSelectedCodeFlow(codeFlows[selectedIndex]);
|
||||
}, [setSelectedCodeFlow, codeFlows]);
|
||||
|
||||
return (
|
||||
<VSCodeDropdown onChange={handleChange}>
|
||||
{codeFlows.map((codeFlow, index) =>
|
||||
<VSCodeOption
|
||||
key={index}
|
||||
value={index}
|
||||
>
|
||||
{getCodeFlowName(codeFlow)}
|
||||
</VSCodeOption>
|
||||
)}
|
||||
</VSCodeDropdown>
|
||||
);
|
||||
};
|
||||
30
extensions/ql-vscode/src/view/common/CodePaths/CodePath.tsx
Normal file
30
extensions/ql-vscode/src/view/common/CodePaths/CodePath.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { AnalysisMessage, CodeFlow, ResultSeverity } from '../../../remote-queries/shared/analysis-result';
|
||||
import { ThreadPath } from './ThreadPath';
|
||||
|
||||
type CodePathProps = {
|
||||
codeFlow: CodeFlow;
|
||||
message: AnalysisMessage;
|
||||
severity: ResultSeverity;
|
||||
}
|
||||
|
||||
export const CodePath = ({
|
||||
codeFlow,
|
||||
message,
|
||||
severity
|
||||
}: CodePathProps) => (
|
||||
<>
|
||||
{codeFlow.threadFlows.map((threadFlow, index) =>
|
||||
<ThreadPath
|
||||
key={index}
|
||||
threadFlow={threadFlow}
|
||||
step={index + 1}
|
||||
message={message}
|
||||
severity={severity}
|
||||
isSource={index === 0}
|
||||
isSink={index === codeFlow.threadFlows.length - 1}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
60
extensions/ql-vscode/src/view/common/CodePaths/CodePaths.tsx
Normal file
60
extensions/ql-vscode/src/view/common/CodePaths/CodePaths.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as React from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeLink } from '@vscode/webview-ui-toolkit/react';
|
||||
|
||||
import { Overlay } from '@primer/react';
|
||||
|
||||
import { AnalysisMessage, CodeFlow, ResultSeverity } from '../../../remote-queries/shared/analysis-result';
|
||||
import { CodePathsOverlay } from './CodePathsOverlay';
|
||||
|
||||
const ShowPathsLink = styled(VSCodeLink)`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
codeFlows: CodeFlow[],
|
||||
ruleDescription: string,
|
||||
message: AnalysisMessage,
|
||||
severity: ResultSeverity
|
||||
};
|
||||
|
||||
export const CodePaths = ({
|
||||
codeFlows,
|
||||
ruleDescription,
|
||||
message,
|
||||
severity
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const linkRef = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
const closeOverlay = () => setIsOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShowPathsLink
|
||||
onClick={() => setIsOpen(true)}
|
||||
ref={linkRef}
|
||||
>
|
||||
Show paths
|
||||
</ShowPathsLink>
|
||||
{isOpen && (
|
||||
<Overlay
|
||||
returnFocusRef={linkRef}
|
||||
onEscape={closeOverlay}
|
||||
onClickOutside={closeOverlay}
|
||||
anchorSide="outside-top"
|
||||
>
|
||||
<CodePathsOverlay
|
||||
codeFlows={codeFlows}
|
||||
ruleDescription={ruleDescription}
|
||||
message={message}
|
||||
severity={severity}
|
||||
onClose={closeOverlay}
|
||||
/>
|
||||
</Overlay>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AnalysisMessage, CodeFlow, ResultSeverity } from '../../../remote-queries/shared/analysis-result';
|
||||
import { SectionTitle } from '../SectionTitle';
|
||||
import { VerticalSpace } from '../VerticalSpace';
|
||||
import { CodeFlowsDropdown } from './CodeFlowsDropdown';
|
||||
import { CodePath } from './CodePath';
|
||||
|
||||
const StyledCloseButton = styled.button`
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 4em;
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none
|
||||
}
|
||||
`;
|
||||
|
||||
const OverlayContainer = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 2em;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
overflow-y: scroll;
|
||||
`;
|
||||
|
||||
const CloseButton = ({ onClick }: { onClick: () => void }) => (
|
||||
<StyledCloseButton onClick={onClick} tabIndex={-1}>
|
||||
<span className="codicon codicon-chrome-close" />
|
||||
</StyledCloseButton>
|
||||
);
|
||||
|
||||
const PathsContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const PathDetailsContainer = styled.div`
|
||||
padding: 0;
|
||||
border: 0;
|
||||
`;
|
||||
|
||||
const PathDropdownContainer = styled.div`
|
||||
flex-grow: 1;
|
||||
padding: 0 0 0 0.2em;
|
||||
border: none;
|
||||
`;
|
||||
|
||||
type CodePathsOverlayProps = {
|
||||
codeFlows: CodeFlow[];
|
||||
ruleDescription: string;
|
||||
message: AnalysisMessage;
|
||||
severity: ResultSeverity;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const CodePathsOverlay = ({
|
||||
codeFlows,
|
||||
ruleDescription,
|
||||
message,
|
||||
severity,
|
||||
onClose,
|
||||
}: CodePathsOverlayProps) => {
|
||||
const [selectedCodeFlow, setSelectedCodeFlow] = useState(codeFlows[0]);
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<CloseButton onClick={onClose} />
|
||||
|
||||
<SectionTitle>{ruleDescription}</SectionTitle>
|
||||
<VerticalSpace size={2} />
|
||||
|
||||
<PathsContainer>
|
||||
<PathDetailsContainer>
|
||||
{codeFlows.length} paths available: {selectedCodeFlow.threadFlows.length} steps in
|
||||
</PathDetailsContainer>
|
||||
<PathDropdownContainer>
|
||||
<CodeFlowsDropdown codeFlows={codeFlows} setSelectedCodeFlow={setSelectedCodeFlow} />
|
||||
</PathDropdownContainer>
|
||||
</PathsContainer>
|
||||
|
||||
<VerticalSpace size={2} />
|
||||
<CodePath
|
||||
codeFlow={selectedCodeFlow}
|
||||
severity={severity}
|
||||
message={message}
|
||||
/>
|
||||
<VerticalSpace size={3} />
|
||||
</OverlayContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeTag } from '@vscode/webview-ui-toolkit/react';
|
||||
|
||||
import { AnalysisMessage, ResultSeverity, ThreadFlow } from '../../../remote-queries/shared/analysis-result';
|
||||
import { SectionTitle } from '../SectionTitle';
|
||||
import { FileCodeSnippet } from '../FileCodeSnippet';
|
||||
|
||||
const Container = styled.div`
|
||||
max-width: 55em;
|
||||
margin-bottom: 1.5em;
|
||||
`;
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
`;
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
flex-grow: 1;
|
||||
padding: 0;
|
||||
border: none;
|
||||
`;
|
||||
|
||||
const TagContainer = styled.div`
|
||||
padding: 0;
|
||||
border: none;
|
||||
`;
|
||||
|
||||
type ThreadPathProps = {
|
||||
threadFlow: ThreadFlow;
|
||||
step: number;
|
||||
message: AnalysisMessage;
|
||||
severity: ResultSeverity;
|
||||
isSource?: boolean;
|
||||
isSink?: boolean;
|
||||
}
|
||||
|
||||
export const ThreadPath = ({
|
||||
threadFlow,
|
||||
step,
|
||||
message,
|
||||
severity,
|
||||
isSource,
|
||||
isSink,
|
||||
}: ThreadPathProps) => (
|
||||
<Container>
|
||||
<HeaderContainer>
|
||||
<TitleContainer>
|
||||
<SectionTitle>Step {step}</SectionTitle>
|
||||
</TitleContainer>
|
||||
{isSource &&
|
||||
<TagContainer>
|
||||
<VSCodeTag>Source</VSCodeTag>
|
||||
</TagContainer>
|
||||
}
|
||||
{isSink &&
|
||||
<TagContainer>
|
||||
<VSCodeTag>Sink</VSCodeTag>
|
||||
</TagContainer>
|
||||
}
|
||||
</HeaderContainer>
|
||||
|
||||
<FileCodeSnippet
|
||||
fileLink={threadFlow.fileLink}
|
||||
codeSnippet={threadFlow.codeSnippet}
|
||||
highlightedRegion={threadFlow.highlightedRegion}
|
||||
severity={severity}
|
||||
message={isSink ? message : threadFlow.message}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
1
extensions/ql-vscode/src/view/common/CodePaths/index.ts
Normal file
1
extensions/ql-vscode/src/view/common/CodePaths/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './CodePaths';
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { HighlightedRegion } from '../../../remote-queries/shared/analysis-result';
|
||||
import { parseHighlightedLine, shouldHighlightLine } from '../../../pure/sarif-utils';
|
||||
|
||||
const replaceSpaceAndTabChar = (text: string) => text.replaceAll(' ', '\u00a0').replaceAll('\t', '\u00a0\u00a0\u00a0\u00a0');
|
||||
|
||||
const HighlightedSpan = styled.span`
|
||||
background-color: var(--vscode-editor-findMatchHighlightBackground);
|
||||
`;
|
||||
|
||||
const PlainCode = ({ text }: { text: string }) => {
|
||||
return <span>{replaceSpaceAndTabChar(text)}</span>;
|
||||
};
|
||||
|
||||
const HighlightedCode = ({ text }: { text: string }) => {
|
||||
return <HighlightedSpan>{replaceSpaceAndTabChar(text)}</HighlightedSpan>;
|
||||
};
|
||||
|
||||
export const CodeSnippetCode = ({
|
||||
line,
|
||||
lineNumber,
|
||||
highlightedRegion
|
||||
}: {
|
||||
line: string,
|
||||
lineNumber: number,
|
||||
highlightedRegion?: HighlightedRegion
|
||||
}) => {
|
||||
if (!highlightedRegion || !shouldHighlightLine(lineNumber, highlightedRegion)) {
|
||||
return <PlainCode text={line} />;
|
||||
}
|
||||
|
||||
const partiallyHighlightedLine = parseHighlightedLine(line, lineNumber, highlightedRegion);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PlainCode text={partiallyHighlightedLine.plainSection1} />
|
||||
<HighlightedCode text={partiallyHighlightedLine.highlightedSection} />
|
||||
<PlainCode text={partiallyHighlightedLine.plainSection2} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AnalysisMessage, HighlightedRegion, ResultSeverity } from '../../../remote-queries/shared/analysis-result';
|
||||
import { CodeSnippetCode } from './CodeSnippetCode';
|
||||
import { CodeSnippetMessage } from './CodeSnippetMessage';
|
||||
|
||||
const MessageContainer = styled.div`
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
`;
|
||||
|
||||
const LineContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const LineNumberContainer = styled.div`
|
||||
border-style: none;
|
||||
padding: 0.01em 0.5em 0.2em;
|
||||
`;
|
||||
|
||||
const CodeSnippetLineCodeContainer = styled.div`
|
||||
flex-grow: 1;
|
||||
border-style: none;
|
||||
padding: 0.01em 0.5em 0.2em 1.5em;
|
||||
word-break: break-word;
|
||||
`;
|
||||
|
||||
type CodeSnippetLineProps = {
|
||||
line: string,
|
||||
lineIndex: number,
|
||||
startingLineIndex: number,
|
||||
highlightedRegion?: HighlightedRegion,
|
||||
severity?: ResultSeverity,
|
||||
message?: AnalysisMessage,
|
||||
messageChildren?: React.ReactNode,
|
||||
};
|
||||
|
||||
export const CodeSnippetLine = ({
|
||||
line,
|
||||
lineIndex,
|
||||
startingLineIndex,
|
||||
highlightedRegion,
|
||||
severity,
|
||||
message,
|
||||
messageChildren
|
||||
}: CodeSnippetLineProps) => {
|
||||
const shouldShowMessage = message &&
|
||||
severity &&
|
||||
highlightedRegion &&
|
||||
highlightedRegion.endLine == startingLineIndex + lineIndex;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LineContainer>
|
||||
<LineNumberContainer>{startingLineIndex + lineIndex}</LineNumberContainer>
|
||||
<CodeSnippetLineCodeContainer>
|
||||
<CodeSnippetCode
|
||||
line={line}
|
||||
lineNumber={startingLineIndex + lineIndex}
|
||||
highlightedRegion={highlightedRegion}
|
||||
/>
|
||||
</CodeSnippetLineCodeContainer>
|
||||
</LineContainer>
|
||||
{shouldShowMessage &&
|
||||
<MessageContainer>
|
||||
<CodeSnippetMessage
|
||||
message={message}
|
||||
severity={severity}
|
||||
>
|
||||
{messageChildren}
|
||||
</CodeSnippetMessage>
|
||||
</MessageContainer>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeLink } from '@vscode/webview-ui-toolkit/react';
|
||||
|
||||
import { AnalysisMessage, ResultSeverity } from '../../../remote-queries/shared/analysis-result';
|
||||
import { createRemoteFileRef } from '../../../pure/location-link-utils';
|
||||
import { VerticalSpace } from '../VerticalSpace';
|
||||
|
||||
const getSeverityColor = (severity: ResultSeverity) => {
|
||||
switch (severity) {
|
||||
case 'Recommendation':
|
||||
return 'var(--vscode-editorInfo-foreground)';
|
||||
case 'Warning':
|
||||
return 'var(--vscode-editorWarning-foreground)';
|
||||
case 'Error':
|
||||
return 'var(--vscode-editorError-foreground)';
|
||||
}
|
||||
};
|
||||
|
||||
const MessageText = styled.div`
|
||||
font-size: small;
|
||||
padding-left: 0.5em;
|
||||
`;
|
||||
|
||||
type CodeSnippetMessageContainerProps = {
|
||||
severity: ResultSeverity;
|
||||
};
|
||||
|
||||
const CodeSnippetMessageContainer = styled.div<CodeSnippetMessageContainerProps>`
|
||||
border-color: var(--vscode-editor-snippetFinalTabstopHighlightBorder);
|
||||
border-width: 0.1em;
|
||||
border-style: solid;
|
||||
border-left-color: ${props => getSeverityColor(props.severity)};
|
||||
border-left-width: 0.3em;
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
`;
|
||||
|
||||
const LocationLink = styled(VSCodeLink)`
|
||||
font-family: var(--vscode-editor-font-family)
|
||||
`;
|
||||
|
||||
type CodeSnippetMessageProps = {
|
||||
message: AnalysisMessage,
|
||||
severity: ResultSeverity,
|
||||
children: React.ReactNode
|
||||
};
|
||||
|
||||
export const CodeSnippetMessage = ({
|
||||
message,
|
||||
severity,
|
||||
children
|
||||
}: CodeSnippetMessageProps) => {
|
||||
return (
|
||||
<CodeSnippetMessageContainer
|
||||
severity={severity}
|
||||
>
|
||||
<MessageText>
|
||||
{message.tokens.map((token, index) => {
|
||||
switch (token.t) {
|
||||
case 'text':
|
||||
return <span key={index}>{token.text}</span>;
|
||||
case 'location':
|
||||
return (
|
||||
<LocationLink
|
||||
key={index}
|
||||
href={
|
||||
createRemoteFileRef(
|
||||
token.location.fileLink,
|
||||
token.location.highlightedRegion?.startLine,
|
||||
token.location.highlightedRegion?.endLine
|
||||
)
|
||||
}
|
||||
>
|
||||
{token.text}
|
||||
</LocationLink>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
})}
|
||||
{
|
||||
children && (
|
||||
<>
|
||||
<VerticalSpace size={2} />
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</MessageText>
|
||||
</CodeSnippetMessageContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeLink } from '@vscode/webview-ui-toolkit/react';
|
||||
|
||||
import {
|
||||
AnalysisMessage,
|
||||
CodeSnippet,
|
||||
FileLink,
|
||||
HighlightedRegion,
|
||||
ResultSeverity
|
||||
} from '../../../remote-queries/shared/analysis-result';
|
||||
import { createRemoteFileRef } from '../../../pure/location-link-utils';
|
||||
import { CodeSnippetMessage } from './CodeSnippetMessage';
|
||||
import { CodeSnippetLine } from './CodeSnippetLine';
|
||||
|
||||
const borderColor = 'var(--vscode-editor-snippetFinalTabstopHighlightBorder)';
|
||||
|
||||
const Container = styled.div`
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: small;
|
||||
`;
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
border: 0.1em solid ${borderColor};
|
||||
border-top-left-radius: 0.2em;
|
||||
border-top-right-radius: 0.2em;
|
||||
padding: 0.5em;
|
||||
`;
|
||||
|
||||
const CodeContainer = styled.div`
|
||||
border-left: 0.1em solid ${borderColor};
|
||||
border-right: 0.1em solid ${borderColor};
|
||||
border-bottom: 0.1em solid ${borderColor};
|
||||
border-bottom-left-radius: 0.2em;
|
||||
border-bottom-right-radius: 0.2em;
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
fileLink: FileLink,
|
||||
codeSnippet?: CodeSnippet,
|
||||
highlightedRegion?: HighlightedRegion,
|
||||
severity?: ResultSeverity,
|
||||
message?: AnalysisMessage,
|
||||
messageChildren?: React.ReactNode,
|
||||
};
|
||||
|
||||
export const FileCodeSnippet = ({
|
||||
fileLink,
|
||||
codeSnippet,
|
||||
highlightedRegion,
|
||||
severity,
|
||||
message,
|
||||
messageChildren,
|
||||
}: Props) => {
|
||||
|
||||
const startingLine = codeSnippet?.startLine || 0;
|
||||
const endingLine = codeSnippet?.endLine || 0;
|
||||
|
||||
const titleFileUri = createRemoteFileRef(
|
||||
fileLink,
|
||||
highlightedRegion?.startLine || startingLine,
|
||||
highlightedRegion?.endLine || endingLine);
|
||||
|
||||
if (!codeSnippet) {
|
||||
return (
|
||||
<Container>
|
||||
<TitleContainer>
|
||||
<VSCodeLink href={titleFileUri}>{fileLink.filePath}</VSCodeLink>
|
||||
</TitleContainer>
|
||||
{message && severity &&
|
||||
<CodeSnippetMessage
|
||||
message={message}
|
||||
severity={severity}
|
||||
>
|
||||
{messageChildren}
|
||||
</CodeSnippetMessage>}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const code = codeSnippet.text.split('\n');
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TitleContainer>
|
||||
<VSCodeLink href={titleFileUri}>{fileLink.filePath}</VSCodeLink>
|
||||
</TitleContainer>
|
||||
<CodeContainer>
|
||||
{code.map((line, index) => (
|
||||
<CodeSnippetLine
|
||||
key={index}
|
||||
line={line}
|
||||
lineIndex={index}
|
||||
startingLineIndex={startingLine}
|
||||
highlightedRegion={highlightedRegion}
|
||||
severity={severity}
|
||||
message={message}
|
||||
messageChildren={messageChildren}
|
||||
/>
|
||||
))}
|
||||
</CodeContainer>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './FileCodeSnippet';
|
||||
@@ -1,9 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const HorizontalSpace = styled.div<{ size: 1 | 2 | 3 }>`
|
||||
export const HorizontalSpace = styled.div<{ size: 1 | 2 | 3 }>`
|
||||
flex: 0 0 auto;
|
||||
display: inline-block;
|
||||
width: ${props => 0.2 * props.size}em;
|
||||
`;
|
||||
|
||||
export default HorizontalSpace;
|
||||
@@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const SectionTitle = styled.h2`
|
||||
export const SectionTitle = styled.h2`
|
||||
font-size: medium;
|
||||
font-weight: 500;
|
||||
padding: 0 0.5em 0 0;
|
||||
@@ -8,5 +8,3 @@ const SectionTitle = styled.h2`
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
`;
|
||||
|
||||
export default SectionTitle;
|
||||
@@ -1,8 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const VerticalSpace = styled.div<{ size: 1 | 2 | 3 }>`
|
||||
export const VerticalSpace = styled.div<{ size: 1 | 2 | 3 }>`
|
||||
flex: 0 0 auto;
|
||||
height: ${props => 0.5 * props.size}em;
|
||||
`;
|
||||
|
||||
export default VerticalSpace;
|
||||
11
extensions/ql-vscode/src/view/common/ViewTitle.tsx
Normal file
11
extensions/ql-vscode/src/view/common/ViewTitle.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ViewTitle = styled.h1`
|
||||
font-size: 2em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 500;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
19
extensions/ql-vscode/src/view/common/icon/Codicon.tsx
Normal file
19
extensions/ql-vscode/src/view/common/icon/Codicon.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
label: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const CodiconIcon = styled.span`
|
||||
vertical-align: text-bottom;
|
||||
`;
|
||||
|
||||
export const Codicon = ({
|
||||
name,
|
||||
label,
|
||||
className
|
||||
}: Props) => <CodiconIcon role="img" aria-label={label} className={classNames('codicon', `codicon-${name}`, className)} />;
|
||||
17
extensions/ql-vscode/src/view/common/icon/ErrorIcon.tsx
Normal file
17
extensions/ql-vscode/src/view/common/icon/ErrorIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Codicon } from './Codicon';
|
||||
|
||||
const Icon = styled(Codicon)`
|
||||
color: var(--vscode-problemsErrorIcon-foreground);
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ErrorIcon = ({
|
||||
label = 'Error',
|
||||
className,
|
||||
}: Props) => <Icon name="error" label={label} className={className} />;
|
||||
17
extensions/ql-vscode/src/view/common/icon/SuccessIcon.tsx
Normal file
17
extensions/ql-vscode/src/view/common/icon/SuccessIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Codicon } from './Codicon';
|
||||
|
||||
const Icon = styled(Codicon)`
|
||||
color: var(--vscode-testing-iconPassed);
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SuccessIcon = ({
|
||||
label = 'Success',
|
||||
className,
|
||||
}: Props) => <Icon name="pass" label={label} className={className} />;
|
||||
17
extensions/ql-vscode/src/view/common/icon/WarningIcon.tsx
Normal file
17
extensions/ql-vscode/src/view/common/icon/WarningIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Codicon } from './Codicon';
|
||||
|
||||
const Icon = styled(Codicon)`
|
||||
color: var(--vscode-problemsWarningIcon-foreground);
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const WarningIcon = ({
|
||||
label = 'Warning',
|
||||
className,
|
||||
}: Props) => <Icon name="warning" label={label} className={className} />;
|
||||
4
extensions/ql-vscode/src/view/common/icon/index.ts
Normal file
4
extensions/ql-vscode/src/view/common/icon/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './Codicon';
|
||||
export * from './ErrorIcon';
|
||||
export * from './SuccessIcon';
|
||||
export * from './WarningIcon';
|
||||
7
extensions/ql-vscode/src/view/common/index.ts
Normal file
7
extensions/ql-vscode/src/view/common/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './icon';
|
||||
export * from './CodePaths';
|
||||
export * from './FileCodeSnippet';
|
||||
export * from './HorizontalSpace';
|
||||
export * from './SectionTitle';
|
||||
export * from './VerticalSpace';
|
||||
export * from './ViewTitle';
|
||||
@@ -1,10 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { WebviewDefinition } from '../webview-interface';
|
||||
import { WebviewDefinition } from '../webview-definition';
|
||||
import { Compare } from './Compare';
|
||||
|
||||
const definition: WebviewDefinition = {
|
||||
component: <Compare />,
|
||||
loadedMessage: 'compareViewLoaded'
|
||||
component: <Compare />
|
||||
};
|
||||
|
||||
export default definition;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { AnalysisAlert } from '../../remote-queries/shared/analysis-result';
|
||||
import CodePaths from './CodePaths';
|
||||
import FileCodeSnippet from './FileCodeSnippet';
|
||||
import { CodePaths, FileCodeSnippet } from '../common';
|
||||
|
||||
const AnalysisAlertResult = ({ alert }: { alert: AnalysisAlert }) => {
|
||||
const showPathsLink = alert.codeFlows.length > 0;
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
import { XCircleIcon } from '@primer/octicons-react';
|
||||
import { Overlay } from '@primer/react';
|
||||
import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTag } from '@vscode/webview-ui-toolkit/react';
|
||||
import * as React from 'react';
|
||||
import { ChangeEvent, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { CodeFlow, AnalysisMessage, ResultSeverity } from '../../remote-queries/shared/analysis-result';
|
||||
import FileCodeSnippet from './FileCodeSnippet';
|
||||
import SectionTitle from './SectionTitle';
|
||||
import VerticalSpace from './VerticalSpace';
|
||||
|
||||
const StyledCloseButton = styled.button`
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 4em;
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
border: none;
|
||||
&:focus-visible {
|
||||
outline: none
|
||||
}
|
||||
`;
|
||||
|
||||
const OverlayContainer = styled.div`
|
||||
padding: 1em;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 2em;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
overflow-y: scroll;
|
||||
`;
|
||||
|
||||
const CloseButton = ({ onClick }: { onClick: () => void }) => (
|
||||
<StyledCloseButton onClick={onClick} tabIndex={-1} >
|
||||
<XCircleIcon size={24} />
|
||||
</StyledCloseButton>
|
||||
);
|
||||
|
||||
const CodePath = ({
|
||||
codeFlow,
|
||||
message,
|
||||
severity
|
||||
}: {
|
||||
codeFlow: CodeFlow;
|
||||
message: AnalysisMessage;
|
||||
severity: ResultSeverity;
|
||||
}) => {
|
||||
return <>
|
||||
{codeFlow.threadFlows.map((threadFlow, index) =>
|
||||
<div key={`thread-flow-${index}`} style={{ maxWidth: '55em' }}>
|
||||
{index !== 0 && <VerticalSpace size={3} />}
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<div style={{ flexGrow: 1, padding: 0, border: 'none' }}>
|
||||
<SectionTitle>Step {index + 1}</SectionTitle>
|
||||
</div>
|
||||
{index === 0 &&
|
||||
<div style={{ padding: 0, border: 'none' }}>
|
||||
<VSCodeTag>Source</VSCodeTag>
|
||||
</div>
|
||||
}
|
||||
{index === codeFlow.threadFlows.length - 1 &&
|
||||
<div style={{ padding: 0, border: 'none' }}>
|
||||
<VSCodeTag>Sink</VSCodeTag>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<VerticalSpace size={2} />
|
||||
<FileCodeSnippet
|
||||
fileLink={threadFlow.fileLink}
|
||||
codeSnippet={threadFlow.codeSnippet}
|
||||
highlightedRegion={threadFlow.highlightedRegion}
|
||||
severity={severity}
|
||||
message={index === codeFlow.threadFlows.length - 1 ? message : threadFlow.message} />
|
||||
</div>
|
||||
)}
|
||||
</>;
|
||||
};
|
||||
|
||||
const getCodeFlowName = (codeFlow: CodeFlow) => {
|
||||
const filePath = codeFlow.threadFlows[codeFlow.threadFlows.length - 1].fileLink.filePath;
|
||||
return filePath.substring(filePath.lastIndexOf('/') + 1);
|
||||
};
|
||||
|
||||
const Menu = ({
|
||||
codeFlows,
|
||||
setSelectedCodeFlow
|
||||
}: {
|
||||
codeFlows: CodeFlow[],
|
||||
setSelectedCodeFlow: (value: React.SetStateAction<CodeFlow>) => void
|
||||
}) => {
|
||||
return <VSCodeDropdown
|
||||
onChange={(event: ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedOption = event.target;
|
||||
const selectedIndex = selectedOption.value as unknown as number;
|
||||
setSelectedCodeFlow(codeFlows[selectedIndex]);
|
||||
}}
|
||||
>
|
||||
{codeFlows.map((codeFlow, index) =>
|
||||
<VSCodeOption
|
||||
key={`codeflow-${index}'`}
|
||||
value={index}
|
||||
>
|
||||
{getCodeFlowName(codeFlow)}
|
||||
</VSCodeOption>
|
||||
)}
|
||||
</VSCodeDropdown>;
|
||||
};
|
||||
|
||||
const CodePaths = ({
|
||||
codeFlows,
|
||||
ruleDescription,
|
||||
message,
|
||||
severity
|
||||
}: {
|
||||
codeFlows: CodeFlow[],
|
||||
ruleDescription: string,
|
||||
message: AnalysisMessage,
|
||||
severity: ResultSeverity
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedCodeFlow, setSelectedCodeFlow] = useState(codeFlows[0]);
|
||||
|
||||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
const linkRef = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
const closeOverlay = () => setIsOpen(false);
|
||||
|
||||
return (
|
||||
<div ref={anchorRef}>
|
||||
<VSCodeLink
|
||||
onClick={() => setIsOpen(true)}
|
||||
ref={linkRef}
|
||||
sx={{ cursor: 'pointer' }}>
|
||||
Show paths
|
||||
</VSCodeLink>
|
||||
{isOpen && (
|
||||
<Overlay
|
||||
returnFocusRef={linkRef}
|
||||
onEscape={closeOverlay}
|
||||
onClickOutside={closeOverlay}
|
||||
anchorSide="outside-top">
|
||||
<OverlayContainer>
|
||||
<CloseButton onClick={closeOverlay} />
|
||||
|
||||
<SectionTitle>{ruleDescription}</SectionTitle>
|
||||
<VerticalSpace size={2} />
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<div style={{ padding: 0, border: 0 }}>
|
||||
{codeFlows.length} paths available: {selectedCodeFlow.threadFlows.length} steps in
|
||||
</div>
|
||||
<div style={{ flexGrow: 1, padding: 0, paddingLeft: '0.2em', border: 'none' }}>
|
||||
<Menu codeFlows={codeFlows} setSelectedCodeFlow={setSelectedCodeFlow} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VerticalSpace size={2} />
|
||||
<CodePath
|
||||
codeFlow={selectedCodeFlow}
|
||||
severity={severity}
|
||||
message={message} />
|
||||
|
||||
<VerticalSpace size={3} />
|
||||
|
||||
</OverlayContainer>
|
||||
</Overlay>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodePaths;
|
||||
@@ -1,261 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { CodeSnippet, FileLink, HighlightedRegion, AnalysisMessage, ResultSeverity } from '../../remote-queries/shared/analysis-result';
|
||||
import VerticalSpace from './VerticalSpace';
|
||||
import { createRemoteFileRef } from '../../pure/location-link-utils';
|
||||
import { parseHighlightedLine, shouldHighlightLine } from '../../pure/sarif-utils';
|
||||
import { VSCodeLink } from '@vscode/webview-ui-toolkit/react';
|
||||
|
||||
const borderColor = 'var(--vscode-editor-snippetFinalTabstopHighlightBorder)';
|
||||
const warningColor = '#966C23';
|
||||
const highlightColor = 'var(--vscode-editor-findMatchHighlightBackground)';
|
||||
|
||||
const getSeverityColor = (severity: ResultSeverity) => {
|
||||
switch (severity) {
|
||||
case 'Recommendation':
|
||||
return 'blue';
|
||||
case 'Warning':
|
||||
return warningColor;
|
||||
case 'Error':
|
||||
return 'red';
|
||||
}
|
||||
};
|
||||
|
||||
const replaceSpaceAndTabChar = (text: string) => text.replaceAll(' ', '\u00a0').replaceAll('\t', '\u00a0\u00a0\u00a0\u00a0');
|
||||
|
||||
const Container = styled.div`
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: small;
|
||||
`;
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
border: 0.1em solid ${borderColor};
|
||||
border-top-left-radius: 0.2em;
|
||||
border-top-right-radius: 0.2em;
|
||||
padding: 0.5em;
|
||||
`;
|
||||
|
||||
const CodeContainer = styled.div`
|
||||
border-left: 0.1em solid ${borderColor};
|
||||
border-right: 0.1em solid ${borderColor};
|
||||
border-bottom: 0.1em solid ${borderColor};
|
||||
border-bottom-left-radius: 0.2em;
|
||||
border-bottom-right-radius: 0.2em;
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
`;
|
||||
|
||||
const MessageText = styled.div`
|
||||
font-size: small;
|
||||
padding-left: 0.5em;
|
||||
`;
|
||||
|
||||
const MessageContainer = styled.div`
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
`;
|
||||
|
||||
const PlainCode = ({ text }: { text: string }) => {
|
||||
return <span>{replaceSpaceAndTabChar(text)}</span>;
|
||||
};
|
||||
|
||||
const HighlightedCode = ({ text }: { text: string }) => {
|
||||
return <span style={{ backgroundColor: highlightColor }}>{replaceSpaceAndTabChar(text)}</span>;
|
||||
};
|
||||
|
||||
const Message = ({
|
||||
message,
|
||||
borderLeftColor,
|
||||
children
|
||||
}: {
|
||||
message: AnalysisMessage,
|
||||
borderLeftColor: string,
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
return <div style={{
|
||||
borderColor: borderColor,
|
||||
borderWidth: '0.1em',
|
||||
borderStyle: 'solid',
|
||||
borderLeftColor: borderLeftColor,
|
||||
borderLeftWidth: '0.3em',
|
||||
paddingTop: '1em',
|
||||
paddingBottom: '1em'
|
||||
}}>
|
||||
<MessageText>
|
||||
{message.tokens.map((token, index) => {
|
||||
switch (token.t) {
|
||||
case 'text':
|
||||
return <span key={`token-${index}`}>{token.text}</span>;
|
||||
case 'location':
|
||||
return <VSCodeLink
|
||||
style={{ fontFamily: 'var(--vscode-editor-font-family)' }}
|
||||
key={`token-${index}`}
|
||||
href={createRemoteFileRef(
|
||||
token.location.fileLink,
|
||||
token.location.highlightedRegion?.startLine,
|
||||
token.location.highlightedRegion?.endLine)}>
|
||||
{token.text}
|
||||
</VSCodeLink>;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
})}
|
||||
{children && <>
|
||||
<VerticalSpace size={2} />
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
</MessageText>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const Code = ({
|
||||
line,
|
||||
lineNumber,
|
||||
highlightedRegion
|
||||
}: {
|
||||
line: string,
|
||||
lineNumber: number,
|
||||
highlightedRegion?: HighlightedRegion
|
||||
}) => {
|
||||
if (!highlightedRegion || !shouldHighlightLine(lineNumber, highlightedRegion)) {
|
||||
return <PlainCode text={line} />;
|
||||
}
|
||||
|
||||
const partiallyHighlightedLine = parseHighlightedLine(line, lineNumber, highlightedRegion);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PlainCode text={partiallyHighlightedLine.plainSection1} />
|
||||
<HighlightedCode text={partiallyHighlightedLine.highlightedSection} />
|
||||
<PlainCode text={partiallyHighlightedLine.plainSection2} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Line = ({
|
||||
line,
|
||||
lineIndex,
|
||||
startingLineIndex,
|
||||
highlightedRegion,
|
||||
severity,
|
||||
message,
|
||||
messageChildren
|
||||
}: {
|
||||
line: string,
|
||||
lineIndex: number,
|
||||
startingLineIndex: number,
|
||||
highlightedRegion?: HighlightedRegion,
|
||||
severity?: ResultSeverity,
|
||||
message?: AnalysisMessage,
|
||||
messageChildren?: React.ReactNode,
|
||||
}) => {
|
||||
const shouldShowMessage = message &&
|
||||
severity &&
|
||||
highlightedRegion &&
|
||||
highlightedRegion.endLine == startingLineIndex + lineIndex;
|
||||
|
||||
return <div>
|
||||
<div style={{ display: 'flex' }} >
|
||||
<div style={{
|
||||
borderStyle: 'none',
|
||||
paddingTop: '0.01em',
|
||||
paddingLeft: '0.5em',
|
||||
paddingRight: '0.5em',
|
||||
paddingBottom: '0.2em'
|
||||
}}>
|
||||
{startingLineIndex + lineIndex}
|
||||
</div>
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
borderStyle: 'none',
|
||||
paddingTop: '0.01em',
|
||||
paddingLeft: '1.5em',
|
||||
paddingRight: '0.5em',
|
||||
paddingBottom: '0.2em',
|
||||
wordBreak: 'break-word'
|
||||
}}>
|
||||
<Code
|
||||
line={line}
|
||||
lineNumber={startingLineIndex + lineIndex}
|
||||
highlightedRegion={highlightedRegion} />
|
||||
</div>
|
||||
</div>
|
||||
{shouldShowMessage &&
|
||||
<MessageContainer>
|
||||
<Message
|
||||
message={message}
|
||||
borderLeftColor={getSeverityColor(severity)}>
|
||||
{messageChildren}
|
||||
</Message>
|
||||
</MessageContainer>
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
const FileCodeSnippet = ({
|
||||
fileLink,
|
||||
codeSnippet,
|
||||
highlightedRegion,
|
||||
severity,
|
||||
message,
|
||||
messageChildren,
|
||||
}: {
|
||||
fileLink: FileLink,
|
||||
codeSnippet?: CodeSnippet,
|
||||
highlightedRegion?: HighlightedRegion,
|
||||
severity?: ResultSeverity,
|
||||
message?: AnalysisMessage,
|
||||
messageChildren?: React.ReactNode,
|
||||
}) => {
|
||||
|
||||
const startingLine = codeSnippet?.startLine || 0;
|
||||
const endingLine = codeSnippet?.endLine || 0;
|
||||
|
||||
const titleFileUri = createRemoteFileRef(
|
||||
fileLink,
|
||||
highlightedRegion?.startLine || startingLine,
|
||||
highlightedRegion?.endLine || endingLine);
|
||||
|
||||
if (!codeSnippet) {
|
||||
return (
|
||||
<Container>
|
||||
<TitleContainer>
|
||||
<VSCodeLink href={titleFileUri}>{fileLink.filePath}</VSCodeLink>
|
||||
</TitleContainer>
|
||||
{message && severity &&
|
||||
<Message
|
||||
message={message}
|
||||
borderLeftColor={getSeverityColor(severity)}>
|
||||
{messageChildren}
|
||||
</Message>}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const code = codeSnippet.text.split('\n');
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TitleContainer>
|
||||
<VSCodeLink href={titleFileUri}>{fileLink.filePath}</VSCodeLink>
|
||||
</TitleContainer>
|
||||
<CodeContainer>
|
||||
{code.map((line, index) => (
|
||||
<Line
|
||||
key={`line-${index}`}
|
||||
line={line}
|
||||
lineIndex={index}
|
||||
startingLineIndex={startingLine}
|
||||
highlightedRegion={highlightedRegion}
|
||||
severity={severity}
|
||||
message={message}
|
||||
messageChildren={messageChildren}
|
||||
/>
|
||||
))}
|
||||
</CodeContainer>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileCodeSnippet;
|
||||
@@ -6,10 +6,7 @@ import { AnalysisSummary, RemoteQueryResult } from '../../remote-queries/shared/
|
||||
import { MAX_RAW_RESULTS } from '../../remote-queries/shared/result-limits';
|
||||
import { vscode } from '../vscode-api';
|
||||
import { VSCodeBadge, VSCodeButton } from '@vscode/webview-ui-toolkit/react';
|
||||
import SectionTitle from './SectionTitle';
|
||||
import VerticalSpace from './VerticalSpace';
|
||||
import HorizontalSpace from './HorizontalSpace';
|
||||
import ViewTitle from './ViewTitle';
|
||||
import { HorizontalSpace, SectionTitle, VerticalSpace, ViewTitle } from '../common';
|
||||
import DownloadButton from './DownloadButton';
|
||||
import { AnalysisResults, getAnalysisResultCount } from '../../remote-queries/shared/analysis-result';
|
||||
import DownloadSpinner from './DownloadSpinner';
|
||||
@@ -273,9 +270,10 @@ const AnalysesResultsTitle = ({ totalAnalysesResults, totalResults }: { totalAna
|
||||
return <SectionTitle>{totalAnalysesResults}/{totalResults} results</SectionTitle>;
|
||||
};
|
||||
|
||||
const exportResults = () => {
|
||||
const exportResults = (queryResult: RemoteQueryResult) => {
|
||||
vscode.postMessage({
|
||||
t: 'remoteQueryExportResults',
|
||||
queryId: queryResult.queryId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -365,7 +363,7 @@ const AnalysesResults = ({
|
||||
totalResults={totalResults} />
|
||||
</div>
|
||||
<div>
|
||||
<VSCodeButton onClick={exportResults}>Export all</VSCodeButton>
|
||||
<VSCodeButton onClick={() => exportResults(queryResult)}>Export all</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
<AnalysesResultsDescription
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ViewTitle = styled.h1`
|
||||
font-size: large;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
export default ViewTitle;
|
||||
@@ -1,10 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { WebviewDefinition } from '../webview-interface';
|
||||
import { WebviewDefinition } from '../webview-definition';
|
||||
import { RemoteQueries } from './RemoteQueries';
|
||||
|
||||
const definition: WebviewDefinition = {
|
||||
component: <RemoteQueries />,
|
||||
loadedMessage: 'remoteQueryLoaded'
|
||||
component: <RemoteQueries />
|
||||
};
|
||||
|
||||
export default definition;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { WebviewDefinition } from '../webview-interface';
|
||||
import { WebviewDefinition } from '../webview-definition';
|
||||
import { ResultsApp } from './results';
|
||||
|
||||
const definition: WebviewDefinition = {
|
||||
component: <ResultsApp />,
|
||||
loadedMessage: 'resultViewLoaded'
|
||||
component: <ResultsApp />
|
||||
};
|
||||
|
||||
export default definition;
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"experimentalDecorators": true
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
7
extensions/ql-vscode/src/view/tsconfig.spec.json
Normal file
7
extensions/ql-vscode/src/view/tsconfig.spec.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
},
|
||||
"exclude": []
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { VSCodeLink } from '@vscode/webview-ui-toolkit/react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const LinkIconButton = styled(VSCodeLink)`
|
||||
.codicon {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ViewTitle } from '../common';
|
||||
import { LinkIconButton } from './LinkIconButton';
|
||||
|
||||
export type QueryDetailsProps = {
|
||||
queryName: string;
|
||||
queryFileName: string;
|
||||
|
||||
onOpenQueryFileClick: () => void;
|
||||
onViewQueryTextClick: () => void;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
max-width: 100%;
|
||||
`;
|
||||
|
||||
const QueryActions = styled.div`
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
`;
|
||||
|
||||
export const QueryDetails = ({
|
||||
queryName,
|
||||
queryFileName,
|
||||
onOpenQueryFileClick,
|
||||
onViewQueryTextClick,
|
||||
}: QueryDetailsProps) => {
|
||||
return (
|
||||
<Container>
|
||||
<ViewTitle>{queryName}</ViewTitle>
|
||||
<QueryActions>
|
||||
<LinkIconButton onClick={onOpenQueryFileClick}>
|
||||
<span slot="start" className="codicon codicon-file-code"></span>
|
||||
{queryFileName}
|
||||
</LinkIconButton>
|
||||
<LinkIconButton onClick={onViewQueryTextClick}>
|
||||
<span slot="start" className="codicon codicon-code"></span>
|
||||
View query
|
||||
</LinkIconButton>
|
||||
</QueryActions>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
27
extensions/ql-vscode/src/view/variant-analysis/StatItem.tsx
Normal file
27
extensions/ql-vscode/src/view/variant-analysis/StatItem.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
title: ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
color: var(--vscode-badge-foreground);
|
||||
font-size: 0.85em;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.6em;
|
||||
`;
|
||||
|
||||
export const StatItem = ({ title, children }: Props) => (
|
||||
<Container>
|
||||
<Header>{title}</Header>
|
||||
<div>{children}</div>
|
||||
</Container>
|
||||
);
|
||||
@@ -0,0 +1,120 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisDomainModel,
|
||||
VariantAnalysisQueryLanguage,
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisStatus
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
import { VariantAnalysisContainer } from './VariantAnalysisContainer';
|
||||
import { VariantAnalysisHeader } from './VariantAnalysisHeader';
|
||||
|
||||
const variantAnalysis: VariantAnalysisDomainModel = {
|
||||
id: 1,
|
||||
controllerRepoId: 1,
|
||||
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,
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
export function VariantAnalysis(): JSX.Element {
|
||||
return (
|
||||
<VariantAnalysisContainer>
|
||||
<VariantAnalysisHeader
|
||||
variantAnalysis={variantAnalysis}
|
||||
onOpenQueryFileClick={() => console.log('Open query')}
|
||||
onViewQueryTextClick={() => console.log('View query')}
|
||||
onStopQueryClick={() => console.log('Stop query')}
|
||||
onCopyRepositoryListClick={() => console.log('Copy repository list')}
|
||||
onExportResultsClick={() => console.log('Export results')}
|
||||
onViewLogsClick={() => console.log('View logs')}
|
||||
/>
|
||||
</VariantAnalysisContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeButton } from '@vscode/webview-ui-toolkit/react';
|
||||
import { VariantAnalysisStatus } from '../../remote-queries/shared/variant-analysis';
|
||||
|
||||
type Props = {
|
||||
variantAnalysisStatus: VariantAnalysisStatus;
|
||||
|
||||
onStopQueryClick: () => void;
|
||||
|
||||
onCopyRepositoryListClick: () => void;
|
||||
onExportResultsClick: () => void;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
`;
|
||||
|
||||
const Button = styled(VSCodeButton)`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const VariantAnalysisActions = ({
|
||||
variantAnalysisStatus,
|
||||
onStopQueryClick,
|
||||
onCopyRepositoryListClick,
|
||||
onExportResultsClick
|
||||
}: Props) => {
|
||||
return (
|
||||
<Container>
|
||||
{variantAnalysisStatus === VariantAnalysisStatus.InProgress && (
|
||||
<Button appearance="secondary" onClick={onStopQueryClick}>
|
||||
Stop query
|
||||
</Button>
|
||||
)}
|
||||
{variantAnalysisStatus === VariantAnalysisStatus.Succeeded && (
|
||||
<>
|
||||
<Button appearance="secondary" onClick={onCopyRepositoryListClick}>
|
||||
Copy repository list
|
||||
</Button>
|
||||
<Button appearance="primary" onClick={onExportResultsClick}>
|
||||
Export results
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const VariantAnalysisContainer = styled.div`
|
||||
max-width: 55em;
|
||||
`;
|
||||
@@ -0,0 +1,93 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
getSkippedRepoCount, getTotalResultCount,
|
||||
hasRepoScanCompleted,
|
||||
VariantAnalysis,
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
import { QueryDetails } from './QueryDetails';
|
||||
import { VariantAnalysisActions } from './VariantAnalysisActions';
|
||||
import { VariantAnalysisStats } from './VariantAnalysisStats';
|
||||
|
||||
export type VariantAnalysisHeaderProps = {
|
||||
variantAnalysis: VariantAnalysis;
|
||||
|
||||
duration?: number | undefined;
|
||||
completedAt?: Date | undefined;
|
||||
|
||||
onOpenQueryFileClick: () => void;
|
||||
onViewQueryTextClick: () => void;
|
||||
|
||||
onStopQueryClick: () => void;
|
||||
|
||||
onCopyRepositoryListClick: () => void;
|
||||
onExportResultsClick: () => void;
|
||||
|
||||
onViewLogsClick: () => void;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2em;
|
||||
`;
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const VariantAnalysisHeader = ({
|
||||
variantAnalysis,
|
||||
duration,
|
||||
completedAt,
|
||||
onOpenQueryFileClick,
|
||||
onViewQueryTextClick,
|
||||
onStopQueryClick,
|
||||
onCopyRepositoryListClick,
|
||||
onExportResultsClick,
|
||||
onViewLogsClick,
|
||||
}: VariantAnalysisHeaderProps) => {
|
||||
const totalScannedRepositoryCount = useMemo(() => {
|
||||
return variantAnalysis.scannedRepos?.length ?? 0;
|
||||
}, [variantAnalysis.scannedRepos]);
|
||||
const completedRepositoryCount = useMemo(() => {
|
||||
return variantAnalysis.scannedRepos?.filter(repo => hasRepoScanCompleted(repo))?.length ?? 0;
|
||||
}, [variantAnalysis.scannedRepos]);
|
||||
const resultCount = useMemo(() => {
|
||||
return getTotalResultCount(variantAnalysis.scannedRepos);
|
||||
}, [variantAnalysis.scannedRepos]);
|
||||
const hasSkippedRepos = useMemo(() => {
|
||||
return getSkippedRepoCount(variantAnalysis.skippedRepos) > 0;
|
||||
}, [variantAnalysis.skippedRepos]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
<QueryDetails
|
||||
queryName={variantAnalysis.query.name}
|
||||
queryFileName={variantAnalysis.query.filePath}
|
||||
onOpenQueryFileClick={onOpenQueryFileClick}
|
||||
onViewQueryTextClick={onViewQueryTextClick}
|
||||
/>
|
||||
<VariantAnalysisActions
|
||||
variantAnalysisStatus={variantAnalysis.status}
|
||||
onStopQueryClick={onStopQueryClick}
|
||||
onCopyRepositoryListClick={onCopyRepositoryListClick}
|
||||
onExportResultsClick={onExportResultsClick}
|
||||
/>
|
||||
</Row>
|
||||
<VariantAnalysisStats
|
||||
variantAnalysisStatus={variantAnalysis.status}
|
||||
totalRepositoryCount={totalScannedRepositoryCount}
|
||||
completedRepositoryCount={completedRepositoryCount}
|
||||
resultCount={resultCount}
|
||||
hasWarnings={hasSkippedRepos}
|
||||
duration={duration}
|
||||
completedAt={completedAt}
|
||||
onViewLogsClick={onViewLogsClick}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import { VariantAnalysisStatus } from '../../remote-queries/shared/variant-analysis';
|
||||
import { formatDecimal } from '../../pure/number';
|
||||
import { ErrorIcon, HorizontalSpace, SuccessIcon, WarningIcon } from '../common';
|
||||
|
||||
type Props = {
|
||||
variantAnalysisStatus: VariantAnalysisStatus;
|
||||
|
||||
totalRepositoryCount: number;
|
||||
completedRepositoryCount?: number | undefined;
|
||||
|
||||
showWarning?: boolean;
|
||||
};
|
||||
|
||||
export const VariantAnalysisRepositoriesStats = ({
|
||||
variantAnalysisStatus,
|
||||
totalRepositoryCount,
|
||||
completedRepositoryCount = 0,
|
||||
showWarning,
|
||||
}: Props) => {
|
||||
if (variantAnalysisStatus === VariantAnalysisStatus.Failed) {
|
||||
return (
|
||||
<>
|
||||
0<HorizontalSpace size={2} /><ErrorIcon />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{formatDecimal(completedRepositoryCount)}/{formatDecimal(totalRepositoryCount)}
|
||||
{showWarning && <><HorizontalSpace size={2} /><WarningIcon /></>}
|
||||
{!showWarning && variantAnalysisStatus === VariantAnalysisStatus.Succeeded &&
|
||||
<><HorizontalSpace size={2} /><SuccessIcon label="Completed" /></>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VariantAnalysisStatus } from '../../remote-queries/shared/variant-analysis';
|
||||
import { StatItem } from './StatItem';
|
||||
import { formatDecimal } from '../../pure/number';
|
||||
import { humanizeUnit } from '../../pure/time';
|
||||
import { VariantAnalysisRepositoriesStats } from './VariantAnalysisRepositoriesStats';
|
||||
import { VariantAnalysisStatusStats } from './VariantAnalysisStatusStats';
|
||||
|
||||
export type VariantAnalysisStatsProps = {
|
||||
variantAnalysisStatus: VariantAnalysisStatus;
|
||||
|
||||
totalRepositoryCount: number;
|
||||
completedRepositoryCount?: number | undefined;
|
||||
|
||||
hasWarnings?: boolean;
|
||||
|
||||
resultCount?: number | undefined;
|
||||
duration?: number | undefined;
|
||||
completedAt?: Date | undefined;
|
||||
|
||||
onViewLogsClick: () => void;
|
||||
};
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 1em;
|
||||
`;
|
||||
|
||||
export const VariantAnalysisStats = ({
|
||||
variantAnalysisStatus,
|
||||
totalRepositoryCount,
|
||||
completedRepositoryCount = 0,
|
||||
hasWarnings,
|
||||
resultCount,
|
||||
duration,
|
||||
completedAt,
|
||||
onViewLogsClick,
|
||||
}: VariantAnalysisStatsProps) => {
|
||||
const completionHeaderName = useMemo(() => {
|
||||
if (variantAnalysisStatus === VariantAnalysisStatus.InProgress) {
|
||||
return 'Running';
|
||||
}
|
||||
|
||||
if (variantAnalysisStatus === VariantAnalysisStatus.Failed) {
|
||||
return 'Failed';
|
||||
}
|
||||
|
||||
if (variantAnalysisStatus === VariantAnalysisStatus.Canceled) {
|
||||
return 'Stopped';
|
||||
}
|
||||
|
||||
if (variantAnalysisStatus === VariantAnalysisStatus.Succeeded && hasWarnings) {
|
||||
return 'Succeeded warnings';
|
||||
}
|
||||
|
||||
return 'Succeeded';
|
||||
}, [variantAnalysisStatus, hasWarnings]);
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<StatItem title="Results">
|
||||
{resultCount !== undefined ? formatDecimal(resultCount) : '-'}
|
||||
</StatItem>
|
||||
<StatItem title="Repositories">
|
||||
<VariantAnalysisRepositoriesStats
|
||||
variantAnalysisStatus={variantAnalysisStatus}
|
||||
totalRepositoryCount={totalRepositoryCount}
|
||||
completedRepositoryCount={completedRepositoryCount}
|
||||
showWarning={hasWarnings}
|
||||
/>
|
||||
</StatItem>
|
||||
<StatItem title="Duration">
|
||||
{duration !== undefined ? humanizeUnit(duration) : '-'}
|
||||
</StatItem>
|
||||
<StatItem title={completionHeaderName}>
|
||||
<VariantAnalysisStatusStats
|
||||
completedAt={completedAt}
|
||||
onViewLogsClick={onViewLogsClick}
|
||||
/>
|
||||
</StatItem>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeLink } from '@vscode/webview-ui-toolkit/react';
|
||||
import { formatDate } from '../../pure/date';
|
||||
|
||||
type Props = {
|
||||
completedAt?: Date | undefined;
|
||||
|
||||
onViewLogsClick: () => void;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
`;
|
||||
|
||||
const Icon = styled.span`
|
||||
font-size: 1em !important;
|
||||
vertical-align: text-bottom;
|
||||
`;
|
||||
|
||||
export const VariantAnalysisStatusStats = ({
|
||||
completedAt,
|
||||
onViewLogsClick,
|
||||
}: Props) => {
|
||||
if (completedAt === undefined) {
|
||||
return <Icon className="codicon codicon-loading codicon-modifier-spin" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<span>{formatDate(completedAt)}</span>
|
||||
<VSCodeLink onClick={onViewLogsClick}>View logs</VSCodeLink>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as React from 'react';
|
||||
import { render as reactRender, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { QueryDetails, QueryDetailsProps } from '../QueryDetails';
|
||||
|
||||
describe(QueryDetails.name, () => {
|
||||
const onOpenQueryFileClick = jest.fn();
|
||||
const onViewQueryTextClick = jest.fn();
|
||||
const onStopQueryClick = jest.fn();
|
||||
const onCopyRepositoryListClick = jest.fn();
|
||||
const onExportResultsClick = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
onOpenQueryFileClick.mockReset();
|
||||
onViewQueryTextClick.mockReset();
|
||||
onStopQueryClick.mockReset();
|
||||
onCopyRepositoryListClick.mockReset();
|
||||
onExportResultsClick.mockReset();
|
||||
});
|
||||
|
||||
const render = (props: Partial<QueryDetailsProps> = {}) =>
|
||||
reactRender(
|
||||
<QueryDetails
|
||||
queryName="Query name"
|
||||
queryFileName="example.ql"
|
||||
onOpenQueryFileClick={onOpenQueryFileClick}
|
||||
onViewQueryTextClick={onViewQueryTextClick}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
it('renders correctly', () => {
|
||||
render();
|
||||
|
||||
expect(screen.getByText('Query name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the query file name as a button', async () => {
|
||||
render();
|
||||
|
||||
await userEvent.click(screen.getByText('example.ql'));
|
||||
expect(onOpenQueryFileClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders a view query button', async () => {
|
||||
render();
|
||||
|
||||
await userEvent.click(screen.getByText('View query'));
|
||||
expect(onViewQueryTextClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
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';
|
||||
|
||||
describe(VariantAnalysisActions.name, () => {
|
||||
const onStopQueryClick = jest.fn();
|
||||
const onCopyRepositoryListClick = jest.fn();
|
||||
const onExportResultsClick = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
onStopQueryClick.mockReset();
|
||||
onCopyRepositoryListClick.mockReset();
|
||||
onExportResultsClick.mockReset();
|
||||
});
|
||||
|
||||
const render = (variantAnalysisStatus: VariantAnalysisStatus) =>
|
||||
reactRender(
|
||||
<VariantAnalysisActions
|
||||
variantAnalysisStatus={variantAnalysisStatus}
|
||||
onStopQueryClick={onStopQueryClick}
|
||||
onCopyRepositoryListClick={onCopyRepositoryListClick}
|
||||
onExportResultsClick={onExportResultsClick}
|
||||
/>
|
||||
);
|
||||
|
||||
it('renders 1 button when in progress', async () => {
|
||||
const { container } = render(VariantAnalysisStatus.InProgress);
|
||||
|
||||
expect(container.querySelectorAll('vscode-button').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('renders the stop query button when in progress', async () => {
|
||||
render(VariantAnalysisStatus.InProgress);
|
||||
|
||||
await userEvent.click(screen.getByText('Stop query'));
|
||||
expect(onStopQueryClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders 2 buttons when succeeded', async () => {
|
||||
const { container } = render(VariantAnalysisStatus.Succeeded);
|
||||
|
||||
expect(container.querySelectorAll('vscode-button').length).toEqual(2);
|
||||
});
|
||||
|
||||
it('renders the copy repository list button when succeeded', async () => {
|
||||
render(VariantAnalysisStatus.Succeeded);
|
||||
|
||||
await userEvent.click(screen.getByText('Copy repository list'));
|
||||
expect(onCopyRepositoryListClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders the export results button when succeeded', async () => {
|
||||
render(VariantAnalysisStatus.Succeeded);
|
||||
|
||||
await userEvent.click(screen.getByText('Export results'));
|
||||
expect(onExportResultsClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not render any buttons when failed', () => {
|
||||
const { container } = render(VariantAnalysisStatus.Failed);
|
||||
|
||||
expect(container.querySelectorAll('vscode-button').length).toEqual(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import * as React from 'react';
|
||||
import { render as reactRender, screen } from '@testing-library/react';
|
||||
import { VariantAnalysisStatus } from '../../../remote-queries/shared/variant-analysis';
|
||||
import { VariantAnalysisStats, VariantAnalysisStatsProps } from '../VariantAnalysisStats';
|
||||
import { userEvent } from '@storybook/testing-library';
|
||||
|
||||
describe(VariantAnalysisStats.name, () => {
|
||||
const onViewLogsClick = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
onViewLogsClick.mockReset();
|
||||
});
|
||||
|
||||
const render = (props: Partial<VariantAnalysisStatsProps> = {}) =>
|
||||
reactRender(
|
||||
<VariantAnalysisStats
|
||||
variantAnalysisStatus={VariantAnalysisStatus.InProgress}
|
||||
totalRepositoryCount={10}
|
||||
onViewLogsClick={onViewLogsClick}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
it('renders correctly', () => {
|
||||
render();
|
||||
|
||||
expect(screen.getByText('Results')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the number of results as a formatted number', () => {
|
||||
render({ resultCount: 123456 });
|
||||
|
||||
expect(screen.getByText('123,456')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the number of repositories as a formatted number', () => {
|
||||
render({ totalRepositoryCount: 123456, completedRepositoryCount: 654321 });
|
||||
|
||||
expect(screen.getByText('654,321/123,456')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a warning icon when has warnings is set', () => {
|
||||
render({ hasWarnings: true });
|
||||
|
||||
expect(screen.getByRole('img', {
|
||||
name: 'Warning',
|
||||
})).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders an error icon when the variant analysis status is failed', () => {
|
||||
render({ variantAnalysisStatus: VariantAnalysisStatus.Failed });
|
||||
|
||||
expect(screen.getByRole('img', {
|
||||
name: 'Error',
|
||||
})).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a completed icon when the variant analysis status is succeeded', () => {
|
||||
render({ variantAnalysisStatus: VariantAnalysisStatus.Succeeded });
|
||||
|
||||
expect(screen.getByRole('img', {
|
||||
name: 'Completed',
|
||||
})).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a view logs link when the variant analysis status is succeeded', () => {
|
||||
render({ variantAnalysisStatus: VariantAnalysisStatus.Succeeded, completedAt: new Date() });
|
||||
|
||||
userEvent.click(screen.getByText('View logs'));
|
||||
expect(onViewLogsClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders a running text when the variant analysis status is in progress', () => {
|
||||
render({ variantAnalysisStatus: VariantAnalysisStatus.InProgress });
|
||||
|
||||
expect(screen.getByText('Running')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a failed text when the variant analysis status is failed', () => {
|
||||
render({ variantAnalysisStatus: VariantAnalysisStatus.Failed });
|
||||
|
||||
expect(screen.getByText('Failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a stopped text when the variant analysis status is canceled', () => {
|
||||
render({ variantAnalysisStatus: VariantAnalysisStatus.Canceled });
|
||||
|
||||
expect(screen.getByText('Stopped')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a succeeded warnings text when the variant analysis status is succeeded and has warnings', () => {
|
||||
render({ variantAnalysisStatus: VariantAnalysisStatus.Succeeded, hasWarnings: true });
|
||||
|
||||
expect(screen.getByText('Succeeded warnings')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a succeeded text when the variant analysis status is succeeded', () => {
|
||||
render({ variantAnalysisStatus: VariantAnalysisStatus.Succeeded });
|
||||
|
||||
expect(screen.getByText('Succeeded')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Succeeded warnings')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
9
extensions/ql-vscode/src/view/variant-analysis/index.tsx
Normal file
9
extensions/ql-vscode/src/view/variant-analysis/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { WebviewDefinition } from '../webview-definition';
|
||||
import { VariantAnalysis } from './VariantAnalysis';
|
||||
|
||||
const definition: WebviewDefinition = {
|
||||
component: <VariantAnalysis />
|
||||
};
|
||||
|
||||
export default definition;
|
||||
3
extensions/ql-vscode/src/view/webview-definition.ts
Normal file
3
extensions/ql-vscode/src/view/webview-definition.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type WebviewDefinition = {
|
||||
component: JSX.Element;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export type WebviewDefinition = {
|
||||
component: JSX.Element,
|
||||
loadedMessage: 'compareViewLoaded' | 'remoteQueryLoaded' | 'resultViewLoaded';
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { vscode } from './vscode-api';
|
||||
|
||||
import { WebviewDefinition } from './webview-interface';
|
||||
import { WebviewDefinition } from './webview-definition';
|
||||
|
||||
// Allow all views to use Codicons
|
||||
import '@vscode/codicons/dist/codicon.css';
|
||||
@@ -29,7 +29,7 @@ const render = () => {
|
||||
view.component,
|
||||
document.getElementById('root'),
|
||||
// Post a message to the extension when fully loaded.
|
||||
() => vscode.postMessage({ t: view.loadedMessage })
|
||||
() => vscode.postMessage({ t: 'viewLoaded', viewName })
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { tmpDir } from '../../helpers';
|
||||
import { getErrorMessage } from '../../pure/helpers-pure';
|
||||
import { HistoryItemLabelProvider } from '../../history-item-label-provider';
|
||||
import { RemoteQueriesManager } from '../../remote-queries/remote-queries-manager';
|
||||
import { InterfaceManager } from '../../interface';
|
||||
import { ResultsView } from '../../interface';
|
||||
import { EvalLogViewer } from '../../eval-log-viewer';
|
||||
import { QueryRunner } from '../../queryRunner';
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('query-history', () => {
|
||||
let queryHistoryManager: QueryHistoryManager | undefined;
|
||||
let doCompareCallback: sinon.SinonStub;
|
||||
|
||||
let localQueriesInterfaceManagerStub: InterfaceManager;
|
||||
let localQueriesResultsViewStub: ResultsView;
|
||||
let remoteQueriesManagerStub: RemoteQueriesManager;
|
||||
|
||||
let tryOpenExternalFile: Function;
|
||||
@@ -54,9 +54,9 @@ describe('query-history', () => {
|
||||
tryOpenExternalFile = (QueryHistoryManager.prototype as any).tryOpenExternalFile;
|
||||
configListener = new QueryHistoryConfigListener();
|
||||
doCompareCallback = sandbox.stub();
|
||||
localQueriesInterfaceManagerStub = {
|
||||
localQueriesResultsViewStub = {
|
||||
showResults: sandbox.stub()
|
||||
} as any as InterfaceManager;
|
||||
} as any as ResultsView;
|
||||
remoteQueriesManagerStub = {
|
||||
onRemoteQueryAdded: sandbox.stub(),
|
||||
onRemoteQueryRemoved: sandbox.stub(),
|
||||
@@ -204,7 +204,7 @@ describe('query-history', () => {
|
||||
|
||||
await queryHistoryManager.handleItemClicked(allHistory[0], [allHistory[0]]);
|
||||
|
||||
expect(localQueriesInterfaceManagerStub.showResults).to.have.been.calledOnceWith(allHistory[0]);
|
||||
expect(localQueriesResultsViewStub.showResults).to.have.been.calledOnceWith(allHistory[0]);
|
||||
expect(queryHistoryManager.treeDataProvider.getCurrent()).to.eq(allHistory[0]);
|
||||
});
|
||||
|
||||
@@ -213,7 +213,7 @@ describe('query-history', () => {
|
||||
|
||||
await queryHistoryManager.handleItemClicked(allHistory[0], [allHistory[0], allHistory[1]]);
|
||||
|
||||
expect(localQueriesInterfaceManagerStub.showResults).not.to.have.been.called;
|
||||
expect(localQueriesResultsViewStub.showResults).not.to.have.been.called;
|
||||
expect(queryHistoryManager.treeDataProvider.getCurrent()).to.be.undefined;
|
||||
});
|
||||
|
||||
@@ -222,7 +222,7 @@ describe('query-history', () => {
|
||||
|
||||
await queryHistoryManager.handleItemClicked(undefined!, []);
|
||||
|
||||
expect(localQueriesInterfaceManagerStub.showResults).not.to.have.been.called;
|
||||
expect(localQueriesResultsViewStub.showResults).not.to.have.been.called;
|
||||
expect(queryHistoryManager.treeDataProvider.getCurrent()).to.be.undefined;
|
||||
});
|
||||
});
|
||||
@@ -251,7 +251,7 @@ describe('query-history', () => {
|
||||
expect(queryHistoryManager.treeDataProvider.allHistory).not.to.contain(toDelete);
|
||||
|
||||
// the same item should be selected
|
||||
expect(localQueriesInterfaceManagerStub.showResults).to.have.been.calledOnceWith(selected);
|
||||
expect(localQueriesResultsViewStub.showResults).to.have.been.calledOnceWith(selected);
|
||||
});
|
||||
|
||||
it('should remove an item and select a new one', async () => {
|
||||
@@ -271,7 +271,7 @@ describe('query-history', () => {
|
||||
expect(queryHistoryManager.treeDataProvider.allHistory).not.to.contain(toDelete);
|
||||
|
||||
// the current item should have been selected
|
||||
expect(localQueriesInterfaceManagerStub.showResults).to.have.been.calledOnceWith(newSelected);
|
||||
expect(localQueriesResultsViewStub.showResults).to.have.been.calledOnceWith(newSelected);
|
||||
});
|
||||
|
||||
describe('Compare callback', () => {
|
||||
@@ -794,7 +794,7 @@ describe('query-history', () => {
|
||||
const qhm = new QueryHistoryManager(
|
||||
{} as QueryRunner,
|
||||
{} as DatabaseManager,
|
||||
localQueriesInterfaceManagerStub,
|
||||
localQueriesResultsViewStub,
|
||||
remoteQueriesManagerStub,
|
||||
{} as EvalLogViewer,
|
||||
'xxx',
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ExtensionContext } from 'vscode';
|
||||
import { createMockExtensionContext } from '../index';
|
||||
import { Credentials } from '../../../authentication';
|
||||
import { MarkdownFile } from '../../../remote-queries/remote-queries-markdown-generation';
|
||||
import * as actionsApiClient from '../../../remote-queries/gh-actions-api-client';
|
||||
import * as actionsApiClient from '../../../remote-queries/gh-api/gh-actions-api-client';
|
||||
import { exportResultsToGist } from '../../../remote-queries/export-results';
|
||||
|
||||
const proxyquire = pq.noPreserveCache();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { fail } from 'assert';
|
||||
import { expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { Credentials } from '../../../authentication';
|
||||
import { cancelRemoteQuery, getRepositoriesMetadata } from '../../../remote-queries/gh-actions-api-client';
|
||||
import { RemoteQuery } from '../../../remote-queries/remote-query';
|
||||
import { Credentials } from '../../../../authentication';
|
||||
import { cancelRemoteQuery, getRepositoriesMetadata } from '../../../../remote-queries/gh-api/gh-actions-api-client';
|
||||
import { RemoteQuery } from '../../../../remote-queries/remote-query';
|
||||
|
||||
describe('gh-actions-api-client mock responses', () => {
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
@@ -17,7 +17,7 @@ import { walkDirectory } from '../../../helpers';
|
||||
import { getErrorMessage } from '../../../pure/helpers-pure';
|
||||
import { HistoryItemLabelProvider } from '../../../history-item-label-provider';
|
||||
import { RemoteQueriesManager } from '../../../remote-queries/remote-queries-manager';
|
||||
import { InterfaceManager } from '../../../interface';
|
||||
import { ResultsView } from '../../../interface';
|
||||
import { EvalLogViewer } from '../../../eval-log-viewer';
|
||||
import { QueryRunner } from '../../../queryRunner';
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('Remote queries and query history manager', function() {
|
||||
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let qhm: QueryHistoryManager;
|
||||
let localQueriesInterfaceManagerStub: InterfaceManager;
|
||||
let localQueriesResultsViewStub: ResultsView;
|
||||
let remoteQueriesManagerStub: RemoteQueriesManager;
|
||||
let rawQueryHistory: any;
|
||||
let remoteQueryResult0: RemoteQueryResult;
|
||||
@@ -57,9 +57,9 @@ describe('Remote queries and query history manager', function() {
|
||||
|
||||
sandbox = sinon.createSandbox();
|
||||
|
||||
localQueriesInterfaceManagerStub = {
|
||||
localQueriesResultsViewStub = {
|
||||
showResults: sandbox.stub()
|
||||
} as any as InterfaceManager;
|
||||
} as any as ResultsView;
|
||||
|
||||
rehydrateRemoteQueryStub = sandbox.stub();
|
||||
removeRemoteQueryStub = sandbox.stub();
|
||||
@@ -92,7 +92,7 @@ describe('Remote queries and query history manager', function() {
|
||||
qhm = new QueryHistoryManager(
|
||||
{} as QueryRunner,
|
||||
{} as DatabaseManager,
|
||||
localQueriesInterfaceManagerStub,
|
||||
localQueriesResultsViewStub,
|
||||
remoteQueriesManagerStub,
|
||||
{} as EvalLogViewer,
|
||||
STORAGE_DIR,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user