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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,4 +15,5 @@ export interface AnalysisSummary {
|
||||
downloadLink: DownloadLink,
|
||||
fileSizeInBytes: number,
|
||||
starCount?: number,
|
||||
lastUpdated?: number,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface AnalysisResults {
|
||||
rawResults?: AnalysisRawResults;
|
||||
resultCount: number,
|
||||
starCount?: number,
|
||||
lastUpdated?: number,
|
||||
}
|
||||
|
||||
export interface AnalysisRawResults {
|
||||
|
||||
@@ -24,4 +24,5 @@ export interface AnalysisSummary {
|
||||
downloadLink: DownloadLink,
|
||||
fileSize: string,
|
||||
starCount?: number,
|
||||
lastUpdated?: number,
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"nwo": "github/vscode-codeql",
|
||||
"resultCount": 5,
|
||||
"starCount": 1,
|
||||
"lastUpdated": 1653447088649,
|
||||
"fileSizeInBytes": 81237,
|
||||
"downloadLink": {
|
||||
"id": "171544171",
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
}]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user