Add variant analysis stats component
This commit is contained in:
9
extensions/ql-vscode/src/pure/number.ts
Normal file
9
extensions/ql-vscode/src/pure/number.ts
Normal 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);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
30
extensions/ql-vscode/src/view/variant-analysis/StatItem.tsx
Normal file
30
extensions/ql-vscode/src/view/variant-analysis/StatItem.tsx
Normal 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>
|
||||
);
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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" />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user