Skip to content

Commit e43b4e6

Browse files
committed
Add sort MRVA results by last updated
1. Refactor references of `Stargazers` to `RepositoryMetadata` since the query is now more generic. 2. Update the graphql query to request last updated as well as stars 3. Update web view to display last updated 4. Update sort mechanism for last updated A few notes: 1. I used `Intl.RelativeTimeFormat` to humanize the times. It wasn't as simple as I had hoped since I need to also make a guess as to which unit to use. 2. The icon used by last updated is not quite what is in the wireframes. But, I wanted to stick with primer icons and I used the closest I can get. 3. The last updated time is retrieved when the query is first loaded into vscode and then never changes. However, this time is always compared with `Date.now()`. So, opening the query up a week from now, all of the last updated times would be one week older (even if the repository has been updated since then). I don't want to re-retrieve the last updated time each time we open the query, so this timestamp will get out of date eventually. Is this confusing as it is?
1 parent 405a6c9 commit e43b4e6

15 files changed

Lines changed: 116 additions & 24 deletions

File tree

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 = 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: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as React from 'react';
2+
import { CalendarIcon } from '@primer/octicons-react';
3+
import styled from 'styled-components';
4+
5+
const Calendar = 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+
<Calendar>
23+
<CalendarIcon size={16} />
24+
</Calendar>
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 MINUTES_IN_MILLIS = 1000 * 60;
42+
const HOURS_IN_MILLIS = 60 * MINUTES_IN_MILLIS;
43+
const DAYS_IN_MILLIS = 24 * HOURS_IN_MILLIS;
44+
const MONTHS_IN_MILLIS = 30 * DAYS_IN_MILLIS;
45+
const YEARS_IN_MILLIS = 365 * DAYS_IN_MILLIS;
46+
47+
function humanizeDuration(from?: number) {
48+
if (!from) {
49+
return '';
50+
}
51+
const diff = Date.now() - from;
52+
if (diff < HOURS_IN_MILLIS) {
53+
return formatter.format(- Math.floor(diff / MINUTES_IN_MILLIS), 'minute');
54+
} else if (diff < DAYS_IN_MILLIS) {
55+
return formatter.format(- Math.floor(diff / HOURS_IN_MILLIS), 'hour');
56+
} else if (diff < MONTHS_IN_MILLIS) {
57+
return formatter.format(- Math.floor(diff / DAYS_IN_MILLIS), 'day');
58+
} else if (diff < YEARS_IN_MILLIS) {
59+
return formatter.format(- Math.floor(diff / MONTHS_IN_MILLIS), 'month');
60+
} else {
61+
return formatter.format(- Math.floor(diff / YEARS_IN_MILLIS), 'year');
62+
}
63+
}

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 './LasstUpdated';
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)