Skip to content

Commit ba0a30d

Browse files
committed
Add variant analysis stats component
1 parent 3079d7f commit ba0a30d

7 files changed

Lines changed: 309 additions & 6 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Contains an assortment of helper constants and functions for working with numbers.
3+
*/
4+
5+
const numberFormatter = new Intl.NumberFormat('en');
6+
7+
export function formatDecimal(value: number): string {
8+
return numberFormatter.format(value);
9+
}

extensions/ql-vscode/src/pure/time.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
* Contains an assortment of helper constants and functions for working with time, dates, and durations.
33
*/
44

5-
export const ONE_MINUTE_IN_MS = 1000 * 60;
5+
export const ONE_SECOND_IN_MS = 1000;
6+
export const ONE_MINUTE_IN_MS = ONE_SECOND_IN_MS * 60;
67
export const ONE_HOUR_IN_MS = ONE_MINUTE_IN_MS * 60;
78
export const TWO_HOURS_IN_MS = ONE_HOUR_IN_MS * 2;
89
export const THREE_HOURS_IN_MS = ONE_HOUR_IN_MS * 3;
@@ -43,20 +44,23 @@ export function humanizeRelativeTime(relativeTimeMillis?: number) {
4344

4445
/**
4546
* Converts a number of milliseconds into a human-readable string with units, indicating an amount of time.
46-
* Negative numbers have no meaning and are considered to be "Less than a minute".
47+
* Negative numbers have no meaning and are considered to be "Less than a second".
4748
*
4849
* @param millis The number of milliseconds to convert.
49-
* @returns A humanized duration. For example, "2 minutes", "2 hours", "2 days", or "2 months".
50+
* @returns A humanized duration. For example, "2 seconds", "2 minutes", "2 hours", "2 days", or "2 months".
5051
*/
5152
export function humanizeUnit(millis?: number): string {
5253
// assume a blank or empty string is a zero
5354
// assume anything less than 0 is a zero
54-
if (!millis || millis < ONE_MINUTE_IN_MS) {
55-
return 'Less than a minute';
55+
if (!millis || millis < ONE_SECOND_IN_MS) {
56+
return 'Less than a second';
5657
}
5758
let unit: string;
5859
let unitDiff: number;
59-
if (millis < ONE_HOUR_IN_MS) {
60+
if (millis < ONE_MINUTE_IN_MS) {
61+
unit = 'second';
62+
unitDiff = Math.floor(millis / ONE_SECOND_IN_MS);
63+
} else if (millis < ONE_HOUR_IN_MS) {
6064
unit = 'minute';
6165
unitDiff = Math.floor(millis / ONE_MINUTE_IN_MS);
6266
} else if (millis < ONE_DAY_IN_MS) {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from 'react';
2+
3+
import { ComponentStory, ComponentMeta } from '@storybook/react';
4+
5+
import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer';
6+
import { VariantAnalysisStats } from '../../view/variant-analysis/VariantAnalysisStats';
7+
import { VariantAnalysisStatus } from '../../remote-queries/shared/variant-analysis';
8+
9+
export default {
10+
title: 'Variant Analysis/Variant Analysis Stats',
11+
component: VariantAnalysisStats,
12+
decorators: [
13+
(Story) => (
14+
<VariantAnalysisContainer>
15+
<Story />
16+
</VariantAnalysisContainer>
17+
)
18+
],
19+
argTypes: {
20+
onViewLogsClick: {
21+
action: 'view-logs-clicked',
22+
table: {
23+
disable: true,
24+
},
25+
},
26+
}
27+
} as ComponentMeta<typeof VariantAnalysisStats>;
28+
29+
const Template: ComponentStory<typeof VariantAnalysisStats> = (args) => (
30+
<VariantAnalysisStats {...args} />
31+
);
32+
33+
export const Starting = Template.bind({});
34+
Starting.args = {
35+
variantAnalysisStatus: VariantAnalysisStatus.InProgress,
36+
totalRepositoryCount: 10,
37+
};
38+
39+
export const Started = Template.bind({});
40+
Started.args = {
41+
...Starting.args,
42+
resultCount: 99_999,
43+
completedRepositoryCount: 2,
44+
};
45+
46+
export const StartedWithWarnings = Template.bind({});
47+
StartedWithWarnings.args = {
48+
...Starting.args,
49+
queryResult: 'warning',
50+
};
51+
52+
export const Succeeded = Template.bind({});
53+
Succeeded.args = {
54+
...Started.args,
55+
totalRepositoryCount: 1000,
56+
completedRepositoryCount: 1000,
57+
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
58+
duration: 720_000,
59+
completedAt: new Date(1661263446000),
60+
};
61+
62+
export const SucceededWithWarnings = Template.bind({});
63+
SucceededWithWarnings.args = {
64+
...Succeeded.args,
65+
totalRepositoryCount: 10,
66+
completedRepositoryCount: 2,
67+
queryResult: 'warning',
68+
};
69+
70+
export const Failed = Template.bind({});
71+
Failed.args = {
72+
...Starting.args,
73+
variantAnalysisStatus: VariantAnalysisStatus.Failed,
74+
duration: 10_000,
75+
completedAt: new Date(1661263446000),
76+
};
77+
78+
export const Stopped = Template.bind({});
79+
Stopped.args = {
80+
...SucceededWithWarnings.args,
81+
queryResult: 'stopped',
82+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from 'react';
2+
import type { ReactNode } from 'react';
3+
import styled from 'styled-components';
4+
5+
type Props = {
6+
title: ReactNode;
7+
children: ReactNode;
8+
};
9+
10+
const Container = styled.div`
11+
flex: 1;
12+
`;
13+
14+
const Header = styled.div`
15+
color: var(--vscode-badge-foreground);
16+
font-size: 0.85em;
17+
font-weight: 800;
18+
text-transform: uppercase;
19+
margin-bottom: 0.6em;
20+
`;
21+
22+
const Content = styled.div`
23+
`;
24+
25+
export const StatItem = ({ title, children }: Props) => (
26+
<Container>
27+
<Header>{title}</Header>
28+
<Content>{children}</Content>
29+
</Container>
30+
);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as React from 'react';
2+
import styled from 'styled-components';
3+
import { VSCodeLink } from '@vscode/webview-ui-toolkit/react';
4+
5+
type Props = {
6+
completedAt?: Date | undefined;
7+
8+
onViewLogsClick: () => void;
9+
};
10+
11+
const Icon = styled.span`
12+
font-size: 1em !important;
13+
vertical-align: text-bottom;
14+
`;
15+
16+
const ViewLogsLink = styled(VSCodeLink)`
17+
margin-top: 0.2em;
18+
`;
19+
20+
export const VariantAnalysisCompletionStats = ({
21+
completedAt,
22+
onViewLogsClick,
23+
}: Props) => {
24+
if (completedAt === undefined) {
25+
return <Icon className="codicon codicon-loading codicon-modifier-spin" />;
26+
}
27+
28+
return (
29+
<>
30+
{completedAt.toLocaleString()}
31+
<ViewLogsLink onClick={onViewLogsClick}>View logs</ViewLogsLink>
32+
</>
33+
);
34+
};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as React from 'react';
2+
import styled from 'styled-components';
3+
import { VariantAnalysisStatus } from '../../remote-queries/shared/variant-analysis';
4+
import { formatDecimal } from '../../pure/number';
5+
6+
type Props = {
7+
variantAnalysisStatus: VariantAnalysisStatus;
8+
9+
totalRepositoryCount: number;
10+
completedRepositoryCount?: number | undefined;
11+
12+
queryResult?: 'warning' | 'stopped';
13+
14+
completedAt?: Date | undefined;
15+
};
16+
17+
const Icon = styled.span`
18+
vertical-align: text-bottom;
19+
margin-left: 0.3em;
20+
`;
21+
22+
const WarningIcon = styled(Icon)`
23+
color: var(--vscode-problemsWarningIcon-foreground);
24+
`;
25+
26+
const ErrorIcon = styled(Icon)`
27+
color: var(--vscode-problemsErrorIcon-foreground);
28+
`;
29+
30+
const SuccessIcon = styled(Icon)`
31+
color: var(--vscode-testing-iconPassed);
32+
`;
33+
34+
export const VariantAnalysisRepositoriesStats = ({
35+
variantAnalysisStatus,
36+
totalRepositoryCount,
37+
completedRepositoryCount = 0,
38+
queryResult,
39+
completedAt,
40+
}: Props) => {
41+
if (variantAnalysisStatus === VariantAnalysisStatus.Failed) {
42+
return (
43+
<>
44+
0<ErrorIcon className="codicon codicon-error" />
45+
</>
46+
);
47+
}
48+
49+
return (
50+
<>
51+
{formatDecimal(completedRepositoryCount)}/{formatDecimal(totalRepositoryCount)}
52+
{queryResult && <WarningIcon className="codicon codicon-warning" />}
53+
{completedAt && !queryResult && variantAnalysisStatus === VariantAnalysisStatus.Succeeded &&
54+
<SuccessIcon className="codicon codicon-pass" />}
55+
</>
56+
);
57+
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import * as React from 'react';
2+
import { useMemo } from 'react';
3+
import styled from 'styled-components';
4+
import { VariantAnalysisStatus } from '../../remote-queries/shared/variant-analysis';
5+
import { StatItem } from './StatItem';
6+
import { formatDecimal } from '../../pure/number';
7+
import { humanizeUnit } from '../../pure/time';
8+
import { VariantAnalysisRepositoriesStats } from './VariantAnalysisRepositoriesStats';
9+
import { VariantAnalysisCompletionStats } from './VariantAnalysisCompletionStats';
10+
11+
export type VariantAnalysisStatsProps = {
12+
variantAnalysisStatus: VariantAnalysisStatus;
13+
14+
totalRepositoryCount: number;
15+
completedRepositoryCount?: number | undefined;
16+
17+
queryResult?: 'warning' | 'stopped';
18+
19+
resultCount?: number | undefined;
20+
duration?: number | undefined;
21+
completedAt?: Date | undefined;
22+
23+
onViewLogsClick: () => void;
24+
};
25+
26+
const Row = styled.div`
27+
display: flex;
28+
width: 100%;
29+
gap: 1em;
30+
`;
31+
32+
export const VariantAnalysisStats = ({
33+
variantAnalysisStatus,
34+
totalRepositoryCount,
35+
completedRepositoryCount = 0,
36+
queryResult,
37+
resultCount,
38+
duration,
39+
completedAt,
40+
onViewLogsClick,
41+
}: VariantAnalysisStatsProps) => {
42+
const completionHeaderName = useMemo(() => {
43+
if (variantAnalysisStatus === VariantAnalysisStatus.InProgress) {
44+
return 'Running';
45+
}
46+
47+
if (variantAnalysisStatus === VariantAnalysisStatus.Failed) {
48+
return 'Failed';
49+
}
50+
51+
if (queryResult === 'warning') {
52+
return 'Succeeded warnings';
53+
}
54+
55+
if (queryResult === 'stopped') {
56+
return 'Stopped';
57+
}
58+
59+
return 'Succeeded';
60+
}, [variantAnalysisStatus, queryResult]);
61+
62+
return (
63+
<Row>
64+
<StatItem title="Results">
65+
{resultCount !== undefined ? formatDecimal(resultCount) : '-'}
66+
</StatItem>
67+
<StatItem title="Repositories">
68+
<VariantAnalysisRepositoriesStats
69+
variantAnalysisStatus={variantAnalysisStatus}
70+
totalRepositoryCount={totalRepositoryCount}
71+
completedRepositoryCount={completedRepositoryCount}
72+
queryResult={queryResult}
73+
completedAt={completedAt}
74+
/>
75+
</StatItem>
76+
<StatItem title="Duration">
77+
{duration !== undefined ? humanizeUnit(duration) : '-'}
78+
</StatItem>
79+
<StatItem title={completionHeaderName}>
80+
<VariantAnalysisCompletionStats
81+
completedAt={completedAt}
82+
onViewLogsClick={onViewLogsClick}
83+
/>
84+
</StatItem>
85+
</Row>
86+
);
87+
};

0 commit comments

Comments
 (0)