Add variant analysis stats component

This commit is contained in:
Koen Vlaswinkel
2022-09-16 12:20:45 +02:00
parent 3079d7f285
commit ba0a30dcfe
7 changed files with 309 additions and 6 deletions

View File

@@ -0,0 +1,9 @@
/*
* Contains an assortment of helper constants and functions for working with numbers.
*/
const numberFormatter = new Intl.NumberFormat('en');
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

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

@@ -0,0 +1,30 @@
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;
`;
const Content = styled.div`
`;
export const StatItem = ({ title, children }: Props) => (
<Container>
<Header>{title}</Header>
<Content>{children}</Content>
</Container>
);

View File

@@ -0,0 +1,34 @@
import * as React from 'react';
import styled from 'styled-components';
import { VSCodeLink } from '@vscode/webview-ui-toolkit/react';
type Props = {
completedAt?: Date | undefined;
onViewLogsClick: () => void;
};
const Icon = styled.span`
font-size: 1em !important;
vertical-align: text-bottom;
`;
const ViewLogsLink = styled(VSCodeLink)`
margin-top: 0.2em;
`;
export const VariantAnalysisCompletionStats = ({
completedAt,
onViewLogsClick,
}: Props) => {
if (completedAt === undefined) {
return <Icon className="codicon codicon-loading codicon-modifier-spin" />;
}
return (
<>
{completedAt.toLocaleString()}
<ViewLogsLink onClick={onViewLogsClick}>View logs</ViewLogsLink>
</>
);
};

View File

@@ -0,0 +1,57 @@
import * as React from 'react';
import styled from 'styled-components';
import { VariantAnalysisStatus } from '../../remote-queries/shared/variant-analysis';
import { formatDecimal } from '../../pure/number';
type Props = {
variantAnalysisStatus: VariantAnalysisStatus;
totalRepositoryCount: number;
completedRepositoryCount?: number | undefined;
queryResult?: 'warning' | 'stopped';
completedAt?: Date | undefined;
};
const Icon = styled.span`
vertical-align: text-bottom;
margin-left: 0.3em;
`;
const WarningIcon = styled(Icon)`
color: var(--vscode-problemsWarningIcon-foreground);
`;
const ErrorIcon = styled(Icon)`
color: var(--vscode-problemsErrorIcon-foreground);
`;
const SuccessIcon = styled(Icon)`
color: var(--vscode-testing-iconPassed);
`;
export const VariantAnalysisRepositoriesStats = ({
variantAnalysisStatus,
totalRepositoryCount,
completedRepositoryCount = 0,
queryResult,
completedAt,
}: Props) => {
if (variantAnalysisStatus === VariantAnalysisStatus.Failed) {
return (
<>
0<ErrorIcon className="codicon codicon-error" />
</>
);
}
return (
<>
{formatDecimal(completedRepositoryCount)}/{formatDecimal(totalRepositoryCount)}
{queryResult && <WarningIcon className="codicon codicon-warning" />}
{completedAt && !queryResult && variantAnalysisStatus === VariantAnalysisStatus.Succeeded &&
<SuccessIcon className="codicon codicon-pass" />}
</>
);
};

View File

@@ -0,0 +1,87 @@
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 { VariantAnalysisCompletionStats } from './VariantAnalysisCompletionStats';
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}
completedAt={completedAt}
/>
</StatItem>
<StatItem title="Duration">
{duration !== undefined ? humanizeUnit(duration) : '-'}
</StatItem>
<StatItem title={completionHeaderName}>
<VariantAnalysisCompletionStats
completedAt={completedAt}
onViewLogsClick={onViewLogsClick}
/>
</StatItem>
</Row>
);
};