From 44e4e489ee6fd262d805a0418bd381625aaee349 Mon Sep 17 00:00:00 2001 From: Kevin Silva Date: Tue, 5 Sep 2023 17:13:00 +0200 Subject: [PATCH] SONAR-20337 - Quality gate left menu should adpot new uI --- .../js/app/components/GlobalContainer.tsx | 5 +- .../js/apps/quality-gates/components/App.tsx | 74 ++++++++++------ .../components/CreateQualityGateForm.tsx | 70 +++++++++------ .../js/apps/quality-gates/components/List.tsx | 85 ++++++++++++------- .../quality-gates/components/ListHeader.tsx | 49 ++++++----- .../components/__tests__/QualityGate-it.tsx | 33 +++---- .../resources/org/sonar/l10n/core.properties | 1 + 7 files changed, 197 insertions(+), 120 deletions(-) diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx index 97aa3eaae2e..47350db2cd6 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -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() {
+ location.pathname.startsWith(element) + ), 'white-background': TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE.includes( location.pathname, ), diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx index 1e5ab66ce5a..4eaf22bbfd9 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx @@ -17,11 +17,19 @@ * 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 { const { canCreate, qualityGates } = this.state; return ( - <> + { translate('quality_gates.page'), )} /> -
+
- - {({ top }) => ( - - )} - + + + + + + {name !== undefined && ( -
+ +
+ )}
- + ); } } +function ContentWrapper({ className, children }: React.PropsWithChildren<{ className: string }>) { + return ( + + {children} + + ); +} + export default function AppWrapper() { const params = useParams(); const navigate = useNavigate(); return ; } + +const StyledContentWrapper = withTheme(styled.div` + box-sizing: border-box; + + background-color: ${themeColor('filterbar')}; + border-right: ${themeBorder('default', 'filterbarBorder')}; + overflow-x: hidden; +`); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx index 14dd91b530c..be2849cdf86 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx @@ -17,11 +17,10 @@ * 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 { this.setState({ name: event.currentTarget.value }); }; + handleFormSubmit = (event: React.SyntheticEvent) => { + 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 ( - + + const body = ( +
-
- - + -
- + + + ); + + return ( + + {translate('quality_gate.create')} + + } + secondaryButtonLabel={translate('cancel')} + /> ); } } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx index 4cd78cc3a26..1aab7e8dc28 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx @@ -17,10 +17,17 @@ * 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 ( -
- {qualityGates.map((qualityGate) => ( - - - {qualityGate.name} - - {qualityGate.isDefault && ( - {translate('default')} - )} - {qualityGate.isBuiltIn && } + + {qualityGates.map((qualityGate) => { + const isDefaultTitle = qualityGate.isDefault ? ` ${translate('default')}` : ''; + const isBuiltInTitle = qualityGate.isBuiltIn + ? ` ${translate('quality_gates.built_in')}` + : ''; + + return ( + { + redirectQualityGate(qualityGate.name); + }} + > +
+ + {qualityGate.name} + - {qualityGate.caycStatus === CaycStatus.NonCompliant && - qualityGate.actions?.manageConditions && ( - - - - )} - - ))} -
+ {(qualityGate.isDefault || qualityGate.isBuiltIn) && ( +
+ {qualityGate.isDefault && {translate('default')}} + {qualityGate.isBuiltIn && } +
+ )} +
+ {qualityGate.caycStatus === CaycStatus.NonCompliant && + qualityGate.actions?.manageConditions && ( + + + + )} + + ); + })} + ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx index fa77e1d3b4d..5e114d1b2c8 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx @@ -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; } -export default function ListHeader({ canCreate, refreshQualityGates }: Props) { +function CreateQualityGateModal({ refreshQualityGates }: Pick) { return ( -
- {canCreate && ( -
- ( - - )} - > - {({ onClick }) => ( - - )} - -
- )} +
+ ( + + )} + > + {({ onClick }) => ( + + {translate('create')} + + )} + +
+ ); +} -
-

{translate('quality_gates.page')}

+export default function ListHeader({ canCreate, refreshQualityGates }: Props) { + return ( +
+
+ + {translate('quality_gates.page')} + + > + +
+ {canCreate && }
); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx index 5fd6dfe27dd..67560e0cfe0 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx @@ -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(); diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index ca2f97c61e6..a38463d3719 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 -- 2.39.5