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?
This commit is contained in:
Andrew Eisenberg
2022-05-24 14:50:10 -07:00
parent 405a6c9901
commit e43b4e66a1
15 changed files with 116 additions and 24 deletions

View File

@@ -119,6 +119,7 @@ export class AnalysesResultsManager {
interpretedResults: [],
resultCount: analysis.resultCount,
starCount: analysis.starCount,
lastUpdated: analysis.lastUpdated,
};
const queryId = analysis.downloadLink.queryId;
const resultsForQuery = this.internalGetAnalysesResults(queryId);

View File

@@ -334,7 +334,7 @@ export async function createGist(
return response.data.html_url;
}
const stargazersQuery = `query Stars($repos: String!, $pageSize: Int!, $cursor: String) {
const repositoriesMetadataQuery = `query Stars($repos: String!, $pageSize: Int!, $cursor: String) {
search(
query: $repos
type: REPOSITORY
@@ -349,6 +349,7 @@ const stargazersQuery = `query Stars($repos: String!, $pageSize: Int!, $cursor:
login
}
stargazerCount
updatedAt
}
}
cursor
@@ -356,7 +357,7 @@ const stargazersQuery = `query Stars($repos: String!, $pageSize: Int!, $cursor:
}
}`;
type StargazersQueryResponse = {
type RepositoriesMetadataQueryResponse = {
search: {
edges: {
cursor: string;
@@ -366,20 +367,23 @@ type StargazersQueryResponse = {
login: string;
};
stargazerCount: number;
updatedAt: string; // Actually a ISO Date string
}
}[]
}
};
export async function getStargazers(credentials: Credentials, nwos: string[], pageSize = 100): Promise<Record<string, number>> {
export type RepositoriesMetadata = Record<string, { starCount: number, lastUpdated: number }>
export async function getRepositoriesMetadata(credentials: Credentials, nwos: string[], pageSize = 100): Promise<RepositoriesMetadata> {
const octokit = await credentials.getOctokit();
const repos = `repo:${nwos.join(' repo:')} fork:true`;
let cursor = null;
const stargazers: Record<string, number> = {};
const metadata: RepositoriesMetadata = {};
try {
do {
const response: StargazersQueryResponse = await octokit.graphql({
query: stargazersQuery,
const response: RepositoriesMetadataQueryResponse = await octokit.graphql({
query: repositoriesMetadataQuery,
repos,
pageSize,
cursor
@@ -390,8 +394,11 @@ export async function getStargazers(credentials: Credentials, nwos: string[], pa
const node = edge.node;
const owner = node.owner.login;
const name = node.name;
const stargazerCount = node.stargazerCount;
stargazers[`${owner}/${name}`] = stargazerCount;
const starCount = node.stargazerCount;
const lastUpdated = new Date(node.updatedAt).getTime();
metadata[`${owner}/${name}`] = {
starCount, lastUpdated
};
}
} while (cursor);
@@ -399,5 +406,5 @@ export async function getStargazers(credentials: Credentials, nwos: string[], pa
void showAndLogErrorMessage(`Error retrieving repository metadata for variant analysis: ${getErrorMessage(e)}`);
}
return stargazers;
return metadata;
}

View File

@@ -307,7 +307,8 @@ export class RemoteQueriesInterfaceManager {
resultCount: analysisResult.resultCount,
downloadLink: analysisResult.downloadLink,
fileSize: this.formatFileSize(analysisResult.fileSizeInBytes),
starCount: analysisResult.starCount
starCount: analysisResult.starCount,
lastUpdated: analysisResult.lastUpdated
}));
}
}

View File

@@ -12,7 +12,7 @@ import { runRemoteQuery } from './run-remote-query';
import { RemoteQueriesInterfaceManager } from './remote-queries-interface';
import { RemoteQuery } from './remote-query';
import { RemoteQueriesMonitor } from './remote-queries-monitor';
import { getRemoteQueryIndex, getStargazers } from './gh-actions-api-client';
import { getRemoteQueryIndex, getRepositoriesMetadata, RepositoriesMetadata } from './gh-actions-api-client';
import { RemoteQueryResultIndex } from './remote-query-result-index';
import { RemoteQueryResult } from './remote-query-result';
import { DownloadLink } from './download-link';
@@ -185,19 +185,20 @@ export class RemoteQueriesManager extends DisposableObject {
executionEndTime: number,
resultIndex: RemoteQueryResultIndex,
queryId: string,
stargazers: Record<string, number>
metadata: RepositoriesMetadata
): RemoteQueryResult {
const analysisSummaries = resultIndex.successes.map(item => ({
nwo: item.nwo,
databaseSha: item.sha || 'HEAD',
resultCount: item.resultCount,
fileSizeInBytes: item.sarifFileSize ? item.sarifFileSize : item.bqrsFileSize,
starCount: metadata[item.nwo].starCount,
lastUpdated: metadata[item.nwo].lastUpdated,
downloadLink: {
id: item.artifactId.toString(),
urlPath: `${resultIndex.artifactsUrlPath}/${item.artifactId}`,
innerFilePath: item.sarifFileSize ? 'results.sarif' : 'results.bqrs',
queryId,
starCount: stargazers[item.nwo]
queryId
} as DownloadLink
}));
const analysisFailures = resultIndex.failures.map(item => ({
@@ -284,8 +285,8 @@ export class RemoteQueriesManager extends DisposableObject {
queryItem.completed = true;
queryItem.status = QueryStatus.Completed;
queryItem.failureReason = undefined;
const stargazers = await this.getStargazersCount(resultIndex, credentials);
const queryResult = this.mapQueryResult(executionEndTime, resultIndex, queryItem.queryId, stargazers);
const metadata = await this.getRepositoriesMetadata(resultIndex, credentials);
const queryResult = this.mapQueryResult(executionEndTime, resultIndex, queryItem.queryId, metadata);
await this.storeJsonFile(queryItem, 'query-result.json', queryResult);
@@ -309,9 +310,9 @@ export class RemoteQueriesManager extends DisposableObject {
}
}
private async getStargazersCount(resultIndex: RemoteQueryResultIndex, credentials: Credentials) {
private async getRepositoriesMetadata(resultIndex: RemoteQueryResultIndex, credentials: Credentials) {
const nwos = resultIndex.successes.map(s => s.nwo);
return await getStargazers(credentials, nwos);
return await getRepositoriesMetadata(credentials, nwos);
}
// Pulled from the analysis results manager, so that we can get access to

View File

@@ -15,4 +15,5 @@ export interface AnalysisSummary {
downloadLink: DownloadLink,
fileSizeInBytes: number,
starCount?: number,
lastUpdated?: number,
}

View File

@@ -9,6 +9,7 @@ export interface AnalysisResults {
rawResults?: AnalysisRawResults;
resultCount: number,
starCount?: number,
lastUpdated?: number,
}
export interface AnalysisRawResults {

View File

@@ -24,4 +24,5 @@ export interface AnalysisSummary {
downloadLink: DownloadLink,
fileSize: string,
starCount?: number,
lastUpdated?: number,
}

View File

@@ -0,0 +1,63 @@
import * as React from 'react';
import { CalendarIcon } from '@primer/octicons-react';
import styled from 'styled-components';
const Calendar = styled.span`
flex-grow: 0;
text-align: right;
margin-right: 0;
`;
const Duration = styled.span`
text-align: left;
width: 8em;
margin-left: 0.5em;
`;
type Props = { lastUpdated?: number };
const LastUpdated = ({ lastUpdated }: Props) => (
Number.isFinite(lastUpdated) ? (
<>
<Calendar>
<CalendarIcon size={16} />
</Calendar>
<Duration>
{humanizeDuration(lastUpdated)}
</Duration>
</>
) : (
<></>
)
);
export default LastUpdated;
const formatter = new Intl.RelativeTimeFormat('en', {
numeric: 'auto'
});
// All these are approximate, specifically months and years
const MINUTES_IN_MILLIS = 1000 * 60;
const HOURS_IN_MILLIS = 60 * MINUTES_IN_MILLIS;
const DAYS_IN_MILLIS = 24 * HOURS_IN_MILLIS;
const MONTHS_IN_MILLIS = 30 * DAYS_IN_MILLIS;
const YEARS_IN_MILLIS = 365 * DAYS_IN_MILLIS;
function humanizeDuration(from?: number) {
if (!from) {
return '';
}
const diff = Date.now() - from;
if (diff < HOURS_IN_MILLIS) {
return formatter.format(- Math.floor(diff / MINUTES_IN_MILLIS), 'minute');
} else if (diff < DAYS_IN_MILLIS) {
return formatter.format(- Math.floor(diff / HOURS_IN_MILLIS), 'hour');
} else if (diff < MONTHS_IN_MILLIS) {
return formatter.format(- Math.floor(diff / DAYS_IN_MILLIS), 'day');
} else if (diff < YEARS_IN_MILLIS) {
return formatter.format(- Math.floor(diff / MONTHS_IN_MILLIS), 'month');
} else {
return formatter.format(- Math.floor(diff / YEARS_IN_MILLIS), 'year');
}
}

View File

@@ -23,6 +23,7 @@ import RepositoriesSearch from './RepositoriesSearch';
import ActionButton from './ActionButton';
import StarCount from './StarCount';
import SortRepoFilter, { Sort, sorter } from './SortRepoFilter';
import LastUpdated from './LasstUpdated';
const numOfReposInContractedMode = 10;
@@ -200,6 +201,7 @@ const SummaryItem = ({
analysisResults={analysisResults} />
</span>
<StarCount starCount={analysisSummary.starCount} />
<LastUpdated lastUpdated={analysisSummary.lastUpdated} />
</>
);

View File

@@ -9,7 +9,7 @@ const SortWrapper = styled.span`
margin-right: 0;
`;
export type Sort = 'name' | 'stars' | 'results';
export type Sort = 'name' | 'stars' | 'results' | 'lastUpdated';
type Props = {
sort: Sort;
setSort: (sort: Sort) => void;
@@ -19,12 +19,14 @@ type Sortable = {
nwo: string;
starCount?: number;
resultCount?: number;
lastUpdated?: number;
};
const sortBy = [
{ name: 'Sort by Name', sort: 'name' },
{ name: 'Sort by Results', sort: 'results' },
{ name: 'Sort by Stars', sort: 'stars' },
{ name: 'Sort by Last Updated', sort: 'lastUpdated' },
];
export function sorter(sort: Sort): (left: Sortable, right: Sortable) => number {
@@ -37,6 +39,12 @@ export function sorter(sort: Sort): (left: Sortable, right: Sortable) => number
return stars;
}
}
if (sort === 'lastUpdated') {
const lastUpdated = (right.lastUpdated || 0) - (left.lastUpdated || 0);
if (lastUpdated !== 0) {
return lastUpdated;
}
}
if (sort === 'results') {
const results = (right.resultCount || 0) - (left.resultCount || 0);
if (results !== 0) {
@@ -44,13 +52,11 @@ export function sorter(sort: Sort): (left: Sortable, right: Sortable) => number
}
}
// Fall back on name compare if results or stars are equal
// Fall back on name compare if results, stars, or lastUpdated are equal
return left.nwo.localeCompare(right.nwo, undefined, { sensitivity: 'base' });
};
}
// FIXME These styles are not correct. Need to figure out
// why the theme is not being applied to the ActionMenu
const SortRepoFilter = ({ sort, setSort }: Props) => {
return <SortWrapper>
<ActionMenu>
@@ -72,7 +78,6 @@ const SortRepoFilter = ({ sort, setSort }: Props) => {
</ActionMenu.Overlay>
</ActionMenu>
</SortWrapper>;
};
export default SortRepoFilter;

View File

@@ -12,6 +12,7 @@ const Count = styled.span`
text-align: left;
width: 2em;
margin-left: 0.5em;
margin-right: 1.5em;
`;
type Props = { starCount?: number };

View File

@@ -5,6 +5,7 @@
"nwo": "github/vscode-codeql",
"resultCount": 15,
"starCount": 1,
"lastUpdated": 1653447088649,
"fileSizeInBytes": 191025,
"downloadLink": {
"id": "171543249",
@@ -17,6 +18,7 @@
"nwo": "other/hucairz",
"resultCount": 15,
"starCount": 1,
"lastUpdated": 1653447088649,
"fileSizeInBytes": 191025,
"downloadLink": {
"id": "11111111",

View File

@@ -5,6 +5,7 @@
"nwo": "github/vscode-codeql",
"resultCount": 5,
"starCount": 1,
"lastUpdated": 1653447088649,
"fileSizeInBytes": 81237,
"downloadLink": {
"id": "171544171",

View File

@@ -2,7 +2,7 @@ import { fail } from 'assert';
import { expect } from 'chai';
import * as sinon from 'sinon';
import { Credentials } from '../../../authentication';
import { cancelRemoteQuery, getStargazers as getStargazersCount } from '../../../remote-queries/gh-actions-api-client';
import { cancelRemoteQuery, getRepositoriesMetadata as getStargazersCount } from '../../../remote-queries/gh-actions-api-client';
import { RemoteQuery } from '../../../remote-queries/remote-query';
describe('gh-actions-api-client mock responses', () => {

View File

@@ -254,6 +254,7 @@ describe('Remote queries and query history manager', function() {
nwo: 'github/vscode-codeql',
status: 'InProgress',
resultCount: 15,
lastUpdated: 1653447088649,
starCount: 1
}]);
@@ -261,11 +262,13 @@ describe('Remote queries and query history manager', function() {
nwo: 'github/vscode-codeql',
status: 'InProgress',
resultCount: 15,
lastUpdated: 1653447088649,
starCount: 1
}, {
nwo: 'other/hucairz',
status: 'InProgress',
resultCount: 15,
lastUpdated: 1653447088649,
starCount: 1
}]);
@@ -277,11 +280,13 @@ describe('Remote queries and query history manager', function() {
nwo: 'github/vscode-codeql',
status: 'Completed',
resultCount: 15,
lastUpdated: 1653447088649,
starCount: 1
}, {
nwo: 'other/hucairz',
status: 'Completed',
resultCount: 15,
lastUpdated: 1653447088649,
starCount: 1
}]);