Browse Source

SONAR-14057 Enable Search for Azure Repositories

tags/8.6.0.39681
Jeremy Davis 3 years ago
parent
commit
090a91d83a

+ 9
- 0
server/sonar-web/src/main/js/api/alm-integrations.ts View File

@@ -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[] }> {

+ 36
- 0
server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx View File

@@ -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<AzureRepository[]>;
searching?: boolean;
searchResults?: T.Dict<AzureRepository[]>;
settings?: AlmSettingsInstance;
submittingToken?: boolean;
tokenValidationFailed: boolean;
@@ -152,6 +156,10 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State
};

handleOpenProject = async (projectKey: string) => {
if (this.state.searchResults) {
return;
}

this.setState(({ loadingRepositories }) => ({
loadingRepositories: { ...loadingRepositories, [projectKey]: true }
}));
@@ -164,6 +172,29 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State
}));
};

handleSearchRepositories = async (searchQuery: string) => {
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<Props, State
patIsValid,
projects,
repositories,
searching,
searchResults,
settings,
submittingToken,
tokenValidationFailed
@@ -222,8 +255,11 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State
loadingRepositories={loadingRepositories}
onOpenProject={this.handleOpenProject}
onPersonalAccessTokenCreate={this.handlePersonalAccessTokenCreate}
onSearch={this.handleSearchRepositories}
projects={projects}
repositories={repositories}
searching={searching}
searchResults={searchResults}
settings={settings}
showPersonalAccessTokenForm={!patIsValid || Boolean(location.query.resetPat)}
submittingToken={submittingToken}

+ 24
- 6
server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx View File

@@ -18,6 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import SearchBox from 'sonar-ui-common/components/controls/SearchBox';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
import { AzureProject, AzureRepository } from '../../../types/alm-integration';
@@ -33,8 +35,11 @@ export interface AzureProjectCreateRendererProps {
loadingRepositories: T.Dict<boolean>;
onOpenProject: (key: string) => void;
onPersonalAccessTokenCreate: (token: string) => void;
onSearch: (query: string) => void;
projects?: AzureProject[];
repositories: T.Dict<AzureRepository[]>;
searching?: boolean;
searchResults?: T.Dict<AzureRepository[]>;
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
/>
</div>
) : (
<AzureProjectsList
loadingRepositories={loadingRepositories}
onOpenProject={props.onOpenProject}
projects={projects}
repositories={repositories}
/>
<>
<div className="huge-spacer-bottom">
<SearchBox
onChange={props.onSearch}
placeholder={translate('onboarding.create_project.search_repositories_by_name')}
/>
</div>
<DeferredSpinner loading={Boolean(searching)}>
<AzureProjectsList
loadingRepositories={loadingRepositories}
onOpenProject={props.onOpenProject}
projects={projects}
repositories={repositories}
searchResults={searchResults}
/>
</DeferredSpinner>
</>
))}
</>
);

+ 37
- 24
server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx View File

@@ -32,56 +32,69 @@ export interface AzureProjectsListProps {
onOpenProject: (key: string) => void;
projects?: AzureProject[];
repositories: T.Dict<AzureRepository[]>;
searchResults?: T.Dict<AzureRepository[]>;
}

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 (
<Alert className="spacer-top" variant="warning">
<FormattedMessage
defaultMessage={translate('onboarding.create_project.azure.no_projects')}
id="onboarding.create_project.azure.no_projects"
values={{
link: (
<Link
to={{
pathname: '/projects/create',
query: { mode: CreateProjectModes.AzureDevOps, resetPat: 1 }
}}>
{translate('onboarding.create_project.update_your_token')}
</Link>
)
}}
/>
{searchResults ? (
translate('onboarding.create_project.azure.no_results')
) : (
<FormattedMessage
defaultMessage={translate('onboarding.create_project.azure.no_projects')}
id="onboarding.create_project.azure.no_projects"
values={{
link: (
<Link
to={{
pathname: '/projects/create',
query: { mode: CreateProjectModes.AzureDevOps, resetPat: 1 }
}}>
{translate('onboarding.create_project.update_your_token')}
</Link>
)
}}
/>
)}
</Alert>
);
}

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 (
<div>
{filteredProjects.map((p, i) => (
{displayedProjects.map((p, i) => (
<AzureProjectAccordion
key={p.key}
key={`${p.key}${keySuffix}`}
loading={Boolean(loadingRepositories[p.key])}
onOpen={props.onOpenProject}
project={p}
repositories={repositories[p.key]}
startsOpen={i === 0}
repositories={searchResults ? searchResults[p.key] : repositories[p.key]}
startsOpen={searchResults !== undefined || i === 0}
/>
))}

<ListFooter
count={filteredProjects.length}
count={displayedProjects.length}
loadMore={() => setPage(p => p + 1)}
total={projects.length}
total={filteredProjects.length}
/>
</div>
);

+ 33
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx View File

@@ -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<AzureProjectCreate['props']> = {}) {
return shallow<AzureProjectCreate>(
<AzureProjectCreate

+ 1
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx View File

@@ -44,6 +44,7 @@ function shallowRender(overrides: Partial<AzureProjectCreateRendererProps>) {
loadingRepositories={{}}
onOpenProject={jest.fn()}
onPersonalAccessTokenCreate={jest.fn()}
onSearch={jest.fn()}
projects={[project]}
repositories={{ [project.key]: [mockAzureRepository()] }}
tokenValidationFailed={false}

+ 14
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectsList-test.tsx View File

@@ -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)

+ 1
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap View File

@@ -7,6 +7,7 @@ exports[`should render correctly 1`] = `
loadingRepositories={Object {}}
onOpenProject={[Function]}
onPersonalAccessTokenCreate={[Function]}
onSearch={[Function]}
repositories={Object {}}
settings={
Object {

+ 31
- 19
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap View File

@@ -64,28 +64,40 @@ exports[`should render correctly: project list 1`] = `
</span>
}
/>
<AzureProjectsList
loadingRepositories={Object {}}
onOpenProject={[MockFunction]}
projects={
Array [
Object {
"key": "azure-project-1",
"name": "Azure Project",
},
]
}
repositories={
Object {
"azure-project-1": Array [
<div
className="huge-spacer-bottom"
>
<SearchBox
onChange={[MockFunction]}
placeholder="onboarding.create_project.search_repositories_by_name"
/>
</div>
<DeferredSpinner
loading={false}
>
<AzureProjectsList
loadingRepositories={Object {}}
onOpenProject={[MockFunction]}
projects={
Array [
Object {
"name": "Azure repo 1",
"projectName": "Azure Project",
"key": "azure-project-1",
"name": "Azure Project",
},
],
]
}
}
/>
repositories={
Object {
"azure-project-1": Array [
Object {
"name": "Azure repo 1",
"projectName": "Azure Project",
},
],
}
}
/>
</DeferredSpinner>
</Fragment>
`;


+ 39
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectsList-test.tsx.snap View File

@@ -53,3 +53,42 @@ exports[`should render correctly: empty 1`] = `
/>
</Alert>
`;

exports[`should render search results correctly: default 1`] = `
<div>
<AzureProjectAccordion
key="p2 - result"
loading={false}
onOpen={[MockFunction]}
project={
Object {
"key": "p2",
"name": "p2",
}
}
repositories={
Array [
Object {
"name": "Azure repo 1",
"projectName": "p2",
},
]
}
startsOpen={true}
/>
<ListFooter
count={1}
loadMore={[Function]}
total={1}
/>
</div>
`;

exports[`should render search results correctly: empty 1`] = `
<Alert
className="spacer-top"
variant="warning"
>
onboarding.create_project.azure.no_results
</Alert>
`;

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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

Loading…
Cancel
Save