]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20086 Migrate Bitbucket Cloud import page to the new UI
authorJeremy Davis <jeremy.davis@sonarsource.com>
Thu, 10 Aug 2023 09:28:45 +0000 (11:28 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 14 Aug 2023 20:02:57 +0000 (20:02 +0000)
server/sonar-web/design-system/src/components/input/InputSearch.tsx
server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreateRender.tsx
server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudSearchForm.tsx
server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreateRenderer.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx
server/sonar-web/src/main/js/apps/create/project/constants.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 25428652814bbf8624499213729b0dd4071ea269..cb3eed1cbeba4c088aeb08dda3264465a1449007 100644 (file)
@@ -145,7 +145,7 @@ export function InputSearch({
       <StyledInputWrapper className="sw-flex sw-items-center">
         {children ?? (
           <input
-            aria-label={searchInputAriaLabel}
+            aria-label={searchInputAriaLabel ?? placeholder}
             autoComplete="off"
             className={inputClassName}
             maxLength={maxLength}
index 0a4a2baf20c2f51ab000ff4521306d86475ffa6c..8012134258beefd0ce1caad58c243086c7a40ae3 100644 (file)
@@ -26,6 +26,7 @@ import { Location, Router } from '../../../../components/hoc/withRouter';
 import { BitbucketCloudRepository } from '../../../../types/alm-integration';
 import { AlmSettingsInstance } from '../../../../types/alm-settings';
 import { Paging } from '../../../../types/types';
+import { BITBUCKET_CLOUD_PROJECTS_PAGESIZE } from '../constants';
 import { CreateProjectApiCallback } from '../types';
 import BitbucketCloudProjectCreateRenderer from './BitbucketCloudProjectCreateRender';
 
@@ -51,7 +52,6 @@ interface State {
   showPersonalAccessTokenForm: boolean;
 }
 
-export const BITBUCKET_CLOUD_PROJECTS_PAGESIZE = 30;
 export default class BitbucketCloudProjectCreate extends React.PureComponent<Props, State> {
   mounted = false;
 
index 770e8cac2ecb325957351e645b6bc5e0d6560ba9..992f8046e4f645d0a90f7552210675cc50f55c23 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { DeferredSpinner, LightPrimary, Title } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../../../helpers/l10n';
-import { getBaseUrl } from '../../../../helpers/system';
 import { BitbucketCloudRepository } from '../../../../types/alm-integration';
 import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings';
 import AlmSettingsInstanceDropdown from '../components/AlmSettingsInstanceDropdown';
-import CreateProjectPageHeader from '../components/CreateProjectPageHeader';
 import PersonalAccessTokenForm from '../components/PersonalAccessTokenForm';
 import WrongBindingCountAlert from '../components/WrongBindingCountAlert';
 import BitbucketCloudSearchForm from './BitbucketCloudSearchForm';
@@ -66,19 +65,14 @@ export default function BitbucketCloudProjectCreateRenderer(
 
   return (
     <>
-      <CreateProjectPageHeader
-        title={
-          <span className="text-middle">
-            <img
-              alt="" // Should be ignored by screen readers
-              className="spacer-right"
-              height="24"
-              src={`${getBaseUrl()}/images/alm/bitbucket.svg`}
-            />
-            {translate('onboarding.create_project.bitbucketcloud.title')}
-          </span>
-        }
-      />
+      <header className="sw-mb-10">
+        <Title className="sw-mb-4">
+          {translate('onboarding.create_project.bitbucketcloud.title')}
+        </Title>
+        <LightPrimary className="sw-body-sm">
+          {translate('onboarding.create_project.bitbucketcloud.subtitle')}
+        </LightPrimary>
+      </header>
 
       <AlmSettingsInstanceDropdown
         almKey={AlmKeys.BitbucketCloud}
@@ -87,7 +81,7 @@ export default function BitbucketCloudProjectCreateRenderer(
         onChangeConfig={props.onSelectedAlmInstanceChange}
       />
 
-      {loading && <i className="spinner" />}
+      <DeferredSpinner loading={loading} />
 
       {!loading && !selectedAlmInstance && (
         <WrongBindingCountAlert alm={AlmKeys.BitbucketCloud} canAdmin={!!canAdmin} />
index 8b90691f34810f99ae62a677f399838be64d0895..206a2bcb082c8eb8c204662e0fdbf6f8ce622783 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { FlagMessage, InputSearch, LightPrimary, Link } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import Link from '../../../../components/common/Link';
-import SearchBox from '../../../../components/controls/SearchBox';
-import Tooltip from '../../../../components/controls/Tooltip';
-import { Button } from '../../../../components/controls/buttons';
-import CheckIcon from '../../../../components/icons/CheckIcon';
-import QualifierIcon from '../../../../components/icons/QualifierIcon';
-import { Alert } from '../../../../components/ui/Alert';
-import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
-import { translate, translateWithParameters } from '../../../../helpers/l10n';
-import { formatMeasure } from '../../../../helpers/measures';
-import { getProjectUrl, queryToSearch } from '../../../../helpers/urls';
+import ListFooter from '../../../../components/controls/ListFooter';
+import { translate } from '../../../../helpers/l10n';
+import { getBaseUrl } from '../../../../helpers/system';
+import { queryToSearch } from '../../../../helpers/urls';
 import { BitbucketCloudRepository } from '../../../../types/alm-integration';
-import { ComponentQualifier } from '../../../../types/component';
-import { MetricType } from '../../../../types/metrics';
+import AlmRepoItem from '../components/AlmRepoItem';
+import { BITBUCKET_CLOUD_PROJECTS_PAGESIZE } from '../constants';
 import { CreateProjectModes } from '../types';
 
 export interface BitbucketCloudSearchFormProps {
@@ -55,7 +49,7 @@ export default function BitbucketCloudSearchForm(props: BitbucketCloudSearchForm
 
   if (repositories.length === 0 && searchQuery.length === 0 && !searching) {
     return (
-      <Alert className="spacer-top" variant="warning">
+      <FlagMessage className="sw-mt-2" variant="warning">
         <FormattedMessage
           defaultMessage={translate('onboarding.create_project.bitbucketcloud.no_projects')}
           id="onboarding.create_project.bitbucketcloud.no_projects"
@@ -72,106 +66,56 @@ export default function BitbucketCloudSearchForm(props: BitbucketCloudSearchForm
             ),
           }}
         />
-      </Alert>
+      </FlagMessage>
     );
   }
 
   return (
-    <div className="boxed-group big-padded create-project-import">
-      <SearchBox
-        className="spacer"
-        loading={searching}
-        minLength={3}
-        onChange={props.onSearch}
-        placeholder={translate('onboarding.create_project.search_prompt')}
-      />
-
-      <hr />
+    <div>
+      <div className="sw-flex sw-items-center sw-mb-6 sw-w-abs-400">
+        <InputSearch
+          clearIconAriaLabel={translate('clear')}
+          loading={searching}
+          minLength={3}
+          onChange={props.onSearch}
+          placeholder={translate('onboarding.create_project.search_prompt')}
+          size="full"
+          value={searchQuery}
+        />
+      </div>
 
       {repositories.length === 0 ? (
-        <div className="padded">{translate('no_results')}</div>
+        <div className="sw-py-6 sw-px-2">
+          <LightPrimary className="sw-body-sm">{translate('no_results')}</LightPrimary>
+        </div>
       ) : (
-        <table className="data zebra zebra-hover">
-          <tbody>
-            {repositories.map((repository) => (
-              <tr key={repository.uuid}>
-                <td>
-                  <Tooltip overlay={repository.slug}>
-                    <strong className="project-name display-inline-block text-ellipsis">
-                      {repository.sqProjectKey ? (
-                        <Link to={getProjectUrl(repository.sqProjectKey)}>
-                          <QualifierIcon
-                            className="spacer-right"
-                            qualifier={ComponentQualifier.Project}
-                          />
-                          {repository.name}
-                        </Link>
-                      ) : (
-                        repository.name
-                      )}
-                    </strong>
-                  </Tooltip>
-                  <br />
-                  <Tooltip overlay={repository.projectKey}>
-                    <span className="text-muted project-path display-inline-block text-ellipsis">
-                      {repository.projectKey}
-                    </span>
-                  </Tooltip>
-                </td>
-                <td>
-                  <Link
-                    className="display-inline-flex-center big-spacer-right"
-                    to={getRepositoryUrl(repository.workspace, repository.slug)}
-                    target="_blank"
-                  >
-                    {translate('onboarding.create_project.bitbucketcloud.link')}
-                  </Link>
-                </td>
-                {repository.sqProjectKey ? (
-                  <td>
-                    <span className="display-flex-center display-flex-justify-end already-set-up">
-                      <CheckIcon className="little-spacer-right" size={12} />
-                      {translate('onboarding.create_project.repository_imported')}
-                    </span>
-                  </td>
-                ) : (
-                  <td className="text-right">
-                    <Button
-                      onClick={() => {
-                        props.onImport(repository.slug);
-                      }}
-                    >
-                      {translate('onboarding.create_project.set_up')}
-                    </Button>
-                  </td>
-                )}
-              </tr>
-            ))}
-          </tbody>
-        </table>
+        <div className="sw-flex sw-flex-col sw-gap-3">
+          {repositories.map((r) => (
+            <AlmRepoItem
+              key={r.uuid}
+              almKey={r.slug}
+              almUrl={getRepositoryUrl(r.workspace, r.slug)}
+              almUrlText={translate('onboarding.create_project.bitbucketcloud.link')}
+              almIconSrc={`${getBaseUrl()}/images/alm/bitbucket.svg`}
+              sqProjectKey={r.sqProjectKey}
+              onImport={props.onImport}
+              primaryTextNode={<span title={r.name}>{r.name}</span>}
+              secondaryTextNode={<span title={r.projectKey}>{r.projectKey}</span>}
+            />
+          ))}
+        </div>
       )}
-      <footer className="spacer-top note text-center">
-        {isLastPage &&
-          translateWithParameters(
-            'x_of_y_shown',
-            formatMeasure(repositories.length, MetricType.Integer, null),
-            formatMeasure(repositories.length, MetricType.Integer, null)
-          )}
-        {!isLastPage && (
-          <Button
-            className="spacer-left"
-            disabled={loadingMore}
-            data-test="show-more"
-            onClick={props.onLoadMore}
-          >
-            {translate('show_more')}
-          </Button>
-        )}
-        <DeferredSpinner
-          className="text-bottom spacer-left position-absolute"
-          loading={loadingMore}
-        />
-      </footer>
+
+      <ListFooter
+        className="sw-mb-10"
+        count={repositories.length}
+        // we don't know the total, so only provide when we've reached the last page
+        total={isLastPage ? repositories.length : undefined}
+        pageSize={BITBUCKET_CLOUD_PROJECTS_PAGESIZE}
+        loadMore={props.onLoadMore}
+        loading={loadingMore}
+        useMIUIButtons
+      />
     </div>
   );
 }
index 17a29443b71d35cd022a8d9d34ad2de0670907ea..69de3118fd35ed8eebe15215b669a4d367f629a3 100644 (file)
@@ -76,12 +76,12 @@ function renderRepositoryList(props: GitHubProjectCreateRendererProps) {
         <div className="sw-flex sw-items-center sw-mb-6">
           <InputSearch
             size="large"
+            loading={loadingRepositories}
             onChange={props.onSearch}
             placeholder={translate('onboarding.create_project.search_repositories')}
             value={searchQuery}
             clearIconAriaLabel={translate('clear')}
           />
-          <DeferredSpinner loading={loadingRepositories} className="sw-ml-2" />
         </div>
 
         {repositories.length === 0 ? (
index 37558ec5f1845202220367c7063022d9ffbfed58..af4d30aa07c8c4a7aea053282be6a7e9a5a571bb 100644 (file)
@@ -17,7 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { act, screen, within } from '@testing-library/react';
+import { act, screen, waitFor, within } from '@testing-library/react';
 
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
@@ -29,6 +29,7 @@ import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServi
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
 import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
 import CreateProjectPage from '../CreateProjectPage';
+import { BITBUCKET_CLOUD_PROJECTS_PAGESIZE } from '../constants';
 
 jest.mock('../../../../api/alm-integrations');
 jest.mock('../../../../api/alm-settings');
@@ -148,7 +149,7 @@ it('should show import project feature when PAT is already set', async () => {
 
   projectItem = screen.getByRole('row', { name: /BitbucketCloud Repo 2/ });
   const setupButton = within(projectItem).getByRole('button', {
-    name: 'onboarding.create_project.set_up',
+    name: 'onboarding.create_project.import',
   });
 
   await user.click(setupButton);
@@ -181,7 +182,7 @@ it('should show search filter when PAT is already set', async () => {
   expect(searchForBitbucketCloudRepositories).toHaveBeenLastCalledWith(
     'conf-bitbucketcloud-2',
     '',
-    30,
+    BITBUCKET_CLOUD_PROJECTS_PAGESIZE,
     1
   );
 
@@ -194,7 +195,7 @@ it('should show search filter when PAT is already set', async () => {
   expect(searchForBitbucketCloudRepositories).toHaveBeenLastCalledWith(
     'conf-bitbucketcloud-2',
     'search',
-    30,
+    BITBUCKET_CLOUD_PROJECTS_PAGESIZE,
     1
   );
 });
@@ -217,7 +218,10 @@ it('should show no result message when there are no projects', async () => {
 
 it('should have load more', async () => {
   const user = userEvent.setup();
-  almIntegrationHandler.createRandomBitbucketCloudProjectsWithLoadMore(2, 4);
+  almIntegrationHandler.createRandomBitbucketCloudProjectsWithLoadMore(
+    BITBUCKET_CLOUD_PROJECTS_PAGESIZE,
+    BITBUCKET_CLOUD_PROJECTS_PAGESIZE + 1
+  );
   renderCreateProject();
 
   expect(screen.getByText('onboarding.create_project.bitbucketcloud.title')).toBeInTheDocument();
@@ -227,23 +231,28 @@ it('should have load more', async () => {
     await selectEvent.select(ui.instanceSelector.get(), [/conf-bitbucketcloud-2/]);
   });
 
-  const loadMore = screen.getByRole('button', { name: 'show_more' });
-  expect(loadMore).toBeInTheDocument();
+  expect(screen.getByRole('button', { name: 'show_more' })).toBeInTheDocument();
 
   /*
    * Next api call response will simulate reaching the last page so we can test the
    * loadmore button disapperance.
    */
-  almIntegrationHandler.createRandomBitbucketCloudProjectsWithLoadMore(4, 4);
-  await user.click(loadMore);
+  almIntegrationHandler.createRandomBitbucketCloudProjectsWithLoadMore(
+    BITBUCKET_CLOUD_PROJECTS_PAGESIZE + 1,
+    BITBUCKET_CLOUD_PROJECTS_PAGESIZE + 1
+  );
+  await user.click(screen.getByRole('button', { name: 'show_more' }));
 
   expect(searchForBitbucketCloudRepositories).toHaveBeenLastCalledWith(
     'conf-bitbucketcloud-2',
     '',
-    30,
+    BITBUCKET_CLOUD_PROJECTS_PAGESIZE,
     2
   );
-  expect(loadMore).not.toBeInTheDocument();
+
+  await waitFor(() => {
+    expect(screen.queryByRole('button', { name: 'show_more' })).not.toBeInTheDocument();
+  });
 });
 
 function renderCreateProject() {
index 209b26dca3f3be48b35b39fe9da7dd392f8939fd..3a00eafc4a4a9e643f0b371a1eee794aafddf08b 100644 (file)
@@ -20,3 +20,5 @@
 export const PROJECT_NAME_MAX_LEN = 255;
 
 export const DEFAULT_BBS_PAGE_SIZE = 25;
+
+export const BITBUCKET_CLOUD_PROJECTS_PAGESIZE = 20;
index a702ea5c2124fc93d664c9d67a5a52122157ffec..7dc5a3b10abf70ccf7bc29006334da7a760aaa9c 100644 (file)
@@ -3960,6 +3960,7 @@ onboarding.create_project.azure.search_results_for_project_X=Search results for
 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.bitbucketcloud.title=Bitbucket Cloud project onboarding
+onboarding.create_project.bitbucketcloud.subtitle=Import projects from one of your Bitbucket Cloud workspaces
 onboarding.create_project.bitbucketcloud.no_projects=No projects could be fetched from Bitbucket. Contact your system administrator, or {link}.
 onboarding.create_project.bitbucketcloud.link=See on Bitbucket
 onboarding.create_project.github.title=GitHub project onboarding