Skip to content

Commit f73bda4

Browse files
authored
Merge pull request #1362 from github/aeisenberg/last-update-sort
Add sort MRVA results by last updated
2 parents 2f9aca7 + 19b65a6 commit f73bda4

File tree

15 files changed

+116
-25
lines changed

15 files changed

+116
-25
lines changed

extensions/ql-vscode/src/remote-queries/analyses-results-manager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export class AnalysesResultsManager {
119119
interpretedResults: [],
120120
resultCount: analysis.resultCount,
121121
starCount: analysis.starCount,
122+
lastUpdated: analysis.lastUpdated,
122123
};
123124
const queryId = analysis.downloadLink.queryId;
124125
const resultsForQuery = this.internalGetAnalysesResults(queryId);

extensions/ql-vscode/src/remote-queries/gh-actions-api-client.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ export async function createGist(
334334
return response.data.html_url;
335335
}
336336

337-
const stargazersQuery = `query Stars($repos: String!, $pageSize: Int!, $cursor: String) {
337+
const repositoriesMetadataQuery = `query Stars($repos: String!, $pageSize: Int!, $cursor: String) {
338338
search(
339339
query: $repos
340340
type: REPOSITORY
@@ -349,14 +349,15 @@ const stargazersQuery = `query Stars($repos: String!, $pageSize: Int!, $cursor:
349349
login
350350
}
351351
stargazerCount
352+
updatedAt
352353
}
353354
}
354355
cursor
355356
}
356357
}
357358
}`;
358359

359-
type StargazersQueryResponse = {
360+
type RepositoriesMetadataQueryResponse = {
360361
search: {
361362
edges: {
362363
cursor: string;
@@ -366,20 +367,23 @@ type StargazersQueryResponse = {
366367
login: string;
367368
};
368369
stargazerCount: number;
370+
updatedAt: string; // Actually a ISO Date string
369371
}
370372
}[]
371373
}
372374
};
373375

374-
export async function getStargazers(credentials: Credentials, nwos: string[], pageSize = 100): Promise<Record<string, number>> {
376+
export type RepositoriesMetadata = Record<string, { starCount: number, lastUpdated: number }>
377+
378+
export async function getRepositoriesMetadata(credentials: Credentials, nwos: string[], pageSize = 100): Promise<RepositoriesMetadata> {
375379
const octokit = await credentials.getOctokit();
376380
const repos = `repo:${nwos.join(' repo:')} fork:true`;
377381
let cursor = null;
378-
const stargazers: Record<string, number> = {};
382+
const metadata: RepositoriesMetadata = {};
379383
try {
380384
do {
381-
const response: StargazersQueryResponse = await octokit.graphql({
382-
query: stargazersQuery,
385+
const response: RepositoriesMetadataQueryResponse = await octokit.graphql({
386+
query: repositoriesMetadataQuery,
383387
repos,
384388
pageSize,
385389
cursor
@@ -390,14 +394,17 @@ export async function getStargazers(credentials: Credentials, nwos: string[], pa
390394
const node = edge.node;
391395
const owner = node.owner.login;
392396
const name = node.name;
393-
const stargazerCount = node.stargazerCount;
394-
stargazers[`${owner}/${name}`] = stargazerCount;
397+
const starCount = node.stargazerCount;
398+
const lastUpdated = Date.now() - new Date(node.updatedAt).getTime();
399+
metadata[`${owner}/${name}`] = {
400+
starCount, lastUpdated
401+
};
395402
}
396403

397404
} while (cursor);
398405
} catch (e) {
399406
void showAndLogErrorMessage(`Error retrieving repository metadata for variant analysis: ${getErrorMessage(e)}`);
400407
}
401408

402-
return stargazers;
409+
return metadata;
403410
}

extensions/ql-vscode/src/remote-queries/remote-queries-interface.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,8 @@ export class RemoteQueriesInterfaceManager {
307307
resultCount: analysisResult.resultCount,
308308
downloadLink: analysisResult.downloadLink,
309309
fileSize: this.formatFileSize(analysisResult.fileSizeInBytes),
310-
starCount: analysisResult.starCount
310+
starCount: analysisResult.starCount,
311+
lastUpdated: analysisResult.lastUpdated
311312
}));
312313
}
313314
}

extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { runRemoteQuery } from './run-remote-query';
1212
import { RemoteQueriesInterfaceManager } from './remote-queries-interface';
1313
import { RemoteQuery } from './remote-query';
1414
import { RemoteQueriesMonitor } from './remote-queries-monitor';
15-
import { getRemoteQueryIndex, getStargazers } from './gh-actions-api-client';
15+
import { getRemoteQueryIndex, getRepositoriesMetadata, RepositoriesMetadata } from './gh-actions-api-client';
1616
import { RemoteQueryResultIndex } from './remote-query-result-index';
1717
import { RemoteQueryResult } from './remote-query-result';
1818
import { DownloadLink } from './download-link';
@@ -185,19 +185,20 @@ export class RemoteQueriesManager extends DisposableObject {
185185
executionEndTime: number,
186186
resultIndex: RemoteQueryResultIndex,
187187
queryId: string,
188-
stargazers: Record<string, number>
188+
metadata: RepositoriesMetadata
189189
): RemoteQueryResult {
190190
const analysisSummaries = resultIndex.successes.map(item => ({
191191
nwo: item.nwo,
192192
databaseSha: item.sha || 'HEAD',
193193
resultCount: item.resultCount,
194194
fileSizeInBytes: item.sarifFileSize ? item.sarifFileSize : item.bqrsFileSize,
195+
starCount: metadata[item.nwo].starCount,
196+
lastUpdated: metadata[item.nwo].lastUpdated,
195197
downloadLink: {
196198
id: item.artifactId.toString(),
197199
urlPath: `${resultIndex.artifactsUrlPath}/${item.artifactId}`,
198200
innerFilePath: item.sarifFileSize ? 'results.sarif' : 'results.bqrs',
199-
queryId,
200-
starCount: stargazers[item.nwo]
201+
queryId
201202
} as DownloadLink
202203
}));
203204
const analysisFailures = resultIndex.failures.map(item => ({
@@ -284,8 +285,8 @@ export class RemoteQueriesManager extends DisposableObject {
284285
queryItem.completed = true;
285286
queryItem.status = QueryStatus.Completed;
286287
queryItem.failureReason = undefined;
287-
const stargazers = await this.getStargazersCount(resultIndex, credentials);
288-
const queryResult = this.mapQueryResult(executionEndTime, resultIndex, queryItem.queryId, stargazers);
288+
const metadata = await this.getRepositoriesMetadata(resultIndex, credentials);
289+
const queryResult = this.mapQueryResult(executionEndTime, resultIndex, queryItem.queryId, metadata);
289290

290291
await this.storeJsonFile(queryItem, 'query-result.json', queryResult);
291292

@@ -309,9 +310,9 @@ export class RemoteQueriesManager extends DisposableObject {
309310
}
310311
}
311312

312-
private async getStargazersCount(resultIndex: RemoteQueryResultIndex, credentials: Credentials) {
313+
private async getRepositoriesMetadata(resultIndex: RemoteQueryResultIndex, credentials: Credentials) {
313314
const nwos = resultIndex.successes.map(s => s.nwo);
314-
return await getStargazers(credentials, nwos);
315+
return await getRepositoriesMetadata(credentials, nwos);
315316
}
316317

317318
// Pulled from the analysis results manager, so that we can get access to

extensions/ql-vscode/src/remote-queries/remote-query-result.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ export interface AnalysisSummary {
1515
downloadLink: DownloadLink,
1616
fileSizeInBytes: number,
1717
starCount?: number,
18+
lastUpdated?: number,
1819
}

extensions/ql-vscode/src/remote-queries/shared/analysis-result.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface AnalysisResults {
99
rawResults?: AnalysisRawResults;
1010
resultCount: number,
1111
starCount?: number,
12+
lastUpdated?: number,
1213
}
1314

1415
export interface AnalysisRawResults {

extensions/ql-vscode/src/remote-queries/shared/remote-query-result.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ export interface AnalysisSummary {
2424
downloadLink: DownloadLink,
2525
fileSize: string,
2626
starCount?: number,
27+
lastUpdated?: number,
2728
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as React from 'react';
2+
import { RepoPushIcon } from '@primer/octicons-react';
3+
import styled from 'styled-components';
4+
5+
const IconContainer = styled.span`
6+
flex-grow: 0;
7+
text-align: right;
8+
margin-right: 0;
9+
`;
10+
11+
const Duration = styled.span`
12+
text-align: left;
13+
width: 8em;
14+
margin-left: 0.5em;
15+
`;
16+
17+
type Props = { lastUpdated?: number };
18+
19+
const LastUpdated = ({ lastUpdated }: Props) => (
20+
Number.isFinite(lastUpdated) ? (
21+
<>
22+
<IconContainer>
23+
<RepoPushIcon size={16} />
24+
</IconContainer>
25+
<Duration>
26+
{humanizeDuration(lastUpdated)}
27+
</Duration>
28+
</>
29+
) : (
30+
<></>
31+
)
32+
);
33+
34+
export default LastUpdated;
35+
36+
const formatter = new Intl.RelativeTimeFormat('en', {
37+
numeric: 'auto'
38+
});
39+
40+
// All these are approximate, specifically months and years
41+
const MINUTE_IN_MILLIS = 1000 * 60;
42+
const HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
43+
const DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
44+
const MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
45+
const YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
46+
47+
function humanizeDuration(diff?: number) {
48+
if (!diff) {
49+
return '';
50+
}
51+
if (diff < HOUR_IN_MILLIS) {
52+
return formatter.format(- Math.floor(diff / MINUTE_IN_MILLIS), 'minute');
53+
} else if (diff < DAY_IN_MILLIS) {
54+
return formatter.format(- Math.floor(diff / HOUR_IN_MILLIS), 'hour');
55+
} else if (diff < MONTH_IN_MILLIS) {
56+
return formatter.format(- Math.floor(diff / DAY_IN_MILLIS), 'day');
57+
} else if (diff < YEAR_IN_MILLIS) {
58+
return formatter.format(- Math.floor(diff / MONTH_IN_MILLIS), 'month');
59+
} else {
60+
return formatter.format(- Math.floor(diff / YEAR_IN_MILLIS), 'year');
61+
}
62+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import RepositoriesSearch from './RepositoriesSearch';
2323
import ActionButton from './ActionButton';
2424
import StarCount from './StarCount';
2525
import SortRepoFilter, { Sort, sorter } from './SortRepoFilter';
26+
import LastUpdated from './LastUpdated';
2627

2728
const numOfReposInContractedMode = 10;
2829

@@ -200,6 +201,7 @@ const SummaryItem = ({
200201
analysisResults={analysisResults} />
201202
</span>
202203
<StarCount starCount={analysisSummary.starCount} />
204+
<LastUpdated lastUpdated={analysisSummary.lastUpdated} />
203205
</>
204206
);
205207

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const SortWrapper = styled.span`
99
margin-right: 0;
1010
`;
1111

12-
export type Sort = 'name' | 'stars' | 'results';
12+
export type Sort = 'name' | 'stars' | 'results' | 'lastUpdated';
1313
type Props = {
1414
sort: Sort;
1515
setSort: (sort: Sort) => void;
@@ -19,12 +19,14 @@ type Sortable = {
1919
nwo: string;
2020
starCount?: number;
2121
resultCount?: number;
22+
lastUpdated?: number;
2223
};
2324

2425
const sortBy = [
2526
{ name: 'Sort by Name', sort: 'name' },
2627
{ name: 'Sort by Results', sort: 'results' },
2728
{ name: 'Sort by Stars', sort: 'stars' },
29+
{ name: 'Sort by Last Updated', sort: 'lastUpdated' },
2830
];
2931

3032
export function sorter(sort: Sort): (left: Sortable, right: Sortable) => number {
@@ -37,20 +39,24 @@ export function sorter(sort: Sort): (left: Sortable, right: Sortable) => number
3739
return stars;
3840
}
3941
}
42+
if (sort === 'lastUpdated') {
43+
const lastUpdated = (right.lastUpdated || 0) - (left.lastUpdated || 0);
44+
if (lastUpdated !== 0) {
45+
return lastUpdated;
46+
}
47+
}
4048
if (sort === 'results') {
4149
const results = (right.resultCount || 0) - (left.resultCount || 0);
4250
if (results !== 0) {
4351
return results;
4452
}
4553
}
4654

47-
// Fall back on name compare if results or stars are equal
55+
// Fall back on name compare if results, stars, or lastUpdated are equal
4856
return left.nwo.localeCompare(right.nwo, undefined, { sensitivity: 'base' });
4957
};
5058
}
5159

52-
// FIXME These styles are not correct. Need to figure out
53-
// why the theme is not being applied to the ActionMenu
5460
const SortRepoFilter = ({ sort, setSort }: Props) => {
5561
return <SortWrapper>
5662
<ActionMenu>
@@ -72,7 +78,6 @@ const SortRepoFilter = ({ sort, setSort }: Props) => {
7278
</ActionMenu.Overlay>
7379
</ActionMenu>
7480
</SortWrapper>;
75-
7681
};
7782

7883
export default SortRepoFilter;

0 commit comments

Comments
 (0)