Merge pull request #1517 from github/koesie10/variant-analysis-stats
Add variant analysis stats component
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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
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);
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
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,4 @@
|
||||
export * from './icon';
|
||||
export * from './HorizontalSpace';
|
||||
export * from './SectionTitle';
|
||||
export * from './VerticalSpace';
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
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