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 { VariantAnalysisResultsManager } from './remote-queries/variant-analysis-results-manager';
import { initializeDbModule } from './databases/db-module';
import { RepositoriesFilterSortState } from './pure/variant-analysis-filter-sort';
/**
* extension.ts
@@ -945,8 +946,8 @@ async function activateWithInstalledDistribution(
);
ctx.subscriptions.push(
commandRunner('codeQL.copyVariantAnalysisRepoList', async (variantAnalysisId: number) => {
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysisId);
commandRunner('codeQL.copyVariantAnalysisRepoList', async (variantAnalysisId: number, filterSort?: RepositoriesFilterSortState) => {
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysisId, filterSort);
})
);

View File

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

View File

@@ -71,3 +71,17 @@ export function compareWithResults(filterSortState: RepositoriesFilterSortState
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 { ProgressCallback, UserCancellationException } from '../commandRunner';
import { CodeQLCliServer } from '../cli';
import {
defaultFilterSortState,
filterAndSortRepositoriesWithResults,
RepositoriesFilterSortState,
} from '../pure/variant-analysis-filter-sort';
export class VariantAnalysisManager extends DisposableObject implements VariantAnalysisViewManager<VariantAnalysisView> {
private static readonly REPO_STATES_FILENAME = 'repo_states.json';
@@ -368,13 +373,15 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
await cancelVariantAnalysis(credentials, variantAnalysis);
}
public async copyRepoListToClipboard(variantAnalysisId: number) {
public async copyRepoListToClipboard(variantAnalysisId: number, filterSort: RepositoriesFilterSortState = defaultFilterSortState) {
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
if (!variantAnalysis) {
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) {
return;
}

View File

@@ -104,7 +104,7 @@ export class VariantAnalysisView extends AbstractWebview<ToVariantAnalysisMessag
await this.openQueryText();
break;
case 'copyRepositoryList':
void commands.executeCommand('codeQL.copyVariantAnalysisRepoList', this.variantAnalysisId);
void commands.executeCommand('codeQL.copyVariantAnalysisRepoList', this.variantAnalysisId, msg.filterSort);
break;
case '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';
@@ -8,6 +8,7 @@ import { VariantAnalysisRepoStatus, VariantAnalysisStatus } from '../../remote-q
import { createMockScannedRepo } from '../../vscode-tests/factories/remote-queries/shared/scanned-repositories';
import { createMockVariantAnalysis } from '../../vscode-tests/factories/remote-queries/shared/variant-analysis';
import { createMockRepositoryWithMetadata } from '../../vscode-tests/factories/remote-queries/shared/repository';
import { defaultFilterSortState, RepositoriesFilterSortState } from '../../pure/variant-analysis-filter-sort';
export default {
title: 'Variant Analysis/Variant Analysis Outcome Panels',
@@ -21,9 +22,13 @@ export default {
],
} as ComponentMeta<typeof VariantAnalysisOutcomePanels>;
const Template: ComponentStory<typeof VariantAnalysisOutcomePanels> = (args) => (
<VariantAnalysisOutcomePanels {...args} />
);
const Template: ComponentStory<typeof VariantAnalysisOutcomePanels> = (args) => {
const [filterSortState, setFilterSortState] = useState<RepositoriesFilterSortState>(defaultFilterSortState);
return (
<VariantAnalysisOutcomePanels {...args} filterSortState={filterSortState} setFilterSortState={setFilterSortState} />
);
};
export const WithoutSkippedRepos = Template.bind({});
WithoutSkippedRepos.args = {

View File

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

View File

@@ -7,7 +7,10 @@ import {
VariantAnalysisScannedRepositoryResult,
VariantAnalysisScannedRepositoryState
} 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`
display: flex;
@@ -47,9 +50,7 @@ export const VariantAnalysisAnalyzedRepos = ({
}, [repositoryResults]);
const repositories = useMemo(() => {
return variantAnalysis.scannedRepos?.filter((repoTask) => {
return matchesFilter(repoTask.repository, filterSortState);
})?.sort(compareWithResults(filterSortState));
return filterAndSortRepositoriesWithResults(variantAnalysis.scannedRepos, filterSortState);
}, [filterSortState, variantAnalysis.scannedRepos]);
return (

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import { CancellationTokenSource, commands, env, extensions, QuickPickItem, Uri,
import { CodeQLExtensionInterface } from '../../../extension';
import { logger } from '../../../logging';
import * as config from '../../../config';
import { setRemoteControllerRepo, setRemoteRepositoryLists } from '../../../config';
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
import * as ghActionsApiClient from '../../../remote-queries/gh-api/gh-actions-api-client';
import { Credentials } from '../../../authentication';
@@ -35,7 +36,7 @@ import {
import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
import { UserCancellationException } from '../../../commandRunner';
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() {
let sandbox: sinon.SinonSandbox;
@@ -766,23 +767,23 @@ describe('Variant Analysis Manager', async function() {
describe('when the variant analysis has repositories with results', () => {
const scannedRepos = [
{
...createMockScannedRepo(),
...createMockScannedRepo('pear'),
resultCount: 100,
},
{
...createMockScannedRepo(),
...createMockScannedRepo('apple'),
resultCount: 0,
},
{
...createMockScannedRepo(),
...createMockScannedRepo('citrus'),
resultCount: 200,
},
{
...createMockScannedRepo(),
...createMockScannedRepo('sky'),
resultCount: undefined,
},
{
...createMockScannedRepo(),
...createMockScannedRepo('banana'),
resultCount: 5,
},
];
@@ -809,8 +810,44 @@ describe('Variant Analysis Manager', async function() {
expect(parsed).to.deep.eq({
'new-repo-list': [
scannedRepos[0].repository.fullName,
scannedRepos[4].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,
],
});