]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20337 - Quality gate left menu should adpot new uI
authorKevin Silva <kevin.silva@sonarsource.com>
Tue, 5 Sep 2023 15:13:00 +0000 (17:13 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 19 Sep 2023 20:02:46 +0000 (20:02 +0000)
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 97aa3eaae2e1bc389afa99ab725a3ec1e1d5ea9a..47350db2cd6e514a39eb6bdf3bdf4aa7173aeade 100644 (file)
@@ -50,6 +50,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND = [
   '/projects',
   '/project/information',
   '/web_api_v2',
+  '/quality_gates',
 ];
 
 const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = ['/tutorials', '/projects/create'];
@@ -66,7 +67,9 @@ export default function GlobalContainer() {
           <div className="global-container">
             <div
               className={classNames('page-wrapper', {
-                'new-background': TEMP_PAGELIST_WITH_NEW_BACKGROUND.includes(location.pathname),
+                'new-background': TEMP_PAGELIST_WITH_NEW_BACKGROUND.some((element) =>
+                  location.pathname.startsWith(element)
+                ),
                 'white-background': TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE.includes(
                   location.pathname,
                 ),
index 1e5ab66ce5a83343d25b5866c6e07d4e137f8e26..4eaf22bbfd9ae78492c0b534878bf570c903d142 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 { withTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import {
+  LAYOUT_FOOTER_HEIGHT,
+  LAYOUT_GLOBAL_NAV_HEIGHT,
+  LargeCenteredLayout,
+  themeBorder,
+  themeColor,
+} from 'design-system';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { NavigateFunction, useNavigate, useParams } from 'react-router-dom';
 import { fetchQualityGates } from '../../../api/quality-gates';
-import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
 import Suggestions from '../../../components/embed-docs-modal/Suggestions';
 import '../../../components/search-navigator.css';
 import Spinner from '../../../components/ui/Spinner';
@@ -115,7 +123,7 @@ class App extends React.PureComponent<Props, State> {
     const { canCreate, qualityGates } = this.state;
 
     return (
-      <>
+      <LargeCenteredLayout id="quality-gates-page">
         <Helmet
           defer={false}
           titleTemplate={translateWithParameters(
@@ -123,44 +131,56 @@ class App extends React.PureComponent<Props, State> {
             translate('quality_gates.page'),
           )}
         />
-        <div className="layout-page" id="quality-gates-page">
+        <div className="sw-grid sw-gap-x-12 sw-gap-y-6 sw-grid-cols-12 sw-w-full">
           <Suggestions suggestions="quality_gates" />
 
-          <ScreenPositionHelper className="layout-page-side-outer">
-            {({ top }) => (
-              <nav className="layout-page-side" style={{ top }}>
-                <div className="layout-page-side-inner">
-                  <div className="layout-page-filters">
-                    <ListHeader
-                      canCreate={canCreate}
-                      refreshQualityGates={this.fetchQualityGates}
-                    />
-                    <Spinner loading={this.state.loading}>
-                      <List qualityGates={qualityGates} currentQualityGate={name} />
-                    </Spinner>
-                  </div>
-                </div>
-              </nav>
-            )}
-          </ScreenPositionHelper>
+          <ContentWrapper className="sw-col-span-3 sw-px-4 sw-py-6">
+            <ListHeader canCreate={canCreate} refreshQualityGates={this.fetchQualityGates} />
+            <Spinner loading={this.state.loading}>
+              <List qualityGates={qualityGates} currentQualityGate={name} />
+            </Spinner>
+          </ContentWrapper>
 
           {name !== undefined && (
-            <Details
-              qualityGateName={name}
-              onSetDefault={this.handleSetDefault}
-              qualityGates={this.state.qualityGates}
-              refreshQualityGates={this.fetchQualityGates}
-            />
+            <ContentWrapper className="sw-col-span-9 sw-overflow-y-auto">
+              <Details
+                qualityGateName={name}
+                onSetDefault={this.handleSetDefault}
+                qualityGates={this.state.qualityGates}
+                refreshQualityGates={this.fetchQualityGates}
+              />
+            </ContentWrapper>
           )}
         </div>
-      </>
+      </LargeCenteredLayout>
     );
   }
 }
 
+function ContentWrapper({ className, children }: React.PropsWithChildren<{ className: string }>) {
+  return (
+    <StyledContentWrapper
+      className={className}
+      style={{
+        height: `calc(100vh - ${LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_FOOTER_HEIGHT}px)`,
+      }}
+    >
+      {children}
+    </StyledContentWrapper>
+  );
+}
+
 export default function AppWrapper() {
   const params = useParams();
   const navigate = useNavigate();
 
   return <App name={params['name']} navigate={navigate} />;
 }
+
+const StyledContentWrapper = withTheme(styled.div`
+  box-sizing: border-box;
+
+  background-color: ${themeColor('filterbar')};
+  border-right: ${themeBorder('default', 'filterbarBorder')};
+  overflow-x: hidden;
+`);
index 14dd91b530c35579814517167595e7014b62a698..be2849cdf86fb364ee18fd16f2815522bc2ad360 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 { ButtonSecondary, FormField, InputField, Modal } from 'design-system';
 import * as React from 'react';
 import { createQualityGate } from '../../../api/quality-gates';
-import ConfirmModal from '../../../components/controls/ConfirmModal';
 import { Router, withRouter } from '../../../components/hoc/withRouter';
-import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
 import { translate } from '../../../helpers/l10n';
 import { getQualityGateUrl } from '../../../helpers/urls';
@@ -43,47 +42,66 @@ export class CreateQualityGateForm extends React.PureComponent<Props, State> {
     this.setState({ name: event.currentTarget.value });
   };
 
+  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+    event.preventDefault();
+    this.handleCreate();
+  };
+
   handleCreate = async () => {
     const { name } = this.state;
 
-    if (name) {
+    if (name !== undefined) {
       const qualityGate = await createQualityGate({ name });
-
       await this.props.onCreate();
-
+      this.props.onClose();
       this.props.router.push(getQualityGateUrl(qualityGate.name));
     }
   };
 
   render() {
     const { name } = this.state;
-    return (
-      <ConfirmModal
-        confirmButtonText={translate('save')}
-        confirmDisable={!name}
-        header={translate('quality_gates.create')}
-        onClose={this.props.onClose}
-        onConfirm={this.handleCreate}
-        size="small"
-      >
+
+    const body = (
+      <form onSubmit={this.handleFormSubmit}>
         <MandatoryFieldsExplanation className="modal-field" />
-        <div className="modal-field">
-          <label htmlFor="quality-gate-form-name">
-            {translate('name')}
-            <MandatoryFieldMarker />
-          </label>
-          <input
-            autoFocus
+        <FormField
+          htmlFor="quality-gate-form-name"
+          label={translate('name')}
+          required
+          requiredAriaLabel={translate('field_required')}
+        >
+          <InputField
+            autoComplete="off"
             id="quality-gate-form-name"
-            maxLength={100}
+            maxLength={256}
+            name="key"
             onChange={this.handleNameChange}
-            required
-            size={50}
             type="text"
+            size="full"
             value={name}
           />
-        </div>
-      </ConfirmModal>
+        </FormField>
+      </form>
+    );
+
+    return (
+      <Modal
+        onClose={this.props.onClose}
+        headerTitle={translate('quality_gates.create')}
+        isScrollable
+        body={body}
+        primaryButton={
+          <ButtonSecondary
+            disabled={name === null || name === ''}
+            form="create-application-form"
+            type="submit"
+            onClick={this.handleCreate}
+          >
+            {translate('quality_gate.create')}
+          </ButtonSecondary>
+        }
+        secondaryButtonLabel={translate('cancel')}
+      />
     );
   }
 }
index 4cd78cc3a2616528bc5ffbbefb74f691468eea29..1aab7e8dc28bdf312f597f3b46123aaed7e80744 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 {
+  Badge,
+  BareButton,
+  FlagWarningIcon,
+  SubnavigationGroup,
+  SubnavigationItem,
+} from 'design-system';
 import * as React from 'react';
-import { NavLink } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
 import Tooltip from '../../../components/controls/Tooltip';
-import AlertWarnIcon from '../../../components/icons/AlertWarnIcon';
+
 import { translate } from '../../../helpers/l10n';
 import { getQualityGateUrl } from '../../../helpers/urls';
 import { CaycStatus, QualityGate } from '../../../types/types';
@@ -32,34 +39,54 @@ interface Props {
 }
 
 export default function List({ qualityGates, currentQualityGate }: Props) {
+  const navigateTo = useNavigate();
+
+  function redirectQualityGate(qualityGateName: string) {
+    navigateTo(getQualityGateUrl(qualityGateName));
+  }
+
   return (
-    <div className="list-group">
-      {qualityGates.map((qualityGate) => (
-        <NavLink
-          className="list-group-item display-flex-center"
-          aria-current={currentQualityGate === qualityGate.name && 'page'}
-          key={qualityGate.name}
-          to={getQualityGateUrl(qualityGate.name)}
-        >
-          <span className="flex-1 text-ellipsis" title={qualityGate.name}>
-            {qualityGate.name}
-          </span>
-          {qualityGate.isDefault && (
-            <span className="badge little-spacer-left">{translate('default')}</span>
-          )}
-          {qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="little-spacer-left" />}
+    <SubnavigationGroup>
+      {qualityGates.map((qualityGate) => {
+        const isDefaultTitle = qualityGate.isDefault ? ` ${translate('default')}` : '';
+        const isBuiltInTitle = qualityGate.isBuiltIn
+          ? ` ${translate('quality_gates.built_in')}`
+          : '';
+
+        return (
+          <SubnavigationItem
+            className="it__list-group-item"
+            active={currentQualityGate === qualityGate.name}
+            key={qualityGate.name}
+            onClick={() => {
+              redirectQualityGate(qualityGate.name);
+            }}
+          >
+            <div className="sw-flex sw-flex-col">
+              <BareButton
+                aria-current={currentQualityGate === qualityGate.name && 'page'}
+                title={`${qualityGate.name}${isDefaultTitle}${isBuiltInTitle}`}
+                className="sw-flex-1 sw-text-ellipsis sw-overflow-hidden sw-max-w-abs-250 sw-whitespace-nowrap"
+              >
+                {qualityGate.name}
+              </BareButton>
 
-          {qualityGate.caycStatus === CaycStatus.NonCompliant &&
-            qualityGate.actions?.manageConditions && (
-              <Tooltip overlay={translate('quality_gates.cayc.tooltip.message')}>
-                <AlertWarnIcon
-                  className="spacer-left"
-                  description={translate('quality_gates.cayc.tooltip.message')}
-                />
-              </Tooltip>
-            )}
-        </NavLink>
-      ))}
-    </div>
+              {(qualityGate.isDefault || qualityGate.isBuiltIn) && (
+                <div className="sw-mt-2">
+                  {qualityGate.isDefault && <Badge>{translate('default')}</Badge>}
+                  {qualityGate.isBuiltIn && <BuiltInQualityGateBadge />}
+                </div>
+              )}
+            </div>
+            {qualityGate.caycStatus === CaycStatus.NonCompliant &&
+              qualityGate.actions?.manageConditions && (
+                <Tooltip overlay={translate('quality_gates.cayc.tooltip.message')}>
+                  <FlagWarningIcon description={translate('quality_gates.cayc.tooltip.message')} />
+                </Tooltip>
+              )}
+          </SubnavigationItem>
+        );
+      })}
+    </SubnavigationGroup>
   );
 }
index fa77e1d3b4db98a2d8f4bdcfbe7f333963b62765..5e114d1b2c8cd7a14b57e7f777861c65e81a2f7e 100644 (file)
@@ -17,9 +17,9 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { ButtonPrimary, HelperHintIcon, Note } from 'design-system';
 import * as React from 'react';
 import DocumentationTooltip from '../../../components/common/DocumentationTooltip';
-import { Button } from '../../../components/controls/buttons';
 import ModalButton from '../../../components/controls/ModalButton';
 import { translate } from '../../../helpers/l10n';
 import CreateQualityGateForm from './CreateQualityGateForm';
@@ -29,27 +29,31 @@ interface Props {
   refreshQualityGates: () => Promise<void>;
 }
 
-export default function ListHeader({ canCreate, refreshQualityGates }: Props) {
+function CreateQualityGateModal({ refreshQualityGates }: Pick<Props, 'refreshQualityGates'>) {
   return (
-    <div className="page-header">
-      {canCreate && (
-        <div className="page-actions">
-          <ModalButton
-            modal={({ onClose }) => (
-              <CreateQualityGateForm onClose={onClose} onCreate={refreshQualityGates} />
-            )}
-          >
-            {({ onClick }) => (
-              <Button data-test="quality-gates__add" onClick={onClick}>
-                {translate('create')}
-              </Button>
-            )}
-          </ModalButton>
-        </div>
-      )}
+    <div>
+      <ModalButton
+        modal={({ onClose }) => (
+          <CreateQualityGateForm onClose={onClose} onCreate={refreshQualityGates} />
+        )}
+      >
+        {({ onClick }) => (
+          <ButtonPrimary data-test="quality-gates__add" onClick={onClick}>
+            {translate('create')}
+          </ButtonPrimary>
+        )}
+      </ModalButton>
+    </div>
+  );
+}
 
-      <div className="display-flex-center">
-        <h1 className="page-title">{translate('quality_gates.page')}</h1>
+export default function ListHeader({ canCreate, refreshQualityGates }: Props) {
+  return (
+    <div className="sw-flex sw-justify-between sw-pb-4">
+      <div className="sw-flex sw-justify-between">
+        <Note as="h1" className="sw-flex sw-items-center sw-body-md-highlight">
+          {translate('quality_gates.page')}
+        </Note>
         <DocumentationTooltip
           className="spacer-left"
           content={translate('quality_gates.help')}
@@ -59,8 +63,11 @@ export default function ListHeader({ canCreate, refreshQualityGates }: Props) {
               label: translate('learn_more'),
             },
           ]}
-        />
+        >
+          <HelperHintIcon />
+        </DocumentationTooltip>
       </div>
+      {canCreate && <CreateQualityGateModal refreshQualityGates={refreshQualityGates} />}
     </div>
   );
 }
index 5fd6dfe27dd431f838f2b20e04f9d84d7db1c8cd..67560e0cfe06f9804c36df5b018ae56a8d356110 100644 (file)
@@ -39,7 +39,7 @@ it('should open the default quality gates', async () => {
 
   const defaultQualityGate = handler.getDefaultQualityGate();
   expect(
-    await screen.findByRole('link', {
+    await screen.findByRole('button', {
       current: 'page',
       name: `${defaultQualityGate.name} default`,
     })
@@ -50,13 +50,13 @@ it('should list all quality gates', async () => {
   renderQualityGateApp();
 
   expect(
-    await screen.findByRole('link', {
+    await screen.findByRole('button', {
       name: `${handler.getDefaultQualityGate().name} default`,
     })
   ).toBeInTheDocument();
 
   expect(
-    screen.getByRole('link', {
+    screen.getByRole('button', {
       name: `${handler.getBuiltInQualityGate().name} quality_gates.built_in`,
     })
   ).toBeInTheDocument();
@@ -72,14 +72,15 @@ it('should be able to create a quality gate then delete it', async () => {
   await user.click(createButton);
   await act(async () => {
     await user.click(screen.getByRole('textbox', { name: /name.*/ }));
-    await user.keyboard('testone{Enter}');
+    await user.keyboard('testone');
+    await user.click(screen.getByRole('button', { name: 'quality_gate.create' }));
   });
-  expect(await screen.findByRole('link', { name: 'testone' })).toBeInTheDocument();
+  expect(await screen.findByRole('button', { name: 'testone' })).toBeInTheDocument();
 
   // Using modal button
   createButton = await screen.findByRole('button', { name: 'create' });
   await user.click(createButton);
-  const saveButton = screen.getByRole('button', { name: 'save' });
+  const saveButton = screen.getByRole('button', { name: 'quality_gate.create' });
 
   expect(saveButton).toBeDisabled();
   const nameInput = screen.getByRole('textbox', { name: /name.*/ });
@@ -89,7 +90,7 @@ it('should be able to create a quality gate then delete it', async () => {
     await user.click(saveButton);
   });
 
-  const newQG = await screen.findByRole('link', { name: 'testtwo' });
+  const newQG = await screen.findByRole('button', { name: 'testtwo' });
 
   expect(newQG).toBeInTheDocument();
 
@@ -102,7 +103,7 @@ it('should be able to create a quality gate then delete it', async () => {
   await user.click(dialogDeleteButton);
 
   await waitFor(() => {
-    expect(screen.queryByRole('link', { name: 'testtwo' })).not.toBeInTheDocument();
+    expect(screen.queryByRole('button', { name: 'testtwo' })).not.toBeInTheDocument();
   });
 });
 
@@ -122,7 +123,7 @@ it('should be able to copy a quality gate which is CAYC compliant', async () =>
     await user.click(nameInput);
     await user.keyboard(' bis{Enter}');
   });
-  expect(await screen.findByRole('link', { name: /.* bis/ })).toBeInTheDocument();
+  expect(await screen.findByRole('button', { name: /.* bis/ })).toBeInTheDocument();
 });
 
 it('should not be able to copy a quality gate which is not CAYC compliant', async () => {
@@ -151,7 +152,7 @@ it('should be able to rename a quality gate', async () => {
   await user.click(nameInput);
   await user.keyboard('{Control>}a{/Control}New Name{Enter}');
 
-  expect(await screen.findByRole('link', { name: /New Name.*/ })).toBeInTheDocument();
+  expect(await screen.findByRole('button', { name: /New Name.*/ })).toBeInTheDocument();
 });
 
 it('should not be able to set as default a quality gate which is not CAYC compliant', async () => {
@@ -170,11 +171,11 @@ it('should be able to set as default a quality gate which is CAYC compliant', as
   handler.setIsAdmin(true);
   renderQualityGateApp();
 
-  const notDefaultQualityGate = await screen.findByRole('link', { name: /Sonar way/ });
+  const notDefaultQualityGate = await screen.findByRole('button', { name: /Sonar way/ });
   await user.click(notDefaultQualityGate);
   const setAsDefaultButton = screen.getByRole('button', { name: 'set_as_default' });
   await user.click(setAsDefaultButton);
-  expect(screen.getByRole('link', { name: /Sonar way/ })).toHaveTextContent('default');
+  expect(screen.getByRole('button', { name: /Sonar way default/ })).toBeInTheDocument();
 });
 
 it('should be able to add a condition', async () => {
@@ -256,7 +257,7 @@ it('should be able to handle duplicate or deprecated condition', async () => {
 
   await user.click(
     // make it a regexp to ignore badges:
-    await screen.findByRole('link', { name: new RegExp(handler.getCorruptedQualityGateName()) })
+    await screen.findByRole('button', { name: new RegExp(handler.getCorruptedQualityGateName()) })
   );
 
   expect(await screen.findByText('quality_gates.duplicated_conditions')).toBeInTheDocument();
@@ -351,7 +352,7 @@ it('should not warn user when quality gate is not CAYC compliant and user has no
   const user = userEvent.setup();
   renderQualityGateApp();
 
-  const nonCompliantQualityGate = await screen.findByRole('link', { name: 'Non Cayc QG' });
+  const nonCompliantQualityGate = await screen.findByRole('button', { name: 'Non Cayc QG' });
 
   await user.click(nonCompliantQualityGate);
 
@@ -364,7 +365,7 @@ it('should warn user when quality gate is not CAYC compliant and user has permis
   handler.setIsAdmin(true);
   renderQualityGateApp();
 
-  const nonCompliantQualityGate = await screen.findByRole('link', { name: /Non Cayc QG/ });
+  const nonCompliantQualityGate = await screen.findByRole('button', { name: /Non Cayc QG/ });
 
   await user.click(nonCompliantQualityGate);
 
@@ -529,7 +530,7 @@ describe('The Permissions section', () => {
 
     // await just to make sure we've loaded the page
     expect(
-      await screen.findByRole('link', {
+      await screen.findByRole('button', {
         name: `${handler.getDefaultQualityGate().name} default`,
       })
     ).toBeInTheDocument();
index ca2f97c61e6e2544edeb0cef90ad9524f2a6c897..a38463d3719ac61653ecc10134f00c4e1562d9a6 100644 (file)
@@ -2084,6 +2084,7 @@ quality_profiles.actions=Open {0} {1} quality profile actions
 #
 #------------------------------------------------------------------------------
 
+quality_gate.create=Create
 quality_gates.create=Create Quality Gate
 quality_gates.rename=Rename Quality Gate
 quality_gates.delete=Delete Quality Gate