]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13900 Display BBS repo search results even if project is unknown
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Fri, 25 Sep 2020 11:43:24 +0000 (13:43 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 28 Sep 2020 20:07:23 +0000 (20:07 +0000)
server/sonar-web/src/main/js/apps/create/project/BitbucketProjectAccordion.tsx
server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/BitbucketSearchResults.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectAccordion-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketSearchResults-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketSearchResults-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/constants.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 9d9ce31adf4d23377bf0326cc6ff96d861b10ce0..a8cca95a4ca995581703e0171c5cd8d6babe1d0a 100644 (file)
@@ -36,7 +36,7 @@ export interface BitbucketProjectAccordionProps {
   onClick?: () => void;
   onSelectRepository: (repo: BitbucketRepository) => void;
   open: boolean;
-  project: BitbucketProject;
+  project?: BitbucketProject;
   repositories: BitbucketRepository[];
   selectedRepository?: BitbucketRepository;
   showingAllRepositories: boolean;
@@ -54,6 +54,8 @@ export default function BitbucketProjectAccordion(props: BitbucketProjectAccordi
 
   const repositoryCount = repositories.length;
 
+  const title = project?.name ?? translate('search_results');
+
   return (
     <BoxedGroupAccordion
       className={classNames('big-spacer-bottom', {
@@ -61,7 +63,6 @@ export default function BitbucketProjectAccordion(props: BitbucketProjectAccordi
         'not-clickable': !props.onClick,
         'no-hover': !props.onClick
       })}
-      key={project.key}
       onClick={
         props.onClick
           ? props.onClick
@@ -70,64 +71,66 @@ export default function BitbucketProjectAccordion(props: BitbucketProjectAccordi
             }
       }
       open={open}
-      title={<h3>{project.name}</h3>}>
+      title={<h3>{title}</h3>}>
       {open && (
-        <div className="display-flex-wrap">
-          {repositoryCount === 0 && (
-            <Alert variant="warning">
-              <FormattedMessage
-                defaultMessage={translate('onboarding.create_project.no_bbs_repos')}
-                id="onboarding.create_project.no_bbs_repos"
-                values={{
-                  link: (
-                    <Link
-                      to={{
-                        pathname: '/projects/create',
-                        query: { mode: CreateProjectModes.BitbucketServer, resetPat: 1 }
-                      }}>
-                      {translate('onboarding.create_project.update_your_token')}
-                    </Link>
-                  )
-                }}
-              />
-            </Alert>
-          )}
+        <>
+          <div className="display-flex-wrap">
+            {repositoryCount === 0 && (
+              <Alert variant="warning">
+                <FormattedMessage
+                  defaultMessage={translate('onboarding.create_project.no_bbs_repos')}
+                  id="onboarding.create_project.no_bbs_repos"
+                  values={{
+                    link: (
+                      <Link
+                        to={{
+                          pathname: '/projects/create',
+                          query: { mode: CreateProjectModes.BitbucketServer, resetPat: 1 }
+                        }}>
+                        {translate('onboarding.create_project.update_your_token')}
+                      </Link>
+                    )
+                  }}
+                />
+              </Alert>
+            )}
 
-          {repositories.map(repo =>
-            repo.sqProjectKey ? (
-              <div
-                className="display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo"
-                key={repo.id}>
-                <CheckIcon className="spacer-right" fill={colors.green} size={14} />
-                <div className="overflow-hidden">
-                  <div className="little-spacer-bottom text-ellipsis">
-                    <strong title={repo.name}>
-                      <Link to={getProjectUrl(repo.sqProjectKey)}>{repo.name}</Link>
-                    </strong>
+            {repositories.map(repo =>
+              repo.sqProjectKey ? (
+                <div
+                  className="display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo"
+                  key={repo.id}>
+                  <CheckIcon className="spacer-right" fill={colors.green} size={14} />
+                  <div className="overflow-hidden">
+                    <div className="little-spacer-bottom text-ellipsis">
+                      <strong title={repo.name}>
+                        <Link to={getProjectUrl(repo.sqProjectKey)}>{repo.name}</Link>
+                      </strong>
+                    </div>
+                    <em>{translate('onboarding.create_project.repository_imported')}</em>
                   </div>
-                  <em>{translate('onboarding.create_project.repository_imported')}</em>
                 </div>
-              </div>
-            ) : (
-              <Radio
-                checked={selectedRepository?.id === repo.id}
-                className={classNames(
-                  'display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo overflow-hidden',
-                  {
-                    disabled: disableRepositories,
-                    'text-muted': disableRepositories,
-                    'link-no-underline': disableRepositories
-                  }
-                )}
-                key={repo.id}
-                onCheck={() => props.onSelectRepository(repo)}
-                value={String(repo.id)}>
-                <strong className="text-ellipsis" title={repo.name}>
-                  {repo.name}
-                </strong>
-              </Radio>
-            )
-          )}
+              ) : (
+                <Radio
+                  checked={selectedRepository?.id === repo.id}
+                  className={classNames(
+                    'display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo overflow-hidden',
+                    {
+                      disabled: disableRepositories,
+                      'text-muted': disableRepositories,
+                      'link-no-underline': disableRepositories
+                    }
+                  )}
+                  key={repo.id}
+                  onCheck={() => props.onSelectRepository(repo)}
+                  value={String(repo.id)}>
+                  <strong className="text-ellipsis" title={repo.name}>
+                    {repo.name}
+                  </strong>
+                </Radio>
+              )
+            )}
+          </div>
 
           {!showingAllRepositories && repositoryCount > 0 && (
             <Alert variant="warning">
@@ -137,7 +140,7 @@ export default function BitbucketProjectAccordion(props: BitbucketProjectAccordi
               )}
             </Alert>
           )}
-        </div>
+        </>
       )}
     </BoxedGroupAccordion>
   );
index 64fbcaa593044512a3d34cb73419e2d24a04c72f..41b4601d9182856e46d6dfbf581b3ce248974218 100644 (file)
@@ -34,6 +34,7 @@ import {
 } from '../../../types/alm-integration';
 import { AlmSettingsInstance } from '../../../types/alm-settings';
 import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer';
+import { DEFAULT_BBS_PAGE_SIZE } from './constants';
 
 interface Props extends Pick<WithRouterProps, 'location'> {
   canAdmin: boolean;
@@ -146,11 +147,31 @@ export default class BitbucketProjectCreate extends React.PureComponent<Props, S
     return Promise.all(
       projects.map(p => {
         return getBitbucketServerRepositories(bitbucketSetting.key, p.name).then(
-          ({ isLastPage, repositories }) => ({
-            isLastPage,
-            repositories,
-            projectKey: p.key
-          })
+          ({ isLastPage, repositories }) => {
+            // Because the WS uses the project name rather than its key to find
+            // repositories, we can match more repositories than we expect. For
+            // example, p.name = "A1" would find repositories for projects "A1",
+            // "A10", "A11", etc. This is a limitation of BBS. To make sure we
+            // don't display incorrect information, filter on the project key.
+            const filteredRepositories = repositories.filter(r => r.projectKey === p.key);
+
+            // And because of the above, the "isLastPage" cannot be relied upon
+            // either. This one is impossible to get 100% for now. We can only
+            // make some assumptions: by default, the page size for BBS is 25
+            // (this is not part of the payload, so we don't know the actual
+            // number; but changing this implies changing some advanced config,
+            // so it's not likely). If the filtered repos is larger than this
+            // number AND isLastPage is false, we'll keep it at false.
+            // Otherwise, we assume it's true.
+            const realIsLastPage =
+              isLastPage || filteredRepositories.length < DEFAULT_BBS_PAGE_SIZE;
+
+            return {
+              repositories: filteredRepositories,
+              isLastPage: realIsLastPage,
+              projectKey: p.key
+            };
+          }
         );
       })
     ).then(results => {
index 6acd8d44104f868ac5e951f411c47414b4795453..c8f6f122e3a47adeac5f7af6f249e48b15cefb15 100644 (file)
@@ -44,17 +44,32 @@ export default function BitbucketSearchResults(props: BitbucketSearchResultsProp
     selectedRepository
   } = props;
 
+  if (searchResults.length === 0 && !searching) {
+    return (
+      <Alert className="big-spacer-top" variant="warning">
+        {translate('onboarding.create_project.no_bbs_repos.filter')}
+      </Alert>
+    );
+  }
+
   const filteredProjects = uniq(
     searchResults.map(r => projects.find(p => p.key === r.projectKey)).filter(isDefined)
   );
 
-  return filteredProjects.length === 0 && !searching ? (
-    <Alert className="big-spacer-top" variant="warning">
-      {translate('onboarding.create_project.no_bbs_repos.filter')}
-    </Alert>
-  ) : (
+  return (
     <div className="big-spacer-top">
       <DeferredSpinner loading={searching}>
+        {filteredProjects.length === 0 && searchResults.length > 0 && (
+          <BitbucketProjectAccordion
+            disableRepositories={disableRepositories}
+            onSelectRepository={props.onSelectRepository}
+            open={true}
+            repositories={searchResults}
+            selectedRepository={selectedRepository}
+            showingAllRepositories={true}
+          />
+        )}
+
         {filteredProjects.map(project => {
           const repositories = searchResults.filter(r => r.projectKey === project.key);
 
index 7f89c5cce6e666e63ad4c1a53c225581239a82a0..81fdd2319b5ca0804f4ac4449edb9383aded70b0 100644 (file)
@@ -39,6 +39,7 @@ it('should render correctly', () => {
     'selected repo'
   );
   expect(shallowRender({ showingAllRepositories: false })).toMatchSnapshot('not showing all repos');
+  expect(shallowRender({ project: undefined })).toMatchSnapshot('no project info');
 });
 
 it('should correctly handle selecting repos', () => {
index 1d7c2893a08649857e84bf8439028020651b2949..3db815e30a4958cffc3d4efc195c3753a0e0db43 100644 (file)
@@ -32,6 +32,9 @@ it('should render correctly', () => {
     shallowRender({ searching: true, projects: undefined, searchResults: undefined })
   ).toMatchSnapshot('searching');
   expect(shallowRender({ searchResults: undefined })).toMatchSnapshot('no results');
+  expect(
+    shallowRender({ searchResults: [mockBitbucketRepository({ projectKey: 'unknown' })] })
+  ).toMatchSnapshot('unknown project in search results');
 });
 
 function shallowRender(props: Partial<BitbucketSearchResultsProps> = {}) {
index 03b1ab6bdf0c5a141c87bf91036bd7c40d9435e8..e995e7e83796c88ff4bd11325b20af4d497e4202 100644 (file)
@@ -3,7 +3,6 @@
 exports[`should render correctly: closed 1`] = `
 <BoxedGroupAccordion
   className="big-spacer-bottom"
-  key="project"
   onClick={[MockFunction]}
   open={false}
   title={
@@ -17,7 +16,6 @@ exports[`should render correctly: closed 1`] = `
 exports[`should render correctly: default 1`] = `
 <BoxedGroupAccordion
   className="big-spacer-bottom open"
-  key="project"
   onClick={[MockFunction]}
   open={true}
   title={
@@ -90,7 +88,6 @@ exports[`should render correctly: default 1`] = `
 exports[`should render correctly: disable options 1`] = `
 <BoxedGroupAccordion
   className="big-spacer-bottom open"
-  key="project"
   onClick={[MockFunction]}
   open={true}
   title={
@@ -163,7 +160,6 @@ exports[`should render correctly: disable options 1`] = `
 exports[`should render correctly: no click handler 1`] = `
 <BoxedGroupAccordion
   className="big-spacer-bottom open not-clickable no-hover"
-  key="project"
   onClick={[Function]}
   open={true}
   title={
@@ -233,10 +229,81 @@ exports[`should render correctly: no click handler 1`] = `
 </BoxedGroupAccordion>
 `;
 
+exports[`should render correctly: no project info 1`] = `
+<BoxedGroupAccordion
+  className="big-spacer-bottom open"
+  onClick={[MockFunction]}
+  open={true}
+  title={
+    <h3>
+      search_results
+    </h3>
+  }
+>
+  <div
+    className="display-flex-wrap"
+  >
+    <Radio
+      checked={false}
+      className="display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo overflow-hidden"
+      key="1"
+      onCheck={[Function]}
+      value="1"
+    >
+      <strong
+        className="text-ellipsis"
+        title="Repo"
+      >
+        Repo
+      </strong>
+    </Radio>
+    <div
+      className="display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo"
+      key="2"
+    >
+      <CheckIcon
+        className="spacer-right"
+        fill="#00aa00"
+        size={14}
+      />
+      <div
+        className="overflow-hidden"
+      >
+        <div
+          className="little-spacer-bottom text-ellipsis"
+        >
+          <strong
+            title="Bar"
+          >
+            <Link
+              onlyActiveOnIndex={false}
+              style={Object {}}
+              to={
+                Object {
+                  "pathname": "/dashboard",
+                  "query": Object {
+                    "branch": undefined,
+                    "id": "bar",
+                  },
+                }
+              }
+            >
+              Bar
+            </Link>
+          </strong>
+        </div>
+        <em>
+          onboarding.create_project.repository_imported
+        </em>
+      </div>
+    </div>
+  </div>
+</BoxedGroupAccordion>
+`;
+
 exports[`should render correctly: no repos 1`] = `
 <BoxedGroupAccordion
   className="big-spacer-bottom open"
-  key="project"
   onClick={[MockFunction]}
   open={true}
   title={
@@ -282,7 +349,6 @@ exports[`should render correctly: no repos 1`] = `
 exports[`should render correctly: not showing all repos 1`] = `
 <BoxedGroupAccordion
   className="big-spacer-bottom open"
-  key="project"
   onClick={[MockFunction]}
   open={true}
   title={
@@ -348,19 +414,18 @@ exports[`should render correctly: not showing all repos 1`] = `
         </em>
       </div>
     </div>
-    <Alert
-      variant="warning"
-    >
-      onboarding.create_project.only_showing_X_first_repos.2
-    </Alert>
   </div>
+  <Alert
+    variant="warning"
+  >
+    onboarding.create_project.only_showing_X_first_repos.2
+  </Alert>
 </BoxedGroupAccordion>
 `;
 
 exports[`should render correctly: selected repo 1`] = `
 <BoxedGroupAccordion
   className="big-spacer-bottom open"
-  key="project"
   onClick={[MockFunction]}
   open={true}
   title={
index 9ef7b93bd7fdb1db79396caedcdb8c5ab49076cb..87539e11b6b33cf04d5a104244f2264c7715d137 100644 (file)
@@ -53,3 +53,30 @@ exports[`should render correctly: searching 1`] = `
   />
 </div>
 `;
+
+exports[`should render correctly: unknown project in search results 1`] = `
+<div
+  className="big-spacer-top"
+>
+  <DeferredSpinner
+    loading={false}
+  >
+    <BitbucketProjectAccordion
+      disableRepositories={false}
+      onSelectRepository={[MockFunction]}
+      open={true}
+      repositories={
+        Array [
+          Object {
+            "id": 1,
+            "name": "Repo",
+            "projectKey": "unknown",
+            "slug": "project__repo",
+          },
+        ]
+      }
+      showingAllRepositories={true}
+    />
+  </DeferredSpinner>
+</div>
+`;
index 967aee5d9b7fe05f5bfffcf26285cb18cd23bb0c..f01d1d67e34a3963c357893a28a5d17efd5db662 100644 (file)
@@ -19,3 +19,5 @@
  */
 
 export const PROJECT_NAME_MAX_LEN = 255;
+
+export const DEFAULT_BBS_PAGE_SIZE = 25;
index 06b37f78f40984f7bd0bfa0162f75fc29352af68..79705e093760b7f1d8cb8d5575bd5d800cca4c3e 100644 (file)
@@ -168,6 +168,7 @@ review=Review
 rule=Rule
 rules=Rules
 save=Save
+search_results=Search results
 search_verb=Search
 see_all=See all
 select_verb=Select