From: Jeremy Davis Date: Tue, 17 Nov 2020 10:31:48 +0000 (+0100) Subject: SONAR-14057 Enable Search for Azure Repositories X-Git-Tag: 8.6.0.39681~71 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=090a91d83a82a9d411f70c5677fae40ae8c36bcd;p=sonarqube.git SONAR-14057 Enable Search for Azure Repositories --- diff --git a/server/sonar-web/src/main/js/api/alm-integrations.ts b/server/sonar-web/src/main/js/api/alm-integrations.ts index 3824f97bb2c..7aed2350b28 100644 --- a/server/sonar-web/src/main/js/api/alm-integrations.ts +++ b/server/sonar-web/src/main/js/api/alm-integrations.ts @@ -61,6 +61,15 @@ export function getAzureRepositories( ); } +export function searchAzureRepositories( + almSetting: string, + repositoryName: string +): Promise<{ repositories: AzureRepository[] }> { + return getJSON('/api/alm_integrations/search_azure_repos', { almSetting, repositoryName }).catch( + throwGlobalError + ); +} + export function getBitbucketServerProjects( almSetting: string ): Promise<{ projects: BitbucketProject[] }> { diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx index 7cd34c6a42a..50decd8e76d 100644 --- a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx @@ -17,12 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { groupBy } from 'lodash'; import * as React from 'react'; import { WithRouterProps } from 'react-router'; import { checkPersonalAccessTokenIsValid, getAzureProjects, getAzureRepositories, + searchAzureRepositories, setAlmPersonalAccessToken } from '../../../api/alm-integrations'; import { AzureProject, AzureRepository } from '../../../types/alm-integration'; @@ -42,6 +44,8 @@ interface State { patIsValid?: boolean; projects?: AzureProject[]; repositories: T.Dict; + searching?: boolean; + searchResults?: T.Dict; settings?: AlmSettingsInstance; submittingToken?: boolean; tokenValidationFailed: boolean; @@ -152,6 +156,10 @@ export default class AzureProjectCreate extends React.PureComponent { + if (this.state.searchResults) { + return; + } + this.setState(({ loadingRepositories }) => ({ loadingRepositories: { ...loadingRepositories, [projectKey]: true } })); @@ -164,6 +172,29 @@ export default class AzureProjectCreate extends React.PureComponent { + const { settings } = this.state; + + if (!settings) { + return; + } + + if (searchQuery.length === 0) { + this.setState({ searchResults: undefined }); + return; + } + + this.setState({ searching: true }); + + const results: AzureRepository[] = await searchAzureRepositories(settings.key, searchQuery) + .then(({ repositories }) => repositories) + .catch(() => []); + + if (this.mounted) { + this.setState({ searching: false, searchResults: groupBy(results, 'projectName') }); + } + }; + checkPersonalAccessToken = () => { const { settings } = this.state; @@ -210,6 +241,8 @@ export default class AzureProjectCreate extends React.PureComponent; onOpenProject: (key: string) => void; onPersonalAccessTokenCreate: (token: string) => void; + onSearch: (query: string) => void; projects?: AzureProject[]; repositories: T.Dict; + searching?: boolean; + searchResults?: T.Dict; settings?: AlmSettingsInstance; showPersonalAccessTokenForm?: boolean; submittingToken?: boolean; @@ -48,6 +53,8 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend loadingRepositories, projects, repositories, + searching, + searchResults, showPersonalAccessTokenForm, settings, submittingToken, @@ -88,12 +95,23 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend /> ) : ( - + <> +
+ +
+ + + + ))} ); diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx index f61257b5711..344e3e204f0 100644 --- a/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx @@ -32,56 +32,69 @@ export interface AzureProjectsListProps { onOpenProject: (key: string) => void; projects?: AzureProject[]; repositories: T.Dict; + searchResults?: T.Dict; } const PAGE_SIZE = 10; export default function AzureProjectsList(props: AzureProjectsListProps) { - const { loadingRepositories, projects = [], repositories } = props; + const { loadingRepositories, projects = [], repositories, searchResults } = props; const [page, setPage] = React.useState(1); - if (projects.length === 0) { + const filteredProjects = searchResults + ? projects.filter(p => searchResults[p.key] !== undefined) + : projects; + + if (filteredProjects.length === 0) { return ( - - {translate('onboarding.create_project.update_your_token')} - - ) - }} - /> + {searchResults ? ( + translate('onboarding.create_project.azure.no_results') + ) : ( + + {translate('onboarding.create_project.update_your_token')} + + ) + }} + /> + )} ); } - const filteredProjects = projects.slice(0, page * PAGE_SIZE); + const displayedProjects = filteredProjects.slice(0, page * PAGE_SIZE); + + // Add a suffix to the key to force react to not reuse AzureProjectAccordions between + // search results and project exploration + const keySuffix = searchResults ? ' - result' : ''; return (
- {filteredProjects.map((p, i) => ( + {displayedProjects.map((p, i) => ( ))} setPage(p => p + 1)} - total={projects.length} + total={filteredProjects.length} />
); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx index c40406179e0..c67d4dcc691 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx @@ -25,6 +25,7 @@ import { checkPersonalAccessTokenIsValid, getAzureProjects, getAzureRepositories, + searchAzureRepositories, setAlmPersonalAccessToken } from '../../../../api/alm-integrations'; import { mockAzureProject, mockAzureRepository } from '../../../../helpers/mocks/alm-integrations'; @@ -38,7 +39,8 @@ jest.mock('../../../../api/alm-integrations', () => { checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue(true), setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null), getAzureProjects: jest.fn().mockResolvedValue({ projects: [] }), - getAzureRepositories: jest.fn().mockResolvedValue({ repositories: [] }) + getAzureRepositories: jest.fn().mockResolvedValue({ repositories: [] }), + searchAzureRepositories: jest.fn().mockResolvedValue({ repositories: [] }) }; }); @@ -137,6 +139,36 @@ it('should handle opening a project', async () => { }); }); +it('should handle searching for repositories', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + const query = 'repo'; + const repositories = [mockAzureRepository({ projectName: 'p2' })]; + (searchAzureRepositories as jest.Mock).mockResolvedValueOnce({ + repositories + }); + wrapper.instance().handleSearchRepositories(query); + expect(wrapper.state().searching).toBe(true); + + expect(searchAzureRepositories).toBeCalledWith('foo', query); + await waitAndUpdate(wrapper); + expect(wrapper.state().searching).toBe(false); + expect(wrapper.state().searchResults).toEqual({ [repositories[0].projectName]: repositories }); + + // Ignore opening a project when search results are displayed + (getAzureRepositories as jest.Mock).mockClear(); + wrapper.instance().handleOpenProject('whatever'); + expect(getAzureRepositories).not.toHaveBeenCalled(); + + // and reset the search field + (searchAzureRepositories as jest.Mock).mockClear(); + + wrapper.instance().handleSearchRepositories(''); + expect(searchAzureRepositories).not.toBeCalled(); + expect(wrapper.state().searchResults).toBeUndefined(); +}); + function shallowRender(overrides: Partial = {}) { return shallow( ) { loadingRepositories={{}} onOpenProject={jest.fn()} onPersonalAccessTokenCreate={jest.fn()} + onSearch={jest.fn()} projects={[project]} repositories={{ [project.key]: [mockAzureRepository()] }} tokenValidationFailed={false} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectsList-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectsList-test.tsx index 7ee191fc84a..ec8d2f2629e 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectsList-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectsList-test.tsx @@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; -import { mockAzureProject } from '../../../../helpers/mocks/alm-integrations'; +import { mockAzureProject, mockAzureRepository } from '../../../../helpers/mocks/alm-integrations'; import AzureProjectAccordion from '../AzureProjectAccordion'; import AzureProjectsList, { AzureProjectsListProps } from '../AzureProjectsList'; @@ -30,6 +30,19 @@ it('should render correctly', () => { expect(shallowRender({ projects: [] })).toMatchSnapshot('empty'); }); +it('should render search results correctly', () => { + const projects = [ + mockAzureProject({ key: 'p1', name: 'p1' }), + mockAzureProject({ key: 'p2', name: 'p2' }), + mockAzureProject({ key: 'p3', name: 'p3' }) + ]; + const searchResults = { + p2: [mockAzureRepository({ projectName: 'p2' })] + }; + expect(shallowRender({ searchResults, projects })).toMatchSnapshot('default'); + expect(shallowRender({ searchResults: {}, projects })).toMatchSnapshot('empty'); +}); + it('should handle pagination', () => { const projects = new Array(21) .fill(1) diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap index 40a022b1e4c..d9116ea8b5d 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap @@ -7,6 +7,7 @@ exports[`should render correctly 1`] = ` loadingRepositories={Object {}} onOpenProject={[Function]} onPersonalAccessTokenCreate={[Function]} + onSearch={[Function]} repositories={Object {}} settings={ Object { diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap index dee9ad781a1..4d6058f355a 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap @@ -64,28 +64,40 @@ exports[`should render correctly: project list 1`] = ` } /> - + + + + + repositories={ + Object { + "azure-project-1": Array [ + Object { + "name": "Azure repo 1", + "projectName": "Azure Project", + }, + ], + } + } + /> + `; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectsList-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectsList-test.tsx.snap index 8711c8e646b..28b54f922c4 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectsList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectsList-test.tsx.snap @@ -53,3 +53,42 @@ exports[`should render correctly: empty 1`] = ` /> `; + +exports[`should render search results correctly: default 1`] = ` +
+ + +
+`; + +exports[`should render search results correctly: empty 1`] = ` + + onboarding.create_project.azure.no_results + +`; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 36cda900b13..2bf94b588ac 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3289,6 +3289,7 @@ onboarding.create_project.go_to_project=Go to project onboarding.create_project.azure.title=Which Azure DevOps Server repository do you want to set up? onboarding.create_project.azure.no_projects=No projects could be fetched from Azure DevOps Server. Contact your system administrator, or {link}. onboarding.create_project.azure.no_repositories=Could not fetch repositories for this project. Contact your system administrator, or {link}. +onboarding.create_project.azure.no_results=No repositories match your search query. onboarding.create_project.github.title=Which GitHub repository do you want to set up? onboarding.create_project.github.choose_organization=Choose organization onboarding.create_project.github.warning.title=Could not connect to GitHub