Merge pull request #1362 from github/aeisenberg/last-update-sort
Add sort MRVA results by last updated
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 = Date.now() - 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,
|
||||
}
|
||||
|
||||
62
extensions/ql-vscode/src/remote-queries/view/LastUpdated.tsx
Normal file
62
extensions/ql-vscode/src/remote-queries/view/LastUpdated.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as React from 'react';
|
||||
import { RepoPushIcon } from '@primer/octicons-react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const IconContainer = 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) ? (
|
||||
<>
|
||||
<IconContainer>
|
||||
<RepoPushIcon size={16} />
|
||||
</IconContainer>
|
||||
<Duration>
|
||||
{humanizeDuration(lastUpdated)}
|
||||
</Duration>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
);
|
||||
|
||||
export default LastUpdated;
|
||||
|
||||
const formatter = new Intl.RelativeTimeFormat('en', {
|
||||
numeric: 'auto'
|
||||
});
|
||||
|
||||
// All these are approximate, specifically months and years
|
||||
const MINUTE_IN_MILLIS = 1000 * 60;
|
||||
const HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
|
||||
const DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
|
||||
const MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
|
||||
const YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
|
||||
|
||||
function humanizeDuration(diff?: number) {
|
||||
if (!diff) {
|
||||
return '';
|
||||
}
|
||||
if (diff < HOUR_IN_MILLIS) {
|
||||
return formatter.format(- Math.floor(diff / MINUTE_IN_MILLIS), 'minute');
|
||||
} else if (diff < DAY_IN_MILLIS) {
|
||||
return formatter.format(- Math.floor(diff / HOUR_IN_MILLIS), 'hour');
|
||||
} else if (diff < MONTH_IN_MILLIS) {
|
||||
return formatter.format(- Math.floor(diff / DAY_IN_MILLIS), 'day');
|
||||
} else if (diff < YEAR_IN_MILLIS) {
|
||||
return formatter.format(- Math.floor(diff / MONTH_IN_MILLIS), 'month');
|
||||
} else {
|
||||
return formatter.format(- Math.floor(diff / YEAR_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 './LastUpdated';
|
||||
|
||||
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 } from '../../../remote-queries/gh-actions-api-client';
|
||||
import { RemoteQuery } from '../../../remote-queries/remote-query';
|
||||
|
||||
describe('gh-actions-api-client mock responses', () => {
|
||||
@@ -61,7 +61,7 @@ describe('gh-actions-api-client real responses', function() {
|
||||
}
|
||||
|
||||
const credentials = await Credentials.initializeWithToken(process.env.VSCODE_CODEQL_GITHUB_TOKEN!);
|
||||
const stargazers = await getStargazersCount(credentials, [
|
||||
const stargazers = await getRepositoriesMetadata(credentials, [
|
||||
'github/codeql',
|
||||
'github/vscode-codeql',
|
||||
'rails/rails',
|
||||
|
||||
@@ -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