Merge remote-tracking branch 'origin/main' into koesie10/refactor-common-components

This commit is contained in:
Koen Vlaswinkel
2022-09-22 10:51:30 +02:00
49 changed files with 736 additions and 71 deletions

5
.vscode/launch.json vendored
View File

@@ -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,

View File

@@ -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.

View File

@@ -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.

View File

@@ -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",

View File

@@ -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",

View File

@@ -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);
})
);

View 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);
}

View File

@@ -422,6 +422,7 @@ export interface RemoteQueryDownloadAllAnalysesResultsMessage {
export interface RemoteQueryExportResultsMessage {
t: 'remoteQueryExportResults';
queryId: string;
}
export interface CopyRepoListMessage {

View 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);
}

View File

@@ -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) {

View File

@@ -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))) {

View File

@@ -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';

View File

@@ -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)',

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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);

View File

@@ -0,0 +1,5 @@
export interface Repository {
id: number,
fullName: string,
private: boolean,
}

View File

@@ -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
}>
}

View File

@@ -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 {

View File

@@ -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',

View File

@@ -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),
};

View File

@@ -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',
};

View File

@@ -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}`);

View File

@@ -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;

View File

@@ -0,0 +1 @@
export * from './CodePaths';

View File

@@ -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)';

View File

@@ -0,0 +1 @@
export * from './FileCodeSnippet';

View 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)} />;

View 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} />;

View 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} />;

View 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} />;

View File

@@ -0,0 +1,4 @@
export * from './Codicon';
export * from './ErrorIcon';
export * from './SuccessIcon';
export * from './WarningIcon';

View File

@@ -1,3 +1,6 @@
export * from './icon';
export * from './CodePaths';
export * from './FileCodeSnippet';
export * from './HorizontalSpace';
export * from './SectionTitle';
export * from './VerticalSpace';

View File

@@ -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;

View File

@@ -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

View 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>
);

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -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" /></>}
</>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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);
});
});

View File

@@ -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();

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
process.env.TZ = 'UTC';
process.env.LANG = 'en-US';

View 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');
});
});

View 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');
});
});

View File

@@ -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');