Merge pull request #1705 from github/koesie10/filter-repositories-by-name

Add repository filter by full name
This commit is contained in:
Koen Vlaswinkel
2022-11-04 11:28:58 +01:00
committed by GitHub
12 changed files with 221 additions and 10 deletions

View File

@@ -5,7 +5,7 @@ import { ComponentMeta } from '@storybook/react';
import RepositoriesSearchComponent from '../../view/remote-queries/RepositoriesSearch';
export default {
title: 'Repositories Search',
title: 'MRVA/Repositories Search',
component: RepositoriesSearchComponent,
argTypes: {
filterValue: {

View File

@@ -0,0 +1,25 @@
import React, { useState } from 'react';
import { ComponentMeta } from '@storybook/react';
import { RepositoriesSearch as RepositoriesSearchComponent } from '../../view/variant-analysis/RepositoriesSearch';
export default {
title: 'Variant Analysis/Repositories Search',
component: RepositoriesSearchComponent,
argTypes: {
value: {
control: {
disable: true,
},
},
}
} as ComponentMeta<typeof RepositoriesSearchComponent>;
export const RepositoriesSearch = () => {
const [value, setValue] = useState('');
return (
<RepositoriesSearchComponent value={value} onChange={setValue} />
);
};

View File

@@ -2,6 +2,8 @@ import React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { faker } from '@faker-js/faker';
import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer';
import { VariantAnalysisAnalyzedRepos } from '../../view/variant-analysis/VariantAnalysisAnalyzedRepos';
import {
@@ -11,6 +13,7 @@ import {
import { AnalysisAlert } from '../../remote-queries/shared/analysis-result';
import { createMockVariantAnalysis } from '../../vscode-tests/factories/remote-queries/shared/variant-analysis';
import { createMockRepositoryWithMetadata } from '../../vscode-tests/factories/remote-queries/shared/repository';
import { createMockScannedRepo } from '../../vscode-tests/factories/remote-queries/shared/scanned-repositories';
import analysesResults from '../remote-queries/data/analysesResultsMessage.json';
@@ -114,5 +117,40 @@ Example.args = {
interpretedResults: interpretedResultsForRepo('expressjs/express'),
}
]
}
;
};
faker.seed(42);
const uniqueStore = {};
const manyScannedRepos = Array.from({ length: 1000 }, (_, i) => {
const mockedScannedRepo = createMockScannedRepo();
return {
...mockedScannedRepo,
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
resultCount: faker.datatype.number({ min: 0, max: 1000 }),
repository: {
...mockedScannedRepo.repository,
// We need to ensure the ID is unique for React keys
id: faker.helpers.unique(faker.datatype.number, [], {
store: uniqueStore,
}),
fullName: `octodemo/${faker.helpers.unique(faker.random.word, [], {
store: uniqueStore,
})}`,
}
};
});
export const PerformanceExample = Template.bind({});
PerformanceExample.args = {
variantAnalysis: {
...createMockVariantAnalysis(VariantAnalysisStatus.Succeeded, manyScannedRepos),
id: 1,
},
repositoryResults: manyScannedRepos.map(repoTask => ({
variantAnalysisId: 1,
repositoryId: repoTask.repository.id,
interpretedResults: interpretedResultsForRepo('facebook/create-react-app'),
}))
};

View File

@@ -6,6 +6,7 @@ type Props = {
name: string;
label: string;
className?: string;
slot?: string;
};
const CodiconIcon = styled.span`
@@ -15,5 +16,6 @@ const CodiconIcon = styled.span`
export const Codicon = ({
name,
label,
className
}: Props) => <CodiconIcon role="img" aria-label={label} className={classNames('codicon', `codicon-${name}`, className)} />;
className,
slot,
}: Props) => <CodiconIcon role="img" aria-label={label} className={classNames('codicon', `codicon-${name}`, className)} slot={slot} />;

View File

@@ -0,0 +1,32 @@
import * as React from 'react';
import { useCallback } from 'react';
import styled from 'styled-components';
import { VSCodeTextField } from '@vscode/webview-ui-toolkit/react';
import { Codicon } from '../common';
const TextField = styled(VSCodeTextField)`
width: 100%;
`;
type Props = {
value: string;
onChange: (value: string) => void;
}
export const RepositoriesSearch = ({ value, onChange }: Props) => {
const handleInput = useCallback((e: InputEvent) => {
const target = e.target as HTMLInputElement;
onChange(target.value);
}, [onChange]);
return (
<TextField
placeholder='Filter by repository owner/name'
value={value}
onInput={handleInput}
>
<Codicon name="search" label="Search..." slot="start" />
</TextField>
);
};

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import { useMemo } from 'react';
import styled from 'styled-components';
import { RepoRow } from './RepoRow';
import {
@@ -6,7 +7,7 @@ import {
VariantAnalysisScannedRepositoryResult,
VariantAnalysisScannedRepositoryState
} from '../../remote-queries/shared/variant-analysis';
import { useMemo } from 'react';
import { matchesSearchValue } from './filterSort';
const Container = styled.div`
display: flex;
@@ -19,12 +20,15 @@ export type VariantAnalysisAnalyzedReposProps = {
variantAnalysis: VariantAnalysis;
repositoryStates?: VariantAnalysisScannedRepositoryState[];
repositoryResults?: VariantAnalysisScannedRepositoryResult[];
searchValue?: string;
}
export const VariantAnalysisAnalyzedRepos = ({
variantAnalysis,
repositoryStates,
repositoryResults,
searchValue,
}: VariantAnalysisAnalyzedReposProps) => {
const repositoryStateById = useMemo(() => {
const map = new Map<number, VariantAnalysisScannedRepositoryState>();
@@ -42,9 +46,19 @@ export const VariantAnalysisAnalyzedRepos = ({
return map;
}, [repositoryResults]);
const repositories = useMemo(() => {
if (searchValue) {
return variantAnalysis.scannedRepos?.filter((repoTask) => {
return matchesSearchValue(repoTask.repository, searchValue);
});
}
return variantAnalysis.scannedRepos;
}, [searchValue, variantAnalysis.scannedRepos]);
return (
<Container>
{variantAnalysis.scannedRepos?.map(repository => {
{repositories?.map(repository => {
const state = repositoryStateById.get(repository.repository.id);
const results = repositoryResultsById.get(repository.repository.id);

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import { useState } from 'react';
import styled from 'styled-components';
import { VSCodeBadge, VSCodePanels, VSCodePanelTab, VSCodePanelView } from '@vscode/webview-ui-toolkit/react';
import { formatDecimal } from '../../pure/number';
@@ -10,6 +11,7 @@ import {
import { VariantAnalysisAnalyzedRepos } from './VariantAnalysisAnalyzedRepos';
import { Alert } from '../common';
import { VariantAnalysisSkippedRepositoriesTab } from './VariantAnalysisSkippedRepositoriesTab';
import { RepositoriesSearch } from './RepositoriesSearch';
export type VariantAnalysisOutcomePanelProps = {
variantAnalysis: VariantAnalysis;
@@ -42,6 +44,8 @@ export const VariantAnalysisOutcomePanels = ({
repositoryStates,
repositoryResults,
}: VariantAnalysisOutcomePanelProps) => {
const [searchValue, setSearchValue] = useState('');
const noCodeqlDbRepos = variantAnalysis.skippedRepos?.noCodeqlDbRepos;
const notFoundRepos = variantAnalysis.skippedRepos?.notFoundRepos;
const overLimitRepositoryCount = variantAnalysis.skippedRepos?.overLimitRepos?.repositoryCount ?? 0;
@@ -70,10 +74,12 @@ export const VariantAnalysisOutcomePanels = ({
return (
<>
{warnings}
<RepositoriesSearch value={searchValue} onChange={setSearchValue} />
<VariantAnalysisAnalyzedRepos
variantAnalysis={variantAnalysis}
repositoryStates={repositoryStates}
repositoryResults={repositoryResults}
searchValue={searchValue}
/>
</>
);
@@ -82,6 +88,7 @@ export const VariantAnalysisOutcomePanels = ({
return (
<>
{warnings}
<RepositoriesSearch value={searchValue} onChange={setSearchValue} />
<VSCodePanels>
<Tab>
Analyzed
@@ -104,6 +111,7 @@ export const VariantAnalysisOutcomePanels = ({
variantAnalysis={variantAnalysis}
repositoryStates={repositoryStates}
repositoryResults={repositoryResults}
searchValue={searchValue}
/>
</VSCodePanelView>
{notFoundRepos?.repositoryCount &&
@@ -111,14 +119,18 @@ export const VariantAnalysisOutcomePanels = ({
<VariantAnalysisSkippedRepositoriesTab
alertTitle='No access'
alertMessage='The following repositories could not be scanned because you do not have read access.'
skippedRepositoryGroup={notFoundRepos} />
skippedRepositoryGroup={notFoundRepos}
searchValue={searchValue}
/>
</VSCodePanelView>}
{noCodeqlDbRepos?.repositoryCount &&
<VSCodePanelView>
<VariantAnalysisSkippedRepositoriesTab
alertTitle='No database'
alertMessage='The following repositories could not be scanned because they do not have an available CodeQL database.'
skippedRepositoryGroup={noCodeqlDbRepos} />
skippedRepositoryGroup={noCodeqlDbRepos}
searchValue={searchValue}
/>
</VSCodePanelView>}
</VSCodePanels>
</>

View File

@@ -1,13 +1,17 @@
import * as React from 'react';
import { useMemo } from 'react';
import styled from 'styled-components';
import { VariantAnalysisSkippedRepositoryGroup } from '../../remote-queries/shared/variant-analysis';
import { Alert } from '../common';
import { RepoRow } from './RepoRow';
import { matchesSearchValue } from './filterSort';
export type VariantAnalysisSkippedRepositoriesTabProps = {
alertTitle: string,
alertMessage: string,
skippedRepositoryGroup: VariantAnalysisSkippedRepositoryGroup,
searchValue?: string,
};
function getSkipReasonAlert(
@@ -39,11 +43,22 @@ export const VariantAnalysisSkippedRepositoriesTab = ({
alertTitle,
alertMessage,
skippedRepositoryGroup,
searchValue,
}: VariantAnalysisSkippedRepositoriesTabProps) => {
const repositories = useMemo(() => {
if (searchValue) {
return skippedRepositoryGroup.repositories?.filter((repo) => {
return matchesSearchValue(repo, searchValue);
});
}
return skippedRepositoryGroup.repositories;
}, [searchValue, skippedRepositoryGroup.repositories]);
return (
<Container>
{getSkipReasonAlert(alertTitle, alertMessage, skippedRepositoryGroup)}
{skippedRepositoryGroup.repositories.map((repo) =>
{repositories.map((repo) =>
<RepoRow key={`repo/${repo.fullName}`} repository={repo} />
)}
</Container>

View File

@@ -110,4 +110,15 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
}));
expect(screen.getByText('This is an empty block.')).toBeInTheDocument();
});
it('uses the search value', () => {
render({
searchValue: 'world-2',
});
expect(screen.queryByText('octodemo/hello-world-1')).not.toBeInTheDocument();
expect(screen.getByText('octodemo/hello-world-2')).toBeInTheDocument();
expect(screen.queryByText('octodemo/hello-world-3')).not.toBeInTheDocument();
expect(screen.queryByText('octodemo/hello-world-4')).not.toBeInTheDocument();
});
});

View File

@@ -97,4 +97,30 @@ describe(VariantAnalysisSkippedRepositoriesTab.name, () => {
expect(screen.getByText('octodemo/hello-galaxy')).toBeInTheDocument();
expect(screen.getByText('octodemo/hello-universe')).toBeInTheDocument();
});
it('uses the search value', async () => {
render({
alertTitle: 'No database',
alertMessage: 'The following repositories could not be scanned because they do not have an available CodeQL database.',
skippedRepositoryGroup: {
repositoryCount: 1,
repositories: [
{
fullName: 'octodemo/hello-world',
},
{
fullName: 'octodemo/hello-galaxy',
},
{
fullName: 'octodemo/hello-universe',
},
],
},
searchValue: 'world',
});
expect(screen.getByText('octodemo/hello-world')).toBeInTheDocument();
expect(screen.queryByText('octodemo/hello-galaxy')).not.toBeInTheDocument();
expect(screen.queryByText('octodemo/hello-universe')).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,27 @@
import { matchesSearchValue } from '../filterSort';
describe(matchesSearchValue.name, () => {
const repository = {
fullName: 'github/codeql'
};
const testCases = [
{ searchValue: undefined, matches: true },
{ searchValue: '', matches: true },
{ searchValue: 'github/codeql', matches: true },
{ searchValue: 'github', matches: true },
{ searchValue: 'git', matches: true },
{ searchValue: 'codeql', matches: true },
{ searchValue: 'code', matches: true },
{ searchValue: 'ql', matches: true },
{ searchValue: '/', matches: true },
{ searchValue: 'gothub/codeql', matches: false },
{ searchValue: 'hello', matches: false },
{ searchValue: 'cod*ql', matches: false },
{ searchValue: 'cod?ql', matches: false },
];
test.each(testCases)('returns $matches if searching for $searchValue', ({ searchValue, matches }) => {
expect(matchesSearchValue(repository, searchValue)).toBe(matches);
});
});

View File

@@ -0,0 +1,9 @@
import { Repository } from '../../remote-queries/shared/repository';
export function matchesSearchValue(repo: Pick<Repository, 'fullName'>, searchValue: string | undefined): boolean {
if (!searchValue) {
return true;
}
return repo.fullName.toLowerCase().includes(searchValue.toLowerCase());
}