diff options
author | Revanshu Paliwal <revanshu.paliwal@sonarsource.com> | 2023-09-14 10:52:57 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-09-19 20:02:46 +0000 |
commit | ea198cf0fad05628dd00f84d8b780130a9e2f3f2 (patch) | |
tree | fc249c478a756fa0b5efacd1edc6e3ac23722aa2 /server/sonar-web/src | |
parent | 352f54af6e3d10abb6b5df976c0b856b768d87f6 (diff) | |
download | sonarqube-ea198cf0fad05628dd00f84d8b780130a9e2f3f2.tar.gz sonarqube-ea198cf0fad05628dd00f84d8b780130a9e2f3f2.zip |
SONAR-20337 Migrating quality gate header to MIUI
Diffstat (limited to 'server/sonar-web/src')
6 files changed, 275 insertions, 194 deletions
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx index 341704ba7d3..b2c5203171b 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx @@ -17,7 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import classNames from 'classnames'; + +import { Badge } from 'design-system'; import * as React from 'react'; import Tooltip from '../../../components/controls/Tooltip'; import { translate } from '../../../helpers/l10n'; @@ -29,7 +30,7 @@ interface Props { export default function BuiltInQualityGateBadge({ className }: Props) { return ( <Tooltip overlay={translate('quality_gates.built_in.help')}> - <div className={classNames('badge', className)}>{translate('quality_gates.built_in')}</div> + <Badge className={className}>{translate('quality_gates.built_in')}</Badge> </Tooltip> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx index f14b9ee6ea6..13193152b68 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.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 { ButtonPrimary, FormField, InputField, Modal } from 'design-system'; import * as React from 'react'; import { copyQualityGate } 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'; @@ -38,6 +37,8 @@ interface State { name: string; } +const FORM_ID = 'rename-quality-gate'; + export class CopyQualityGateForm extends React.PureComponent<Props, State> { constructor(props: Props) { super(props); @@ -48,7 +49,9 @@ export class CopyQualityGateForm extends React.PureComponent<Props, State> { this.setState({ name: event.currentTarget.value }); }; - handleCopy = () => { + handleCopy = (event: React.FormEvent) => { + event.preventDefault(); + const { qualityGate } = this.props; const { name } = this.state; @@ -61,35 +64,40 @@ export class CopyQualityGateForm extends React.PureComponent<Props, State> { render() { const { qualityGate } = this.props; const { name } = this.state; - const confirmDisable = !name || (qualityGate && qualityGate.name === name); + const buttonDisabled = !name || (qualityGate && qualityGate.name === name); return ( - <ConfirmModal - confirmButtonText={translate('copy')} - confirmDisable={confirmDisable} - header={translate('quality_gates.copy')} + <Modal + headerTitle={translate('quality_gates.copy')} onClose={this.props.onClose} - onConfirm={this.handleCopy} - size="small" - > - <MandatoryFieldsExplanation className="modal-field" /> - <div className="modal-field"> - <label htmlFor="quality-gate-form-name"> - {translate('name')} - <MandatoryFieldMarker /> - </label> - <input - autoFocus - id="quality-gate-form-name" - maxLength={100} - onChange={this.handleNameChange} - required - size={50} - type="text" - value={name} - /> - </div> - </ConfirmModal> + body={ + <form id={FORM_ID} onSubmit={this.handleCopy}> + <MandatoryFieldsExplanation /> + <FormField + label={translate('name')} + htmlFor="quality-gate-form-name" + required + className="sw-my-2" + > + <InputField + autoFocus + id="quality-gate-form-name" + maxLength={100} + onChange={this.handleNameChange} + size="auto" + type="text" + value={name} + /> + </FormField> + </form> + } + primaryButton={ + <ButtonPrimary autoFocus type="submit" disabled={buttonDisabled} form={FORM_ID}> + {translate('copy')} + </ButtonPrimary> + } + secondaryButtonLabel={translate('cancel')} + /> ); } } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx index b58782e8634..a022e111554 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx @@ -17,16 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { DangerButtonPrimary, Modal } from 'design-system'; import * as React from 'react'; import { deleteQualityGate } from '../../../api/quality-gates'; -import { Button } from '../../../components/controls/buttons'; -import ConfirmButton from '../../../components/controls/ConfirmButton'; import { Router, withRouter } from '../../../components/hoc/withRouter'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getQualityGatesUrl } from '../../../helpers/urls'; import { QualityGate } from '../../../types/types'; interface Props { + readonly onClose: () => void; onDelete: () => Promise<void>; qualityGate: QualityGate; router: Router; @@ -46,26 +46,17 @@ export class DeleteQualityGateForm extends React.PureComponent<Props> { const { qualityGate } = this.props; return ( - <ConfirmButton - confirmButtonText={translate('delete')} - isDestructive - modalBody={translateWithParameters( - 'quality_gates.delete.confirm.message', - qualityGate.name, - )} - modalHeader={translate('quality_gates.delete')} - onConfirm={this.onDelete} - > - {({ onClick }) => ( - <Button - className="little-spacer-left button-red" - id="quality-gate-delete" - onClick={onClick} - > + <Modal + headerTitle={translate('quality_gates.delete')} + onClose={this.props.onClose} + body={translateWithParameters('quality_gates.delete.confirm.message', qualityGate.name)} + primaryButton={ + <DangerButtonPrimary autoFocus type="submit" onClick={this.onDelete}> {translate('delete')} - </Button> - )} - </ConfirmButton> + </DangerButtonPrimary> + } + secondaryButtonLabel={translate('cancel')} + /> ); } } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx index 4aff608825c..ed02c326faa 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx @@ -17,12 +17,22 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { + ActionsDropdown, + Badge, + ButtonSecondary, + DangerButtonPrimary, + FlagWarningIcon, + ItemButton, + ItemDangerButton, + ItemDivider, + SubTitle, +} from 'design-system'; +import { countBy } from 'lodash'; import * as React from 'react'; import { setQualityGateAsDefault } from '../../../api/quality-gates'; -import ModalButton from '../../../components/controls/ModalButton'; import Tooltip from '../../../components/controls/Tooltip'; -import { Button } from '../../../components/controls/buttons'; -import AlertWarnIcon from '../../../components/icons/AlertWarnIcon'; import { translate } from '../../../helpers/l10n'; import { CaycStatus, QualityGate } from '../../../types/types'; import BuiltInQualityGateBadge from './BuiltInQualityGateBadge'; @@ -40,122 +50,180 @@ interface Props { const TOOLTIP_MOUSE_LEAVE_DELAY = 0.3; -export default class DetailsHeader extends React.PureComponent<Props> { - handleActionRefresh = () => { - const { refreshItem, refreshList } = this.props; +export default function DetailsHeader({ + refreshItem, + refreshList, + onSetDefault, + qualityGate, +}: Props) { + const [isRenameFormOpen, setIsRenameFormOpen] = React.useState(false); + const [isCopyFormOpen, setIsCopyFormOpen] = React.useState(false); + const [isRemoveFormOpen, setIsRemoveFormOpen] = React.useState(false); + const actions = qualityGate.actions ?? {}; + const actionsCount = countBy([ + actions.rename, + actions.copy, + actions.delete, + actions.setAsDefault, + ])['true']; + const canEdit = Boolean(actions?.manageConditions); + + const handleActionRefresh = () => { return Promise.all([refreshItem(), refreshList()]).then( () => {}, () => {}, ); }; - handleSetAsDefaultClick = () => { - const { qualityGate } = this.props; + const handleSetAsDefaultClick = () => { if (!qualityGate.isDefault) { // Optimistic update - this.props.onSetDefault(); + onSetDefault(); setQualityGateAsDefault({ name: qualityGate.name }).then( - this.handleActionRefresh, - this.handleActionRefresh, + handleActionRefresh, + handleActionRefresh, ); } }; - render() { - const { qualityGate } = this.props; - const actions = qualityGate.actions || ({} as any); - const canEdit = Boolean(actions?.manageConditions); - - return ( - <div className="layout-page-header-panel layout-page-main-header issues-main-header"> - <div className="layout-page-header-panel-inner layout-page-main-header-inner"> - <div className="layout-page-main-inner"> - <div className="pull-left display-flex-center"> - <h2>{qualityGate.name}</h2> - {qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="spacer-left" />} - {qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && ( - <Tooltip overlay={<CaycBadgeTooltip />} mouseLeaveDelay={TOOLTIP_MOUSE_LEAVE_DELAY}> - <AlertWarnIcon className="spacer-left" description={<CaycBadgeTooltip />} /> - </Tooltip> - )} - </div> - - <div className="pull-right"> - {actions.rename && ( - <ModalButton - modal={({ onClose }) => ( - <RenameQualityGateForm - onClose={onClose} - onRename={this.props.refreshList} - qualityGate={qualityGate} - /> - )} + return ( + <> + <div className="it__layout-page-main-header sw-flex sw-items-center sw-justify-between sw-mb-9"> + <div className="sw-flex sw-flex-col"> + <div className="sw-flex sw-items-center"> + <SubTitle className="sw-m-0">{qualityGate.name}</SubTitle> + {qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && ( + <Tooltip overlay={<CaycBadgeTooltip />} mouseLeaveDelay={TOOLTIP_MOUSE_LEAVE_DELAY}> + <FlagWarningIcon className="sw-ml-2" description={<CaycBadgeTooltip />} /> + </Tooltip> + )} + </div> + <div className="sw-flex sw-gap-2 sw-mt-4"> + {qualityGate.isDefault && <Badge>{translate('default')}</Badge>} + {qualityGate.isBuiltIn && <BuiltInQualityGateBadge />} + </div> + </div> + {actionsCount === 1 && ( + <> + {actions.rename && ( + <ButtonSecondary onClick={() => setIsRenameFormOpen(true)}> + {translate('rename')} + </ButtonSecondary> + )} + {actions.copy && ( + <Tooltip + overlay={ + qualityGate.caycStatus === CaycStatus.NonCompliant + ? translate('quality_gates.cannot_copy_no_cayc') + : null + } + > + <ButtonSecondary + disabled={qualityGate.caycStatus === CaycStatus.NonCompliant} + onClick={() => setIsCopyFormOpen(true)} > - {({ onClick }) => ( - <Button id="quality-gate-rename" onClick={onClick}> - {translate('rename')} - </Button> - )} - </ModalButton> - )} - {actions.copy && ( - <ModalButton - modal={({ onClose }) => ( - <CopyQualityGateForm - onClose={onClose} - onCopy={this.handleActionRefresh} - qualityGate={qualityGate} - /> - )} + {translate('copy')} + </ButtonSecondary> + </Tooltip> + )} + {actions.setAsDefault && ( + <Tooltip + overlay={ + qualityGate.caycStatus === CaycStatus.NonCompliant + ? translate('quality_gates.cannot_set_default_no_cayc') + : null + } + > + <ButtonSecondary + disabled={qualityGate.caycStatus === CaycStatus.NonCompliant} + onClick={handleSetAsDefaultClick} > - {({ onClick }) => ( - <Tooltip - overlay={ - qualityGate.caycStatus === CaycStatus.NonCompliant - ? translate('quality_gates.cannot_copy_no_cayc') - : null - } - > - <Button - className="little-spacer-left" - id="quality-gate-copy" - onClick={onClick} - disabled={qualityGate.caycStatus === CaycStatus.NonCompliant} - > - {translate('copy')} - </Button> - </Tooltip> - )} - </ModalButton> - )} - {actions.setAsDefault && ( - <Tooltip - overlay={ - qualityGate.caycStatus === CaycStatus.NonCompliant - ? translate('quality_gates.cannot_set_default_no_cayc') - : null - } + {translate('set_as_default')} + </ButtonSecondary> + </Tooltip> + )} + {actions.delete && ( + <DangerButtonPrimary onClick={() => setIsRemoveFormOpen(true)}> + {translate('delete')} + </DangerButtonPrimary> + )} + </> + )} + + {actionsCount > 1 && ( + <ActionsDropdown allowResizing id="quality-gate-actions"> + {actions.rename && ( + <ItemButton onClick={() => setIsRenameFormOpen(true)}> + {translate('rename')} + </ItemButton> + )} + {actions.copy && ( + <Tooltip + overlay={ + qualityGate.caycStatus === CaycStatus.NonCompliant + ? translate('quality_gates.cannot_copy_no_cayc') + : null + } + > + <ItemButton + disabled={qualityGate.caycStatus === CaycStatus.NonCompliant} + onClick={() => setIsCopyFormOpen(true)} > - <Button - className="little-spacer-left" - disabled={qualityGate.caycStatus === CaycStatus.NonCompliant} - id="quality-gate-toggle-default" - onClick={this.handleSetAsDefaultClick} - > - {translate('set_as_default')} - </Button> - </Tooltip> - )} - {actions.delete && ( - <DeleteQualityGateForm - onDelete={this.props.refreshList} - qualityGate={qualityGate} - /> - )} - </div> - </div> - </div> + {translate('copy')} + </ItemButton> + </Tooltip> + )} + {actions.setAsDefault && ( + <Tooltip + overlay={ + qualityGate.caycStatus === CaycStatus.NonCompliant + ? translate('quality_gates.cannot_set_default_no_cayc') + : null + } + > + <ItemButton + disabled={qualityGate.caycStatus === CaycStatus.NonCompliant} + onClick={handleSetAsDefaultClick} + > + {translate('set_as_default')} + </ItemButton> + </Tooltip> + )} + {actions.delete && ( + <> + <ItemDivider /> + <ItemDangerButton onClick={() => setIsRemoveFormOpen(true)}> + {translate('delete')} + </ItemDangerButton> + </> + )} + </ActionsDropdown> + )} </div> - ); - } + + {isRenameFormOpen && ( + <RenameQualityGateForm + onClose={() => setIsRenameFormOpen(false)} + onRename={handleActionRefresh} + qualityGate={qualityGate} + /> + )} + + {isCopyFormOpen && ( + <CopyQualityGateForm + onClose={() => setIsCopyFormOpen(false)} + onCopy={handleActionRefresh} + qualityGate={qualityGate} + /> + )} + + {isRemoveFormOpen && ( + <DeleteQualityGateForm + onClose={() => setIsRemoveFormOpen(false)} + onDelete={refreshList} + qualityGate={qualityGate} + /> + )} + </> + ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.tsx index f1dc6dc79fa..9d2c26bf662 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.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 { ButtonPrimary, FormField, InputField, Modal } from 'design-system/lib'; import * as React from 'react'; import { renameQualityGate } from '../../../api/quality-gates'; -import ConfirmModal from '../../../components/controls/ConfirmModal'; -import { withRouter, WithRouterProps } from '../../../components/hoc/withRouter'; -import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker'; +import { WithRouterProps, withRouter } from '../../../components/hoc/withRouter'; import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation'; import { translate } from '../../../helpers/l10n'; import { getQualityGateUrl } from '../../../helpers/urls'; @@ -37,6 +36,8 @@ interface State { name: string; } +const FORM_ID = 'rename-quality-gate'; + class RenameQualityGateForm extends React.PureComponent<Props, State> { constructor(props: Props) { super(props); @@ -47,13 +48,15 @@ class RenameQualityGateForm extends React.PureComponent<Props, State> { this.setState({ name: event.currentTarget.value }); }; - handleRename = () => { + handleRename = (event: React.FormEvent) => { + event.preventDefault(); + const { qualityGate, router } = this.props; const { name } = this.state; return renameQualityGate({ currentName: qualityGate.name, name }).then(() => { - this.props.onRename(); router.push(getQualityGateUrl(name)); + this.props.onRename(); }); }; @@ -63,32 +66,37 @@ class RenameQualityGateForm extends React.PureComponent<Props, State> { const confirmDisable = !name || (qualityGate && qualityGate.name === name); return ( - <ConfirmModal - confirmButtonText={translate('rename')} - confirmDisable={confirmDisable} - header={translate('quality_gates.rename')} + <Modal + headerTitle={translate('quality_gates.rename')} onClose={this.props.onClose} - onConfirm={this.handleRename} - size="small" - > - <MandatoryFieldsExplanation className="modal-field" /> - <div className="modal-field"> - <label htmlFor="quality-gate-form-name"> - {translate('name')} - <MandatoryFieldMarker /> - </label> - <input - autoFocus - id="quality-gate-form-name" - maxLength={100} - onChange={this.handleNameChange} - required - size={50} - type="text" - value={name} - /> - </div> - </ConfirmModal> + body={ + <form id={FORM_ID} onSubmit={this.handleRename}> + <MandatoryFieldsExplanation /> + <FormField + label={translate('name')} + htmlFor="quality-gate-form-name" + required + className="sw-my-2" + > + <InputField + autoFocus + id="quality-gate-form-name" + maxLength={100} + onChange={this.handleNameChange} + size="auto" + type="text" + value={name} + /> + </FormField> + </form> + } + primaryButton={ + <ButtonPrimary autoFocus type="submit" disabled={confirmDisable} form={FORM_ID}> + {translate('rename')} + </ButtonPrimary> + } + secondaryButtonLabel={translate('cancel')} + /> ); } } 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 8522ed7a488..ec15cdda312 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 @@ -96,7 +96,9 @@ it('should be able to create a quality gate then delete it', async () => { // Delete the quality gate await user.click(newQG); - const deleteButton = await screen.findByRole('button', { name: 'delete' }); + + await user.click(screen.getByLabelText('menu')); + const deleteButton = screen.getByRole('menuitem', { name: 'delete' }); await user.click(deleteButton); const popup = screen.getByRole('dialog'); const dialogDeleteButton = within(popup).getByRole('button', { name: 'delete' }); @@ -114,7 +116,8 @@ it('should be able to copy a quality gate which is CAYC compliant', async () => const notDefaultQualityGate = await screen.findByText('Sonar way'); await user.click(notDefaultQualityGate); - const copyButton = await screen.findByRole('button', { name: 'copy' }); + await user.click(screen.getByLabelText('menu')); + const copyButton = screen.getByRole('menuitem', { name: 'copy' }); await user.click(copyButton); const nameInput = screen.getByRole('textbox', { name: /name.*/ }); @@ -133,8 +136,8 @@ it('should not be able to copy a quality gate which is not CAYC compliant', asyn const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily'); await user.click(notDefaultQualityGate); - - const copyButton = await screen.findByRole('button', { name: 'copy' }); + await user.click(screen.getByLabelText('menu')); + const copyButton = screen.getByRole('menuitem', { name: 'copy' }); expect(copyButton).toBeDisabled(); }); @@ -143,8 +146,8 @@ it('should be able to rename a quality gate', async () => { const user = userEvent.setup(); handler.setIsAdmin(true); renderQualityGateApp(); - - const renameButton = await screen.findByRole('button', { name: 'rename' }); + await user.click(await screen.findByLabelText('menu')); + const renameButton = screen.getByRole('menuitem', { name: 'rename' }); await user.click(renameButton); const nameInput = screen.getByRole('textbox', { name: /name.*/ }); @@ -162,7 +165,8 @@ it('should not be able to set as default a quality gate which is not CAYC compli const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily'); await user.click(notDefaultQualityGate); - const setAsDefaultButton = screen.getByRole('button', { name: 'set_as_default' }); + await user.click(screen.getByLabelText('menu')); + const setAsDefaultButton = screen.getByRole('menuitem', { name: 'set_as_default' }); expect(setAsDefaultButton).toBeDisabled(); }); @@ -173,7 +177,8 @@ it('should be able to set as default a quality gate which is CAYC compliant', as 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(screen.getByLabelText('menu')); + const setAsDefaultButton = screen.getByRole('menuitem', { name: 'set_as_default' }); await user.click(setAsDefaultButton); expect(screen.getByRole('button', { name: /Sonar way default/ })).toBeInTheDocument(); }); |