Skip to content

Commit be62bd3

Browse files
committed
Add metadata to repository row
This will add the star count and last updated fields to the repository row. We are able to re-use some components from remote queries, but we cannot re-use `LastUpdated` since it requires a numeric duration, while we are dealing with an ISO8601 date.
1 parent fcb1ef4 commit be62bd3

File tree

12 files changed

+193
-15
lines changed

12 files changed

+193
-15
lines changed

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,19 @@ export function humanizeRelativeTime(relativeTimeMillis?: number) {
2929
return '';
3030
}
3131

32+
// If the time is in the past, we need -3_600_035 to be formatted as "1 hour ago" instead of "2 hours ago"
33+
const round = relativeTimeMillis < 0 ? Math.ceil : Math.floor;
34+
3235
if (Math.abs(relativeTimeMillis) < ONE_HOUR_IN_MS) {
33-
return durationFormatter.format(Math.floor(relativeTimeMillis / ONE_MINUTE_IN_MS), 'minute');
36+
return durationFormatter.format(round(relativeTimeMillis / ONE_MINUTE_IN_MS), 'minute');
3437
} else if (Math.abs(relativeTimeMillis) < ONE_DAY_IN_MS) {
35-
return durationFormatter.format(Math.floor(relativeTimeMillis / ONE_HOUR_IN_MS), 'hour');
38+
return durationFormatter.format(round(relativeTimeMillis / ONE_HOUR_IN_MS), 'hour');
3639
} else if (Math.abs(relativeTimeMillis) < ONE_MONTH_IN_MS) {
37-
return durationFormatter.format(Math.floor(relativeTimeMillis / ONE_DAY_IN_MS), 'day');
40+
return durationFormatter.format(round(relativeTimeMillis / ONE_DAY_IN_MS), 'day');
3841
} else if (Math.abs(relativeTimeMillis) < ONE_YEAR_IN_MS) {
39-
return durationFormatter.format(Math.floor(relativeTimeMillis / ONE_MONTH_IN_MS), 'month');
42+
return durationFormatter.format(round(relativeTimeMillis / ONE_MONTH_IN_MS), 'month');
4043
} else {
41-
return durationFormatter.format(Math.floor(relativeTimeMillis / ONE_YEAR_IN_MS), 'year');
44+
return durationFormatter.format(round(relativeTimeMillis / ONE_YEAR_IN_MS), 'year');
4245
}
4346
}
4447

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from 'react';
2+
3+
import { ComponentStory, ComponentMeta } from '@storybook/react';
4+
5+
import { LastUpdated as LastUpdatedComponent } from '../../view/common/LastUpdated';
6+
7+
export default {
8+
title: 'Last Updated',
9+
component: LastUpdatedComponent,
10+
} as ComponentMeta<typeof LastUpdatedComponent>;
11+
12+
const Template: ComponentStory<typeof LastUpdatedComponent> = (args) => (
13+
<LastUpdatedComponent {...args} />
14+
);
15+
16+
export const LastUpdated = Template.bind({});
17+
18+
LastUpdated.args = {
19+
lastUpdated: new Date(Date.now() - 3_600_000).toISOString(), // 1 hour ago
20+
};

extensions/ql-vscode/src/stories/remote-queries/StarCount.stories.tsx renamed to extensions/ql-vscode/src/stories/common/StarCount.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22

33
import { ComponentStory, ComponentMeta } from '@storybook/react';
44

5-
import StarCountComponent from '../../view/remote-queries/StarCount';
5+
import StarCountComponent from '../../view/common/StarCount';
66

77
export default {
88
title: 'Star Count',

extensions/ql-vscode/src/stories/remote-queries/LastUpdated.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
55
import LastUpdatedComponent from '../../view/remote-queries/LastUpdated';
66

77
export default {
8-
title: 'Last Updated',
8+
title: 'MRVA/Last Updated',
99
component: LastUpdatedComponent,
1010
} as ComponentMeta<typeof LastUpdatedComponent>;
1111

extensions/ql-vscode/src/stories/variant-analysis/RepoRow.stories.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ Pending.args = {
3737
id: 63537249,
3838
fullName: 'facebook/create-react-app',
3939
private: false,
40+
stargazersCount: 97_761,
41+
updatedAt: '2022-11-01T13:07:05Z',
4042
},
4143
status: VariantAnalysisRepoStatus.Pending,
4244
};
@@ -104,6 +106,8 @@ SkippedPublic.args = {
104106
...createMockRepositoryWithMetadata(),
105107
fullName: 'octodemo/hello-globe',
106108
private: false,
109+
stargazersCount: 83_372,
110+
updatedAt: '2022-10-28T14:10:35Z',
107111
}
108112
};
109113

@@ -113,5 +117,7 @@ SkippedPrivate.args = {
113117
...createMockRepositoryWithMetadata(),
114118
fullName: 'octodemo/hello-globe',
115119
private: true,
120+
stargazersCount: 83_372,
121+
updatedAt: '2022-05-28T14:10:35Z',
116122
}
117123
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as React from 'react';
2+
import { useMemo } from 'react';
3+
import styled from 'styled-components';
4+
5+
import { parseDate } from '../../pure/date';
6+
import { humanizeRelativeTime } from '../../pure/time';
7+
8+
import { Codicon } from './icon';
9+
10+
const IconContainer = styled.span`
11+
flex-grow: 0;
12+
text-align: right;
13+
margin-right: 0;
14+
`;
15+
16+
const Duration = styled.span`
17+
display: inline-block;
18+
text-align: left;
19+
width: 8em;
20+
margin-left: 0.5em;
21+
`;
22+
23+
type Props = {
24+
lastUpdated?: string | null;
25+
};
26+
27+
export const LastUpdated = ({ lastUpdated }: Props) => {
28+
const date = useMemo(() => parseDate(lastUpdated), [lastUpdated]);
29+
30+
if (!date) {
31+
return null;
32+
}
33+
34+
return (
35+
<div>
36+
<IconContainer>
37+
<Codicon name="repo-push" label="Last updated" />
38+
</IconContainer>
39+
<Duration>
40+
{humanizeRelativeTime(date.getTime() - Date.now())}
41+
</Duration>
42+
</div>
43+
);
44+
};

extensions/ql-vscode/src/view/remote-queries/StarCount.tsx renamed to extensions/ql-vscode/src/view/common/StarCount.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
2-
import { StarIcon } from '@primer/octicons-react';
32
import styled from 'styled-components';
3+
import { Codicon } from './icon';
44

55
const Star = styled.span`
66
flex-grow: 2;
@@ -9,19 +9,22 @@ const Star = styled.span`
99
`;
1010

1111
const Count = styled.span`
12+
display: inline-block;
1213
text-align: left;
1314
width: 2em;
1415
margin-left: 0.5em;
1516
margin-right: 1.5em;
1617
`;
1718

18-
type Props = { starCount?: number };
19+
type Props = {
20+
starCount?: number;
21+
};
1922

2023
const StarCount = ({ starCount }: Props) => (
2124
Number.isFinite(starCount) ? (
2225
<>
2326
<Star>
24-
<StarIcon size={16} />
27+
<Codicon name="star-empty" label="Stars count" />
2528
</Star>
2629
<Count>
2730
{displayStars(starCount!)}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import * as React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
4+
import StarCount from '../StarCount';
5+
6+
describe(StarCount.name, () => {
7+
it('renders undefined stars correctly', () => {
8+
const { container } = render(<StarCount />);
9+
10+
expect(container).toBeEmptyDOMElement();
11+
});
12+
13+
it('renders NaN stars correctly', () => {
14+
const { container } = render(<StarCount starCount={NaN} />);
15+
16+
expect(container).toBeEmptyDOMElement();
17+
});
18+
19+
const testCases = [
20+
{ starCount: 0, expected: '0' },
21+
{ starCount: 1, expected: '1' },
22+
{ starCount: 15, expected: '15' },
23+
{ starCount: 578, expected: '578' },
24+
{ starCount: 999, expected: '999' },
25+
{ starCount: 1_000, expected: '1000' },
26+
{ starCount: 1_001, expected: '1.0k' },
27+
{ starCount: 5_789, expected: '5.8k' },
28+
{ starCount: 9_999, expected: '10.0k' },
29+
{ starCount: 10_000, expected: '10.0k' },
30+
{ starCount: 10_001, expected: '10k' },
31+
{ starCount: 73_543, expected: '74k' },
32+
{ starCount: 155_783, expected: '156k' },
33+
{ starCount: 999_999, expected: '1000k' },
34+
{ starCount: 1_000_000, expected: '1000k' },
35+
{ starCount: 1_000_001, expected: '1000k' },
36+
];
37+
38+
test.each(testCases)('renders $starCount stars as $expected', ({ starCount, expected }) => {
39+
render(<StarCount starCount={starCount} />);
40+
41+
expect(screen.getByText(expected)).toBeInTheDocument();
42+
});
43+
});

extensions/ql-vscode/src/view/remote-queries/RemoteQueries.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { AlertIcon, CodeSquareIcon, FileCodeIcon, RepoIcon, TerminalIcon } from
1515
import AnalysisAlertResult from './AnalysisAlertResult';
1616
import RawResultsTable from './RawResultsTable';
1717
import RepositoriesSearch from './RepositoriesSearch';
18-
import StarCount from './StarCount';
18+
import StarCount from '../common/StarCount';
1919
import SortRepoFilter, { Sort, sorter } from './SortRepoFilter';
2020
import LastUpdated from './LastUpdated';
2121
import RepoListCopyButton from './RepoListCopyButton';

extensions/ql-vscode/src/view/variant-analysis/RepoRow.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import {
99
} from '../../remote-queries/shared/variant-analysis';
1010
import { formatDecimal } from '../../pure/number';
1111
import { Codicon, ErrorIcon, LoadingIcon, SuccessIcon, WarningIcon } from '../common';
12-
import { Repository } from '../../remote-queries/shared/repository';
12+
import { RepositoryWithMetadata } from '../../remote-queries/shared/repository';
1313
import { AnalysisAlert, AnalysisRawResults } from '../../remote-queries/shared/analysis-result';
1414
import { vscode } from '../vscode-api';
1515
import { AnalyzedRepoItemContent } from './AnalyzedRepoItemContent';
16+
import StarCount from '../common/StarCount';
17+
import { LastUpdated } from '../common/LastUpdated';
1618

1719
// This will ensure that these icons have a className which we can use in the TitleContainer
1820
const ExpandCollapseCodicon = styled(Codicon)``;
@@ -21,6 +23,7 @@ const TitleContainer = styled.button`
2123
display: flex;
2224
gap: 0.5em;
2325
align-items: center;
26+
width: 100%;
2427
2528
color: var(--vscode-editor-foreground);
2629
background-color: transparent;
@@ -41,6 +44,11 @@ const VisibilityText = styled.span`
4144
color: var(--vscode-descriptionForeground);
4245
`;
4346

47+
const MetadataContainer = styled.div`
48+
display: flex;
49+
margin-left: auto;
50+
`;
51+
4452
type VisibilityProps = {
4553
isPrivate?: boolean;
4654
}
@@ -65,7 +73,7 @@ const getErrorLabel = (status: VariantAnalysisRepoStatus.Failed | VariantAnalysi
6573

6674
export type RepoRowProps = {
6775
// Only fullName is required
68-
repository: Partial<Repository> & Pick<Repository, 'fullName'>;
76+
repository: Partial<RepositoryWithMetadata> & Pick<RepositoryWithMetadata, 'fullName'>;
6977
status?: VariantAnalysisRepoStatus;
7078
downloadStatus?: VariantAnalysisScannedRepositoryDownloadStatus;
7179
resultCount?: number;
@@ -131,6 +139,10 @@ export const RepoRow = ({
131139
{!status && <WarningIcon />}
132140
</span>
133141
{downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.InProgress && <LoadingIcon label="Downloading" />}
142+
<MetadataContainer>
143+
<div><StarCount starCount={repository.stargazersCount} /></div>
144+
<LastUpdated lastUpdated={repository.updatedAt} />
145+
</MetadataContainer>
134146
</TitleContainer>
135147
{isExpanded && expandableContentLoaded &&
136148
<AnalyzedRepoItemContent status={status} interpretedResults={interpretedResults} rawResults={rawResults} />}

0 commit comments

Comments
 (0)