Add filtering and sorting to exported repo list

This will pass the filter and sort parameters in the export repo list
message so it can be used by the command to filter and sort the
repositories which are placed in the repo list.
This commit is contained in:
Koen Vlaswinkel
2022-11-14 14:50:49 +01:00
parent 82a7fc5070
commit bb4307ea3e
11 changed files with 110 additions and 31 deletions

View File

@@ -116,6 +116,7 @@ import { createVariantAnalysisContentProvider } from './remote-queries/variant-a
import { VSCodeMockGitHubApiServer } from './mocks/vscode-mock-gh-api-server'; import { VSCodeMockGitHubApiServer } from './mocks/vscode-mock-gh-api-server';
import { VariantAnalysisResultsManager } from './remote-queries/variant-analysis-results-manager'; import { VariantAnalysisResultsManager } from './remote-queries/variant-analysis-results-manager';
import { initializeDbModule } from './databases/db-module'; import { initializeDbModule } from './databases/db-module';
import { RepositoriesFilterSortState } from './pure/variant-analysis-filter-sort';
/** /**
* extension.ts * extension.ts
@@ -945,8 +946,8 @@ async function activateWithInstalledDistribution(
); );
ctx.subscriptions.push( ctx.subscriptions.push(
commandRunner('codeQL.copyVariantAnalysisRepoList', async (variantAnalysisId: number) => { commandRunner('codeQL.copyVariantAnalysisRepoList', async (variantAnalysisId: number, filterSort?: RepositoriesFilterSortState) => {
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysisId); await variantAnalysisManager.copyRepoListToClipboard(variantAnalysisId, filterSort);
}) })
); );

View File

@@ -7,6 +7,7 @@ import {
VariantAnalysisScannedRepositoryResult, VariantAnalysisScannedRepositoryResult,
VariantAnalysisScannedRepositoryState, VariantAnalysisScannedRepositoryState,
} from '../remote-queries/shared/variant-analysis'; } from '../remote-queries/shared/variant-analysis';
import { RepositoriesFilterSortState } from './variant-analysis-filter-sort';
/** /**
* This module contains types and code that are shared between * This module contains types and code that are shared between
@@ -474,6 +475,7 @@ export interface OpenQueryTextMessage {
export interface CopyRepositoryListMessage { export interface CopyRepositoryListMessage {
t: 'copyRepositoryList'; t: 'copyRepositoryList';
filterSort?: RepositoriesFilterSortState;
} }
export interface OpenLogsMessage { export interface OpenLogsMessage {

View File

@@ -71,3 +71,17 @@ export function compareWithResults(filterSortState: RepositoriesFilterSortState
return fallbackSort(left.repository, right.repository); return fallbackSort(left.repository, right.repository);
}; };
} }
// These define the behavior for undefined input values
export function filterAndSortRepositoriesWithResults<T extends SortableResult>(repositories: T[], filterSortState: RepositoriesFilterSortState | undefined): T[];
export function filterAndSortRepositoriesWithResults<T extends SortableResult>(repositories: T[] | undefined, filterSortState: RepositoriesFilterSortState | undefined): T[] | undefined;
export function filterAndSortRepositoriesWithResults<T extends SortableResult>(repositories: T[] | undefined, filterSortState: RepositoriesFilterSortState | undefined): T[] | undefined {
if (!repositories) {
return undefined;
}
return repositories
.filter(repo => matchesFilter(repo.repository, filterSortState))
.sort(compareWithResults(filterSortState));
}

View File

@@ -32,6 +32,11 @@ import * as os from 'os';
import { cancelVariantAnalysis } from './gh-api/gh-actions-api-client'; import { cancelVariantAnalysis } from './gh-api/gh-actions-api-client';
import { ProgressCallback, UserCancellationException } from '../commandRunner'; import { ProgressCallback, UserCancellationException } from '../commandRunner';
import { CodeQLCliServer } from '../cli'; import { CodeQLCliServer } from '../cli';
import {
defaultFilterSortState,
filterAndSortRepositoriesWithResults,
RepositoriesFilterSortState,
} from '../pure/variant-analysis-filter-sort';
export class VariantAnalysisManager extends DisposableObject implements VariantAnalysisViewManager<VariantAnalysisView> { export class VariantAnalysisManager extends DisposableObject implements VariantAnalysisViewManager<VariantAnalysisView> {
private static readonly REPO_STATES_FILENAME = 'repo_states.json'; private static readonly REPO_STATES_FILENAME = 'repo_states.json';
@@ -368,13 +373,15 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
await cancelVariantAnalysis(credentials, variantAnalysis); await cancelVariantAnalysis(credentials, variantAnalysis);
} }
public async copyRepoListToClipboard(variantAnalysisId: number) { public async copyRepoListToClipboard(variantAnalysisId: number, filterSort: RepositoriesFilterSortState = defaultFilterSortState) {
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId); const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
if (!variantAnalysis) { if (!variantAnalysis) {
throw new Error(`No variant analysis with id: ${variantAnalysisId}`); throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
} }
const fullNames = variantAnalysis.scannedRepos?.filter(a => a.resultCount && a.resultCount > 0).map(a => a.repository.fullName); const filteredRepositories = filterAndSortRepositoriesWithResults(variantAnalysis.scannedRepos ?? [], filterSort);
const fullNames = filteredRepositories.filter(a => a.resultCount && a.resultCount > 0).map(a => a.repository.fullName);
if (!fullNames || fullNames.length === 0) { if (!fullNames || fullNames.length === 0) {
return; return;
} }

View File

@@ -104,7 +104,7 @@ export class VariantAnalysisView extends AbstractWebview<ToVariantAnalysisMessag
await this.openQueryText(); await this.openQueryText();
break; break;
case 'copyRepositoryList': case 'copyRepositoryList':
void commands.executeCommand('codeQL.copyVariantAnalysisRepoList', this.variantAnalysisId); void commands.executeCommand('codeQL.copyVariantAnalysisRepoList', this.variantAnalysisId, msg.filterSort);
break; break;
case 'openLogs': case 'openLogs':
await this.openLogs(); await this.openLogs();

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react'; import { ComponentMeta, ComponentStory } from '@storybook/react';
@@ -8,6 +8,7 @@ import { VariantAnalysisRepoStatus, VariantAnalysisStatus } from '../../remote-q
import { createMockScannedRepo } from '../../vscode-tests/factories/remote-queries/shared/scanned-repositories'; import { createMockScannedRepo } from '../../vscode-tests/factories/remote-queries/shared/scanned-repositories';
import { createMockVariantAnalysis } from '../../vscode-tests/factories/remote-queries/shared/variant-analysis'; import { createMockVariantAnalysis } from '../../vscode-tests/factories/remote-queries/shared/variant-analysis';
import { createMockRepositoryWithMetadata } from '../../vscode-tests/factories/remote-queries/shared/repository'; import { createMockRepositoryWithMetadata } from '../../vscode-tests/factories/remote-queries/shared/repository';
import { defaultFilterSortState, RepositoriesFilterSortState } from '../../pure/variant-analysis-filter-sort';
export default { export default {
title: 'Variant Analysis/Variant Analysis Outcome Panels', title: 'Variant Analysis/Variant Analysis Outcome Panels',
@@ -21,9 +22,13 @@ export default {
], ],
} as ComponentMeta<typeof VariantAnalysisOutcomePanels>; } as ComponentMeta<typeof VariantAnalysisOutcomePanels>;
const Template: ComponentStory<typeof VariantAnalysisOutcomePanels> = (args) => ( const Template: ComponentStory<typeof VariantAnalysisOutcomePanels> = (args) => {
<VariantAnalysisOutcomePanels {...args} /> const [filterSortState, setFilterSortState] = useState<RepositoriesFilterSortState>(defaultFilterSortState);
);
return (
<VariantAnalysisOutcomePanels {...args} filterSortState={filterSortState} setFilterSortState={setFilterSortState} />
);
};
export const WithoutSkippedRepos = Template.bind({}); export const WithoutSkippedRepos = Template.bind({});
WithoutSkippedRepos.args = { WithoutSkippedRepos.args = {

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { import {
VariantAnalysis as VariantAnalysisDomainModel, VariantAnalysis as VariantAnalysisDomainModel,
@@ -11,6 +11,7 @@ import { VariantAnalysisOutcomePanels } from './VariantAnalysisOutcomePanels';
import { VariantAnalysisLoading } from './VariantAnalysisLoading'; import { VariantAnalysisLoading } from './VariantAnalysisLoading';
import { ToVariantAnalysisMessage } from '../../pure/interface-types'; import { ToVariantAnalysisMessage } from '../../pure/interface-types';
import { vscode } from '../vscode-api'; import { vscode } from '../vscode-api';
import { defaultFilterSortState, RepositoriesFilterSortState } from '../../pure/variant-analysis-filter-sort';
type Props = { type Props = {
variantAnalysis?: VariantAnalysisDomainModel; variantAnalysis?: VariantAnalysisDomainModel;
@@ -36,12 +37,6 @@ const stopQuery = () => {
}); });
}; };
const copyRepositoryList = () => {
vscode.postMessage({
t: 'copyRepositoryList',
});
};
const openLogs = () => { const openLogs = () => {
vscode.postMessage({ vscode.postMessage({
t: 'openLogs', t: 'openLogs',
@@ -57,6 +52,8 @@ export function VariantAnalysis({
const [repoStates, setRepoStates] = useState<VariantAnalysisScannedRepositoryState[]>(initialRepoStates); const [repoStates, setRepoStates] = useState<VariantAnalysisScannedRepositoryState[]>(initialRepoStates);
const [repoResults, setRepoResults] = useState<VariantAnalysisScannedRepositoryResult[]>(initialRepoResults); const [repoResults, setRepoResults] = useState<VariantAnalysisScannedRepositoryResult[]>(initialRepoResults);
const [filterSortState, setFilterSortState] = useState<RepositoriesFilterSortState>(defaultFilterSortState);
useEffect(() => { useEffect(() => {
const listener = (evt: MessageEvent) => { const listener = (evt: MessageEvent) => {
if (evt.origin === window.origin) { if (evt.origin === window.origin) {
@@ -90,6 +87,13 @@ export function VariantAnalysis({
}; };
}, []); }, []);
const copyRepositoryList = useCallback(() => {
vscode.postMessage({
t: 'copyRepositoryList',
filterSort: filterSortState,
});
}, [filterSortState]);
if (variantAnalysis?.actionsWorkflowRunId === undefined) { if (variantAnalysis?.actionsWorkflowRunId === undefined) {
return <VariantAnalysisLoading />; return <VariantAnalysisLoading />;
} }
@@ -109,6 +113,8 @@ export function VariantAnalysis({
variantAnalysis={variantAnalysis} variantAnalysis={variantAnalysis}
repositoryStates={repoStates} repositoryStates={repoStates}
repositoryResults={repoResults} repositoryResults={repoResults}
filterSortState={filterSortState}
setFilterSortState={setFilterSortState}
/> />
</> </>
); );

View File

@@ -7,7 +7,10 @@ import {
VariantAnalysisScannedRepositoryResult, VariantAnalysisScannedRepositoryResult,
VariantAnalysisScannedRepositoryState VariantAnalysisScannedRepositoryState
} from '../../remote-queries/shared/variant-analysis'; } from '../../remote-queries/shared/variant-analysis';
import { compareWithResults, matchesFilter, RepositoriesFilterSortState } from '../../pure/variant-analysis-filter-sort'; import {
filterAndSortRepositoriesWithResults,
RepositoriesFilterSortState,
} from '../../pure/variant-analysis-filter-sort';
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
@@ -47,9 +50,7 @@ export const VariantAnalysisAnalyzedRepos = ({
}, [repositoryResults]); }, [repositoryResults]);
const repositories = useMemo(() => { const repositories = useMemo(() => {
return variantAnalysis.scannedRepos?.filter((repoTask) => { return filterAndSortRepositoriesWithResults(variantAnalysis.scannedRepos, filterSortState);
return matchesFilter(repoTask.repository, filterSortState);
})?.sort(compareWithResults(filterSortState));
}, [filterSortState, variantAnalysis.scannedRepos]); }, [filterSortState, variantAnalysis.scannedRepos]);
return ( return (

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { useState } from 'react'; import { Dispatch, SetStateAction } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { VSCodeBadge, VSCodePanels, VSCodePanelTab, VSCodePanelView } from '@vscode/webview-ui-toolkit/react'; import { VSCodeBadge, VSCodePanels, VSCodePanelTab, VSCodePanelView } from '@vscode/webview-ui-toolkit/react';
import { formatDecimal } from '../../pure/number'; import { formatDecimal } from '../../pure/number';
@@ -12,7 +12,7 @@ import {
import { VariantAnalysisAnalyzedRepos } from './VariantAnalysisAnalyzedRepos'; import { VariantAnalysisAnalyzedRepos } from './VariantAnalysisAnalyzedRepos';
import { Alert } from '../common'; import { Alert } from '../common';
import { VariantAnalysisSkippedRepositoriesTab } from './VariantAnalysisSkippedRepositoriesTab'; import { VariantAnalysisSkippedRepositoriesTab } from './VariantAnalysisSkippedRepositoriesTab';
import { defaultFilterSortState, RepositoriesFilterSortState } from '../../pure/variant-analysis-filter-sort'; import { RepositoriesFilterSortState } from '../../pure/variant-analysis-filter-sort';
import { RepositoriesSearchSortRow } from './RepositoriesSearchSortRow'; import { RepositoriesSearchSortRow } from './RepositoriesSearchSortRow';
import { FailureReasonAlert } from './FailureReasonAlert'; import { FailureReasonAlert } from './FailureReasonAlert';
@@ -20,6 +20,9 @@ export type VariantAnalysisOutcomePanelProps = {
variantAnalysis: VariantAnalysis; variantAnalysis: VariantAnalysis;
repositoryStates?: VariantAnalysisScannedRepositoryState[]; repositoryStates?: VariantAnalysisScannedRepositoryState[];
repositoryResults?: VariantAnalysisScannedRepositoryResult[]; repositoryResults?: VariantAnalysisScannedRepositoryResult[];
filterSortState: RepositoriesFilterSortState;
setFilterSortState: Dispatch<SetStateAction<RepositoriesFilterSortState>>;
}; };
const Tab = styled(VSCodePanelTab)` const Tab = styled(VSCodePanelTab)`
@@ -46,9 +49,9 @@ export const VariantAnalysisOutcomePanels = ({
variantAnalysis, variantAnalysis,
repositoryStates, repositoryStates,
repositoryResults, repositoryResults,
filterSortState,
setFilterSortState,
}: VariantAnalysisOutcomePanelProps) => { }: VariantAnalysisOutcomePanelProps) => {
const [filterSortState, setFilterSortState] = useState<RepositoriesFilterSortState>(defaultFilterSortState);
const scannedReposCount = variantAnalysis.scannedRepos?.length ?? 0; const scannedReposCount = variantAnalysis.scannedRepos?.length ?? 0;
const noCodeqlDbRepos = variantAnalysis.skippedRepos?.noCodeqlDbRepos; const noCodeqlDbRepos = variantAnalysis.skippedRepos?.noCodeqlDbRepos;
const notFoundRepos = variantAnalysis.skippedRepos?.notFoundRepos; const notFoundRepos = variantAnalysis.skippedRepos?.notFoundRepos;

View File

@@ -12,6 +12,7 @@ import {
createMockScannedRepo, createMockScannedRepo,
createMockScannedRepos createMockScannedRepos
} from '../../../vscode-tests/factories/remote-queries/shared/scanned-repositories'; } from '../../../vscode-tests/factories/remote-queries/shared/scanned-repositories';
import { defaultFilterSortState } from '../../../pure/variant-analysis-filter-sort';
describe(VariantAnalysisOutcomePanels.name, () => { describe(VariantAnalysisOutcomePanels.name, () => {
const defaultVariantAnalysis = { const defaultVariantAnalysis = {
@@ -81,6 +82,8 @@ describe(VariantAnalysisOutcomePanels.name, () => {
...defaultVariantAnalysis, ...defaultVariantAnalysis,
...variantAnalysis, ...variantAnalysis,
}} }}
filterSortState={defaultFilterSortState}
setFilterSortState={jest.fn()}
{...props} {...props}
/> />
); );

View File

@@ -4,6 +4,7 @@ import { CancellationTokenSource, commands, env, extensions, QuickPickItem, Uri,
import { CodeQLExtensionInterface } from '../../../extension'; import { CodeQLExtensionInterface } from '../../../extension';
import { logger } from '../../../logging'; import { logger } from '../../../logging';
import * as config from '../../../config'; import * as config from '../../../config';
import { setRemoteControllerRepo, setRemoteRepositoryLists } from '../../../config';
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client'; import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
import * as ghActionsApiClient from '../../../remote-queries/gh-api/gh-actions-api-client'; import * as ghActionsApiClient from '../../../remote-queries/gh-api/gh-actions-api-client';
import { Credentials } from '../../../authentication'; import { Credentials } from '../../../authentication';
@@ -35,7 +36,7 @@ import {
import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response'; import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
import { UserCancellationException } from '../../../commandRunner'; import { UserCancellationException } from '../../../commandRunner';
import { Repository } from '../../../remote-queries/gh-api/repository'; import { Repository } from '../../../remote-queries/gh-api/repository';
import { setRemoteControllerRepo, setRemoteRepositoryLists } from '../../../config'; import { defaultFilterSortState, SortKey } from '../../../pure/variant-analysis-filter-sort';
describe('Variant Analysis Manager', async function() { describe('Variant Analysis Manager', async function() {
let sandbox: sinon.SinonSandbox; let sandbox: sinon.SinonSandbox;
@@ -766,23 +767,23 @@ describe('Variant Analysis Manager', async function() {
describe('when the variant analysis has repositories with results', () => { describe('when the variant analysis has repositories with results', () => {
const scannedRepos = [ const scannedRepos = [
{ {
...createMockScannedRepo(), ...createMockScannedRepo('pear'),
resultCount: 100, resultCount: 100,
}, },
{ {
...createMockScannedRepo(), ...createMockScannedRepo('apple'),
resultCount: 0, resultCount: 0,
}, },
{ {
...createMockScannedRepo(), ...createMockScannedRepo('citrus'),
resultCount: 200, resultCount: 200,
}, },
{ {
...createMockScannedRepo(), ...createMockScannedRepo('sky'),
resultCount: undefined, resultCount: undefined,
}, },
{ {
...createMockScannedRepo(), ...createMockScannedRepo('banana'),
resultCount: 5, resultCount: 5,
}, },
]; ];
@@ -809,8 +810,44 @@ describe('Variant Analysis Manager', async function() {
expect(parsed).to.deep.eq({ expect(parsed).to.deep.eq({
'new-repo-list': [ 'new-repo-list': [
scannedRepos[0].repository.fullName, scannedRepos[4].repository.fullName,
scannedRepos[2].repository.fullName, scannedRepos[2].repository.fullName,
scannedRepos[0].repository.fullName,
],
});
});
it('should use the sort key', async () => {
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id, {
...defaultFilterSortState,
sortKey: SortKey.ResultsCount,
});
const text = writeTextStub.getCalls()[0].lastArg;
const parsed = JSON.parse('{' + text + '}');
expect(parsed).to.deep.eq({
'new-repo-list': [
scannedRepos[2].repository.fullName,
scannedRepos[0].repository.fullName,
scannedRepos[4].repository.fullName,
],
});
});
it('should use the search value', async () => {
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id, {
...defaultFilterSortState,
searchValue: 'ban',
});
const text = writeTextStub.getCalls()[0].lastArg;
const parsed = JSON.parse('{' + text + '}');
expect(parsed).to.deep.eq({
'new-repo-list': [
scannedRepos[4].repository.fullName, scannedRepos[4].repository.fullName,
], ],
}); });