);
}
+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[] }> {
* 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';
patIsValid?: boolean;
projects?: AzureProject[];
repositories: T.Dict<AzureRepository[]>;
+ searching?: boolean;
+ searchResults?: T.Dict<AzureRepository[]>;
settings?: AlmSettingsInstance;
submittingToken?: boolean;
tokenValidationFailed: boolean;
};
handleOpenProject = async (projectKey: string) => {
+ if (this.state.searchResults) {
+ return;
+ }
+
this.setState(({ loadingRepositories }) => ({
loadingRepositories: { ...loadingRepositories, [projectKey]: true }
}));
}));
};
+ 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;
patIsValid,
projects,
repositories,
+ searching,
+ searchResults,
settings,
submittingToken,
tokenValidationFailed
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}
* 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';
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;
loadingRepositories,
projects,
repositories,
+ searching,
+ searchResults,
showPersonalAccessTokenForm,
settings,
submittingToken,
/>
</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>
+ </>
))}
</>
);
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>
);
checkPersonalAccessTokenIsValid,
getAzureProjects,
getAzureRepositories,
+ searchAzureRepositories,
setAlmPersonalAccessToken
} from '../../../../api/alm-integrations';
import { mockAzureProject, mockAzureRepository } from '../../../../helpers/mocks/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: [] })
};
});
});
});
+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
loadingRepositories={{}}
onOpenProject={jest.fn()}
onPersonalAccessTokenCreate={jest.fn()}
+ onSearch={jest.fn()}
projects={[project]}
repositories={{ [project.key]: [mockAzureRepository()] }}
tokenValidationFailed={false}
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';
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)
loadingRepositories={Object {}}
onOpenProject={[Function]}
onPersonalAccessTokenCreate={[Function]}
+ onSearch={[Function]}
repositories={Object {}}
settings={
Object {
</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>
`;
/>
</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>
+`;
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