Merge remote-tracking branch 'origin/main' into koesie10/refactor-common-components
This commit is contained in:
5
.vscode/launch.json
vendored
5
.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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
## 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)
|
||||
- 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
|
||||
|
||||
@@ -20,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.
|
||||
|
||||
|
||||
4
extensions/ql-vscode/package-lock.json
generated
4
extensions/ql-vscode/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "vscode-codeql",
|
||||
"version": "1.6.13",
|
||||
"version": "1.7.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vscode-codeql",
|
||||
"version": "1.6.13",
|
||||
"version": "1.7.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/plugin-retry": "^3.0.9",
|
||||
|
||||
@@ -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",
|
||||
@@ -1192,7 +1192,7 @@
|
||||
"watch:extension": "tsc --watch",
|
||||
"watch:webpack": "gulp watchView",
|
||||
"test": "npm-run-all -p test:*",
|
||||
"test:unit": "mocha --exit -r ts-node/register test/pure-tests/**/*.ts",
|
||||
"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",
|
||||
|
||||
@@ -915,8 +915,8 @@ 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);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -422,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) {
|
||||
|
||||
@@ -40,7 +40,7 @@ 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 { ResultsView } from './interface';
|
||||
@@ -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))) {
|
||||
|
||||
@@ -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 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 { unzipFile } from '../pure/zip';
|
||||
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';
|
||||
|
||||
@@ -13,7 +13,7 @@ import { runRemoteQuery } from './run-remote-query';
|
||||
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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ export class RemoteQueriesView extends AbstractWebview<ToRemoteQueriesMessage, F
|
||||
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,
|
||||
}
|
||||
@@ -1,5 +1,74 @@
|
||||
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',
|
||||
}
|
||||
|
||||
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
|
||||
}>
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -47,6 +47,12 @@ export default {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
onViewLogsClick: {
|
||||
action: 'view-logs-clicked',
|
||||
table: {
|
||||
disable: true,
|
||||
}
|
||||
},
|
||||
}
|
||||
} as ComponentMeta<typeof VariantAnalysisHeader>;
|
||||
|
||||
@@ -59,16 +65,25 @@ InProgress.args = {
|
||||
queryName: 'Query name',
|
||||
queryFileName: 'example.ql',
|
||||
variantAnalysisStatus: VariantAnalysisStatus.InProgress,
|
||||
totalRepositoryCount: 10,
|
||||
completedRepositoryCount: 2,
|
||||
resultCount: 99_999,
|
||||
};
|
||||
|
||||
export const Succeeded = Template.bind({});
|
||||
Succeeded.args = {
|
||||
...InProgress.args,
|
||||
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
|
||||
totalRepositoryCount: 1000,
|
||||
completedRepositoryCount: 1000,
|
||||
duration: 720_000,
|
||||
completedAt: new Date(1661263446000),
|
||||
};
|
||||
|
||||
export const Failed = Template.bind({});
|
||||
Failed.args = {
|
||||
...InProgress.args,
|
||||
variantAnalysisStatus: 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,
|
||||
queryResult: 'warning',
|
||||
};
|
||||
|
||||
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,
|
||||
queryResult: 'warning',
|
||||
};
|
||||
|
||||
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,
|
||||
queryResult: 'stopped',
|
||||
};
|
||||
@@ -194,7 +194,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}`);
|
||||
|
||||
@@ -5,9 +5,10 @@ import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTag } from '@vscode/web
|
||||
|
||||
import { Overlay } from '@primer/react';
|
||||
|
||||
import { AnalysisMessage, CodeFlow, ResultSeverity, ThreadFlow } from '../../remote-queries/shared/analysis-result';
|
||||
import { SectionTitle, VerticalSpace } from '../common';
|
||||
import { FileCodeSnippet } from './FileCodeSnippet';
|
||||
import { CodeFlow, AnalysisMessage, ResultSeverity, ThreadFlow } from '../../../remote-queries/shared/analysis-result';
|
||||
import { SectionTitle } from '../SectionTitle';
|
||||
import { VerticalSpace } from '../VerticalSpace';
|
||||
import { FileCodeSnippet } from '../FileCodeSnippet';
|
||||
|
||||
const StyledCloseButton = styled.button`
|
||||
position: absolute;
|
||||
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';
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
FileLink,
|
||||
HighlightedRegion,
|
||||
ResultSeverity
|
||||
} from '../../remote-queries/shared/analysis-result';
|
||||
import { createRemoteFileRef } from '../../pure/location-link-utils';
|
||||
import { parseHighlightedLine, shouldHighlightLine } from '../../pure/sarif-utils';
|
||||
import { VerticalSpace } from '../common';
|
||||
} from '../../../remote-queries/shared/analysis-result';
|
||||
import { createRemoteFileRef } from '../../../pure/location-link-utils';
|
||||
import { parseHighlightedLine, shouldHighlightLine } from '../../../pure/sarif-utils';
|
||||
import { VerticalSpace } from '../VerticalSpace';
|
||||
|
||||
const borderColor = 'var(--vscode-editor-snippetFinalTabstopHighlightBorder)';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './FileCodeSnippet';
|
||||
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';
|
||||
@@ -1,3 +1,6 @@
|
||||
export * from './icon';
|
||||
export * from './CodePaths';
|
||||
export * from './FileCodeSnippet';
|
||||
export * from './HorizontalSpace';
|
||||
export * from './SectionTitle';
|
||||
export * from './VerticalSpace';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -270,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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -362,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
|
||||
|
||||
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>
|
||||
);
|
||||
@@ -9,12 +9,14 @@ export function VariantAnalysis(): JSX.Element {
|
||||
<VariantAnalysisHeader
|
||||
queryName="Example query"
|
||||
queryFileName="example.ql"
|
||||
totalRepositoryCount={10}
|
||||
variantAnalysisStatus={VariantAnalysisStatus.InProgress}
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -3,12 +3,22 @@ import styled from 'styled-components';
|
||||
import { VariantAnalysisStatus } from '../../remote-queries/shared/variant-analysis';
|
||||
import { QueryDetails } from './QueryDetails';
|
||||
import { VariantAnalysisActions } from './VariantAnalysisActions';
|
||||
import { VariantAnalysisStats } from './VariantAnalysisStats';
|
||||
|
||||
export type VariantAnalysisHeaderProps = {
|
||||
queryName: string;
|
||||
queryFileName: string;
|
||||
variantAnalysisStatus: VariantAnalysisStatus;
|
||||
|
||||
totalRepositoryCount: number;
|
||||
completedRepositoryCount?: number | undefined;
|
||||
|
||||
queryResult?: 'warning' | 'stopped';
|
||||
|
||||
resultCount?: number | undefined;
|
||||
duration?: number | undefined;
|
||||
completedAt?: Date | undefined;
|
||||
|
||||
onOpenQueryFileClick: () => void;
|
||||
onViewQueryTextClick: () => void;
|
||||
|
||||
@@ -16,9 +26,17 @@ export type VariantAnalysisHeaderProps = {
|
||||
|
||||
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;
|
||||
`;
|
||||
@@ -26,26 +44,45 @@ const Container = styled.div`
|
||||
export const VariantAnalysisHeader = ({
|
||||
queryName,
|
||||
queryFileName,
|
||||
totalRepositoryCount,
|
||||
completedRepositoryCount,
|
||||
queryResult,
|
||||
resultCount,
|
||||
duration,
|
||||
completedAt,
|
||||
variantAnalysisStatus,
|
||||
onOpenQueryFileClick,
|
||||
onViewQueryTextClick,
|
||||
onStopQueryClick,
|
||||
onCopyRepositoryListClick,
|
||||
onExportResultsClick
|
||||
onExportResultsClick,
|
||||
onViewLogsClick,
|
||||
}: VariantAnalysisHeaderProps) => {
|
||||
return (
|
||||
<Container>
|
||||
<QueryDetails
|
||||
queryName={queryName}
|
||||
queryFileName={queryFileName}
|
||||
onOpenQueryFileClick={onOpenQueryFileClick}
|
||||
onViewQueryTextClick={onViewQueryTextClick}
|
||||
/>
|
||||
<VariantAnalysisActions
|
||||
<Row>
|
||||
<QueryDetails
|
||||
queryName={queryName}
|
||||
queryFileName={queryFileName}
|
||||
onOpenQueryFileClick={onOpenQueryFileClick}
|
||||
onViewQueryTextClick={onViewQueryTextClick}
|
||||
/>
|
||||
<VariantAnalysisActions
|
||||
variantAnalysisStatus={variantAnalysisStatus}
|
||||
onStopQueryClick={onStopQueryClick}
|
||||
onCopyRepositoryListClick={onCopyRepositoryListClick}
|
||||
onExportResultsClick={onExportResultsClick}
|
||||
/>
|
||||
</Row>
|
||||
<VariantAnalysisStats
|
||||
variantAnalysisStatus={variantAnalysisStatus}
|
||||
onStopQueryClick={onStopQueryClick}
|
||||
onCopyRepositoryListClick={onCopyRepositoryListClick}
|
||||
onExportResultsClick={onExportResultsClick}
|
||||
totalRepositoryCount={totalRepositoryCount}
|
||||
completedRepositoryCount={completedRepositoryCount}
|
||||
queryResult={queryResult}
|
||||
resultCount={resultCount}
|
||||
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;
|
||||
|
||||
queryResult?: 'warning' | 'stopped';
|
||||
};
|
||||
|
||||
export const VariantAnalysisRepositoriesStats = ({
|
||||
variantAnalysisStatus,
|
||||
totalRepositoryCount,
|
||||
completedRepositoryCount = 0,
|
||||
queryResult,
|
||||
}: Props) => {
|
||||
if (variantAnalysisStatus === VariantAnalysisStatus.Failed) {
|
||||
return (
|
||||
<>
|
||||
0<HorizontalSpace size={2} /><ErrorIcon />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{formatDecimal(completedRepositoryCount)}/{formatDecimal(totalRepositoryCount)}
|
||||
{queryResult && <><HorizontalSpace size={2} /><WarningIcon /></>}
|
||||
{!queryResult && 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;
|
||||
|
||||
queryResult?: 'warning' | 'stopped';
|
||||
|
||||
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,
|
||||
queryResult,
|
||||
resultCount,
|
||||
duration,
|
||||
completedAt,
|
||||
onViewLogsClick,
|
||||
}: VariantAnalysisStatsProps) => {
|
||||
const completionHeaderName = useMemo(() => {
|
||||
if (variantAnalysisStatus === VariantAnalysisStatus.InProgress) {
|
||||
return 'Running';
|
||||
}
|
||||
|
||||
if (variantAnalysisStatus === VariantAnalysisStatus.Failed) {
|
||||
return 'Failed';
|
||||
}
|
||||
|
||||
if (queryResult === 'warning') {
|
||||
return 'Succeeded warnings';
|
||||
}
|
||||
|
||||
if (queryResult === 'stopped') {
|
||||
return 'Stopped';
|
||||
}
|
||||
|
||||
return 'Succeeded';
|
||||
}, [variantAnalysisStatus, queryResult]);
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<StatItem title="Results">
|
||||
{resultCount !== undefined ? formatDecimal(resultCount) : '-'}
|
||||
</StatItem>
|
||||
<StatItem title="Repositories">
|
||||
<VariantAnalysisRepositoriesStats
|
||||
variantAnalysisStatus={variantAnalysisStatus}
|
||||
totalRepositoryCount={totalRepositoryCount}
|
||||
completedRepositoryCount={completedRepositoryCount}
|
||||
queryResult={queryResult}
|
||||
/>
|
||||
</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,80 @@
|
||||
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 the query result is a warning', () => {
|
||||
render({ queryResult: 'warning' });
|
||||
|
||||
expect(screen.getByRole('img', {
|
||||
name: 'Warning',
|
||||
})).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a warning icon when the query result is stopped', () => {
|
||||
render({ queryResult: 'stopped' });
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
2
extensions/ql-vscode/test/mocha.setup.js
Normal file
2
extensions/ql-vscode/test/mocha.setup.js
Normal file
@@ -0,0 +1,2 @@
|
||||
process.env.TZ = 'UTC';
|
||||
process.env.LANG = 'en-US';
|
||||
11
extensions/ql-vscode/test/pure-tests/date.test.ts
Normal file
11
extensions/ql-vscode/test/pure-tests/date.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { expect } from 'chai';
|
||||
import 'mocha';
|
||||
|
||||
import { formatDate } from '../../src/pure/date';
|
||||
|
||||
describe('Date', () => {
|
||||
it('should return a formatted date', () => {
|
||||
expect(formatDate(new Date(1663326904000))).to.eq('Sep 16, 11:15 AM');
|
||||
expect(formatDate(new Date(1631783704000))).to.eq('Sep 16, 2021, 9:15 AM');
|
||||
});
|
||||
});
|
||||
12
extensions/ql-vscode/test/pure-tests/number.test.ts
Normal file
12
extensions/ql-vscode/test/pure-tests/number.test.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { expect } from 'chai';
|
||||
import 'mocha';
|
||||
|
||||
import { formatDecimal } from '../../src/pure/number';
|
||||
|
||||
describe('Number', () => {
|
||||
it('should return a formatted decimal', () => {
|
||||
expect(formatDecimal(9)).to.eq('9');
|
||||
expect(formatDecimal(10_000)).to.eq('10,000');
|
||||
expect(formatDecimal(100_000_000_000)).to.eq('100,000,000,000');
|
||||
});
|
||||
});
|
||||
@@ -5,10 +5,13 @@ import { humanizeRelativeTime, humanizeUnit } from '../../src/pure/time';
|
||||
|
||||
describe('Time', () => {
|
||||
it('should return a humanized unit', () => {
|
||||
expect(humanizeUnit(undefined)).to.eq('Less than a minute');
|
||||
expect(humanizeUnit(0)).to.eq('Less than a minute');
|
||||
expect(humanizeUnit(-1)).to.eq('Less than a minute');
|
||||
expect(humanizeUnit(1000 * 60 - 1)).to.eq('Less than a minute');
|
||||
expect(humanizeUnit(undefined)).to.eq('Less than a second');
|
||||
expect(humanizeUnit(0)).to.eq('Less than a second');
|
||||
expect(humanizeUnit(-1)).to.eq('Less than a second');
|
||||
expect(humanizeUnit(1000 - 1)).to.eq('Less than a second');
|
||||
expect(humanizeUnit(1000)).to.eq('1 second');
|
||||
expect(humanizeUnit(1000 * 2)).to.eq('2 seconds');
|
||||
expect(humanizeUnit(1000 * 60 - 1)).to.eq('59 seconds');
|
||||
expect(humanizeUnit(1000 * 60)).to.eq('1 minute');
|
||||
expect(humanizeUnit(1000 * 60 * 2 - 1)).to.eq('1 minute');
|
||||
expect(humanizeUnit(1000 * 60 * 2)).to.eq('2 minutes');
|
||||
|
||||
Reference in New Issue
Block a user