Merge pull request #1705 from github/koesie10/filter-repositories-by-name
Add repository filter by full name
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
};
|
||||
@@ -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'),
|
||||
}))
|
||||
};
|
||||
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
}
|
||||
Reference in New Issue
Block a user