Skip to content

Commit 18111ff

Browse files
committed
Add repository filter by full name
This adds a new textbox to the outcome panels that allows filtering by the repository full name (e.g. `github/vscode-codeql`). The filtering uses the same logic as the existing remote queries filter, i.e. by converting the input and the repository full name to lower case and checking the the latter includes the former.
1 parent 1487ff5 commit 18111ff

12 files changed

+221
-10
lines changed

extensions/ql-vscode/src/stories/remote-queries/RepositoriesSearch.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ComponentMeta } from '@storybook/react';
55
import RepositoriesSearchComponent from '../../view/remote-queries/RepositoriesSearch';
66

77
export default {
8-
title: 'Repositories Search',
8+
title: 'MRVA/Repositories Search',
99
component: RepositoriesSearchComponent,
1010
argTypes: {
1111
filterValue: {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React, { useState } from 'react';
2+
3+
import { ComponentMeta } from '@storybook/react';
4+
5+
import { RepositoriesSearch as RepositoriesSearchComponent } from '../../view/variant-analysis/RepositoriesSearch';
6+
7+
export default {
8+
title: 'Variant Analysis/Repositories Search',
9+
component: RepositoriesSearchComponent,
10+
argTypes: {
11+
value: {
12+
control: {
13+
disable: true,
14+
},
15+
},
16+
}
17+
} as ComponentMeta<typeof RepositoriesSearchComponent>;
18+
19+
export const RepositoriesSearch = () => {
20+
const [value, setValue] = useState('');
21+
22+
return (
23+
<RepositoriesSearchComponent value={value} onChange={setValue} />
24+
);
25+
};

extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisAnalyzedRepos.stories.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import React from 'react';
22

33
import { ComponentMeta, ComponentStory } from '@storybook/react';
44

5+
import { faker } from '@faker-js/faker';
6+
57
import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer';
68
import { VariantAnalysisAnalyzedRepos } from '../../view/variant-analysis/VariantAnalysisAnalyzedRepos';
79
import {
@@ -11,6 +13,7 @@ import {
1113
import { AnalysisAlert } from '../../remote-queries/shared/analysis-result';
1214
import { createMockVariantAnalysis } from '../../vscode-tests/factories/remote-queries/shared/variant-analysis';
1315
import { createMockRepositoryWithMetadata } from '../../vscode-tests/factories/remote-queries/shared/repository';
16+
import { createMockScannedRepo } from '../../vscode-tests/factories/remote-queries/shared/scanned-repositories';
1417

1518
import analysesResults from '../remote-queries/data/analysesResultsMessage.json';
1619

@@ -111,5 +114,40 @@ Example.args = {
111114
interpretedResults: interpretedResultsForRepo('expressjs/express'),
112115
}
113116
]
114-
}
115-
;
117+
};
118+
119+
faker.seed(42);
120+
const uniqueStore = {};
121+
122+
const manyScannedRepos = Array.from({ length: 1000 }, (_, i) => {
123+
const mockedScannedRepo = createMockScannedRepo();
124+
125+
return {
126+
...mockedScannedRepo,
127+
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
128+
resultCount: faker.datatype.number({ min: 0, max: 1000 }),
129+
repository: {
130+
...mockedScannedRepo.repository,
131+
// We need to ensure the ID is unique for React keys
132+
id: faker.helpers.unique(faker.datatype.number, [], {
133+
store: uniqueStore,
134+
}),
135+
fullName: `octodemo/${faker.helpers.unique(faker.random.word, [], {
136+
store: uniqueStore,
137+
})}`,
138+
}
139+
};
140+
});
141+
142+
export const PerformanceExample = Template.bind({});
143+
PerformanceExample.args = {
144+
variantAnalysis: {
145+
...createMockVariantAnalysis(VariantAnalysisStatus.Succeeded, manyScannedRepos),
146+
id: 1,
147+
},
148+
repositoryResults: manyScannedRepos.map(repoTask => ({
149+
variantAnalysisId: 1,
150+
repositoryId: repoTask.repository.id,
151+
interpretedResults: interpretedResultsForRepo('facebook/create-react-app'),
152+
}))
153+
};

extensions/ql-vscode/src/view/common/icon/Codicon.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ type Props = {
66
name: string;
77
label: string;
88
className?: string;
9+
slot?: string;
910
};
1011

1112
const CodiconIcon = styled.span`
@@ -15,5 +16,6 @@ const CodiconIcon = styled.span`
1516
export const Codicon = ({
1617
name,
1718
label,
18-
className
19-
}: Props) => <CodiconIcon role="img" aria-label={label} className={classNames('codicon', `codicon-${name}`, className)} />;
19+
className,
20+
slot,
21+
}: Props) => <CodiconIcon role="img" aria-label={label} className={classNames('codicon', `codicon-${name}`, className)} slot={slot} />;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as React from 'react';
2+
import { useCallback } from 'react';
3+
import styled from 'styled-components';
4+
import { VSCodeTextField } from '@vscode/webview-ui-toolkit/react';
5+
import { Codicon } from '../common';
6+
7+
const TextField = styled(VSCodeTextField)`
8+
width: 100%;
9+
`;
10+
11+
type Props = {
12+
value: string;
13+
onChange: (value: string) => void;
14+
}
15+
16+
export const RepositoriesSearch = ({ value, onChange }: Props) => {
17+
const handleInput = useCallback((e: InputEvent) => {
18+
const target = e.target as HTMLInputElement;
19+
20+
onChange(target.value);
21+
}, [onChange]);
22+
23+
return (
24+
<TextField
25+
placeholder='Filter by repository owner/name'
26+
value={value}
27+
onInput={handleInput}
28+
>
29+
<Codicon name="search" label="Search..." slot="start" />
30+
</TextField>
31+
);
32+
};

extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisAnalyzedRepos.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import * as React from 'react';
2+
import { useMemo } from 'react';
23
import styled from 'styled-components';
34
import { RepoRow } from './RepoRow';
45
import {
56
VariantAnalysis,
67
VariantAnalysisScannedRepositoryResult,
78
VariantAnalysisScannedRepositoryState
89
} from '../../remote-queries/shared/variant-analysis';
9-
import { useMemo } from 'react';
10+
import { matchesSearchValue } from './filterSort';
1011

1112
const Container = styled.div`
1213
display: flex;
@@ -19,12 +20,15 @@ export type VariantAnalysisAnalyzedReposProps = {
1920
variantAnalysis: VariantAnalysis;
2021
repositoryStates?: VariantAnalysisScannedRepositoryState[];
2122
repositoryResults?: VariantAnalysisScannedRepositoryResult[];
23+
24+
searchValue?: string;
2225
}
2326

2427
export const VariantAnalysisAnalyzedRepos = ({
2528
variantAnalysis,
2629
repositoryStates,
2730
repositoryResults,
31+
searchValue,
2832
}: VariantAnalysisAnalyzedReposProps) => {
2933
const repositoryStateById = useMemo(() => {
3034
const map = new Map<number, VariantAnalysisScannedRepositoryState>();
@@ -42,9 +46,19 @@ export const VariantAnalysisAnalyzedRepos = ({
4246
return map;
4347
}, [repositoryResults]);
4448

49+
const repositories = useMemo(() => {
50+
if (searchValue) {
51+
return variantAnalysis.scannedRepos?.filter((repoTask) => {
52+
return matchesSearchValue(repoTask.repository, searchValue);
53+
});
54+
}
55+
56+
return variantAnalysis.scannedRepos;
57+
}, [searchValue, variantAnalysis.scannedRepos]);
58+
4559
return (
4660
<Container>
47-
{variantAnalysis.scannedRepos?.map(repository => {
61+
{repositories?.map(repository => {
4862
const state = repositoryStateById.get(repository.repository.id);
4963
const results = repositoryResultsById.get(repository.repository.id);
5064

extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisOutcomePanels.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as React from 'react';
2+
import { useState } from 'react';
23
import styled from 'styled-components';
34
import { VSCodeBadge, VSCodePanels, VSCodePanelTab, VSCodePanelView } from '@vscode/webview-ui-toolkit/react';
45
import { formatDecimal } from '../../pure/number';
@@ -10,6 +11,7 @@ import {
1011
import { VariantAnalysisAnalyzedRepos } from './VariantAnalysisAnalyzedRepos';
1112
import { Alert } from '../common';
1213
import { VariantAnalysisSkippedRepositoriesTab } from './VariantAnalysisSkippedRepositoriesTab';
14+
import { RepositoriesSearch } from './RepositoriesSearch';
1315

1416
export type VariantAnalysisOutcomePanelProps = {
1517
variantAnalysis: VariantAnalysis;
@@ -42,6 +44,8 @@ export const VariantAnalysisOutcomePanels = ({
4244
repositoryStates,
4345
repositoryResults,
4446
}: VariantAnalysisOutcomePanelProps) => {
47+
const [searchValue, setSearchValue] = useState('');
48+
4549
const noCodeqlDbRepos = variantAnalysis.skippedRepos?.noCodeqlDbRepos;
4650
const notFoundRepos = variantAnalysis.skippedRepos?.notFoundRepos;
4751
const overLimitRepositoryCount = variantAnalysis.skippedRepos?.overLimitRepos?.repositoryCount ?? 0;
@@ -70,10 +74,12 @@ export const VariantAnalysisOutcomePanels = ({
7074
return (
7175
<>
7276
{warnings}
77+
<RepositoriesSearch value={searchValue} onChange={setSearchValue} />
7378
<VariantAnalysisAnalyzedRepos
7479
variantAnalysis={variantAnalysis}
7580
repositoryStates={repositoryStates}
7681
repositoryResults={repositoryResults}
82+
searchValue={searchValue}
7783
/>
7884
</>
7985
);
@@ -82,6 +88,7 @@ export const VariantAnalysisOutcomePanels = ({
8288
return (
8389
<>
8490
{warnings}
91+
<RepositoriesSearch value={searchValue} onChange={setSearchValue} />
8592
<VSCodePanels>
8693
<Tab>
8794
Analyzed
@@ -104,21 +111,26 @@ export const VariantAnalysisOutcomePanels = ({
104111
variantAnalysis={variantAnalysis}
105112
repositoryStates={repositoryStates}
106113
repositoryResults={repositoryResults}
114+
searchValue={searchValue}
107115
/>
108116
</VSCodePanelView>
109117
{notFoundRepos?.repositoryCount &&
110118
<VSCodePanelView>
111119
<VariantAnalysisSkippedRepositoriesTab
112120
alertTitle='No access'
113121
alertMessage='The following repositories could not be scanned because you do not have read access.'
114-
skippedRepositoryGroup={notFoundRepos} />
122+
skippedRepositoryGroup={notFoundRepos}
123+
searchValue={searchValue}
124+
/>
115125
</VSCodePanelView>}
116126
{noCodeqlDbRepos?.repositoryCount &&
117127
<VSCodePanelView>
118128
<VariantAnalysisSkippedRepositoriesTab
119129
alertTitle='No database'
120130
alertMessage='The following repositories could not be scanned because they do not have an available CodeQL database.'
121-
skippedRepositoryGroup={noCodeqlDbRepos} />
131+
skippedRepositoryGroup={noCodeqlDbRepos}
132+
searchValue={searchValue}
133+
/>
122134
</VSCodePanelView>}
123135
</VSCodePanels>
124136
</>

extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisSkippedRepositoriesTab.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import * as React from 'react';
2+
import { useMemo } from 'react';
23
import styled from 'styled-components';
34
import { VariantAnalysisSkippedRepositoryGroup } from '../../remote-queries/shared/variant-analysis';
45
import { Alert } from '../common';
56
import { RepoRow } from './RepoRow';
7+
import { matchesSearchValue } from './filterSort';
68

79
export type VariantAnalysisSkippedRepositoriesTabProps = {
810
alertTitle: string,
911
alertMessage: string,
1012
skippedRepositoryGroup: VariantAnalysisSkippedRepositoryGroup,
13+
14+
searchValue?: string,
1115
};
1216

1317
function getSkipReasonAlert(
@@ -39,11 +43,22 @@ export const VariantAnalysisSkippedRepositoriesTab = ({
3943
alertTitle,
4044
alertMessage,
4145
skippedRepositoryGroup,
46+
searchValue,
4247
}: VariantAnalysisSkippedRepositoriesTabProps) => {
48+
const repositories = useMemo(() => {
49+
if (searchValue) {
50+
return skippedRepositoryGroup.repositories?.filter((repo) => {
51+
return matchesSearchValue(repo, searchValue);
52+
});
53+
}
54+
55+
return skippedRepositoryGroup.repositories;
56+
}, [searchValue, skippedRepositoryGroup.repositories]);
57+
4358
return (
4459
<Container>
4560
{getSkipReasonAlert(alertTitle, alertMessage, skippedRepositoryGroup)}
46-
{skippedRepositoryGroup.repositories.map((repo) =>
61+
{repositories.map((repo) =>
4762
<RepoRow key={`repo/${repo.fullName}`} repository={repo} />
4863
)}
4964
</Container>

extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisAnalyzedRepos.spec.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,15 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
107107
}));
108108
expect(screen.getByText('This is an empty block.')).toBeInTheDocument();
109109
});
110+
111+
it('uses the search value', () => {
112+
render({
113+
searchValue: 'world-2',
114+
});
115+
116+
expect(screen.queryByText('octodemo/hello-world-1')).not.toBeInTheDocument();
117+
expect(screen.getByText('octodemo/hello-world-2')).toBeInTheDocument();
118+
expect(screen.queryByText('octodemo/hello-world-3')).not.toBeInTheDocument();
119+
expect(screen.queryByText('octodemo/hello-world-4')).not.toBeInTheDocument();
120+
});
110121
});

extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisSkippedRepositoriesTab.spec.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,30 @@ describe(VariantAnalysisSkippedRepositoriesTab.name, () => {
9797
expect(screen.getByText('octodemo/hello-galaxy')).toBeInTheDocument();
9898
expect(screen.getByText('octodemo/hello-universe')).toBeInTheDocument();
9999
});
100+
101+
it('uses the search value', async () => {
102+
render({
103+
alertTitle: 'No database',
104+
alertMessage: 'The following repositories could not be scanned because they do not have an available CodeQL database.',
105+
skippedRepositoryGroup: {
106+
repositoryCount: 1,
107+
repositories: [
108+
{
109+
fullName: 'octodemo/hello-world',
110+
},
111+
{
112+
fullName: 'octodemo/hello-galaxy',
113+
},
114+
{
115+
fullName: 'octodemo/hello-universe',
116+
},
117+
],
118+
},
119+
searchValue: 'world',
120+
});
121+
122+
expect(screen.getByText('octodemo/hello-world')).toBeInTheDocument();
123+
expect(screen.queryByText('octodemo/hello-galaxy')).not.toBeInTheDocument();
124+
expect(screen.queryByText('octodemo/hello-universe')).not.toBeInTheDocument();
125+
});
100126
});

0 commit comments

Comments
 (0)