diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2023-03-16 13:53:23 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-03-27 20:03:03 +0000 |
commit | bb35852a6179fa4f1533132bcbc2d8438c449247 (patch) | |
tree | 0705ce252e151528d514a31df4a6c6dc163fd14d /server/sonar-web | |
parent | 9bbd0d7e20e3753fa7a0d2c37ecd0b936cffaae2 (diff) | |
download | sonarqube-bb35852a6179fa4f1533132bcbc2d8438c449247.tar.gz sonarqube-bb35852a6179fa4f1533132bcbc2d8438c449247.zip |
SONAR-18776 New UI for Header Meta (analysis status, homepage, version)
Diffstat (limited to 'server/sonar-web')
37 files changed, 1110 insertions, 1411 deletions
diff --git a/server/sonar-web/design-system/src/components/FlagMessage.tsx b/server/sonar-web/design-system/src/components/FlagMessage.tsx new file mode 100644 index 00000000000..3a3aed03c94 --- /dev/null +++ b/server/sonar-web/design-system/src/components/FlagMessage.tsx @@ -0,0 +1,122 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import styled from '@emotion/styled'; +import classNames from 'classnames'; +import * as React from 'react'; +import tw from 'twin.macro'; +import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; +import { ThemeColors } from '../types/theme'; +import { FlagErrorIcon, FlagInfoIcon, FlagSuccessIcon, FlagWarningIcon } from './icons'; + +export type Variant = 'error' | 'warning' | 'success' | 'info'; + +interface Props { + ariaLabel: string; + variant: Variant; +} + +interface VariantInformation { + backGroundColor: ThemeColors; + borderColor: ThemeColors; + icon: JSX.Element; + role: string; +} + +function getVariantInfo(variant: Variant): VariantInformation { + const variantList: Record<Variant, VariantInformation> = { + error: { + icon: <FlagErrorIcon />, + borderColor: 'errorBorder', + backGroundColor: 'errorBackground', + role: 'alert', + }, + warning: { + icon: <FlagWarningIcon />, + borderColor: 'warningBorder', + backGroundColor: 'warningBackground', + role: 'alert', + }, + success: { + icon: <FlagSuccessIcon />, + borderColor: 'successBorder', + backGroundColor: 'successBackground', + role: 'status', + }, + info: { + icon: <FlagInfoIcon />, + borderColor: 'infoBorder', + backGroundColor: 'infoBackground', + role: 'status', + }, + }; + + return variantList[variant]; +} + +export function FlagMessage(props: Props & React.HTMLAttributes<HTMLDivElement>) { + const { ariaLabel, className, variant, ...domProps } = props; + const variantInfo = getVariantInfo(variant); + + return ( + <StyledFlag + aria-label={ariaLabel} + className={classNames('alert', className)} + role={variantInfo.role} + variantInfo={variantInfo} + {...domProps} + > + <StyledFlagInner> + <StyledFlagIcon variantInfo={variantInfo}>{variantInfo.icon}</StyledFlagIcon> + <StyledFlagContent>{props.children}</StyledFlagContent> + </StyledFlagInner> + </StyledFlag> + ); +} + +export const StyledFlag = styled.div<{ + variantInfo: VariantInformation; +}>` + ${tw`sw-inline-flex`} + ${tw`sw-min-h-10`} + ${tw`sw-rounded-1`} + border: ${({ variantInfo }) => themeBorder('default', variantInfo.borderColor)}; + background-color: ${themeColor('flagMessageBackground')}; +`; + +const StyledFlagInner = styled.div` + ${tw`sw-flex sw-items-stretch`} + ${tw`sw-box-border`} +`; + +const StyledFlagIcon = styled.div<{ variantInfo: VariantInformation }>` + ${tw`sw-flex sw-justify-center sw-items-center`} + ${tw`sw-rounded-l-1`} + ${tw`sw-px-3`} + background-color: ${({ variantInfo }) => themeColor(variantInfo.backGroundColor)}; +`; + +const StyledFlagContent = styled.div` + ${tw`sw-flex sw-flex-auto sw-items-center`} + ${tw`sw-overflow-auto`} + ${tw`sw-text-left`} + ${tw`sw-mx-3 sw-my-2`} + ${tw`sw-body-sm`} + color: ${themeContrast('flagMessageBackground')}; +`; diff --git a/server/sonar-web/design-system/src/components/__tests__/FlagMessage-test.tsx b/server/sonar-web/design-system/src/components/__tests__/FlagMessage-test.tsx new file mode 100644 index 00000000000..b51dddcad2a --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/FlagMessage-test.tsx @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { screen } from '@testing-library/react'; +import { render } from '../../helpers/testUtils'; +import { FCProps } from '../../types/misc'; +import { FlagMessage, Variant } from '../FlagMessage'; + +it.each([ + ['error', 'alert', '1px solid rgb(249,112,102)'], + ['warning', 'alert', '1px solid rgb(248,205,92)'], + ['success', 'status', '1px solid rgb(50,213,131)'], + ['info', 'status', '1px solid rgb(110,185,228)'], +])('should render properly for "%s" variant', (variant: Variant, expectedRole, color) => { + renderFlagMessage({ variant }); + + const item = screen.getByRole(expectedRole); + expect(item).toBeInTheDocument(); + expect(item).toHaveStyle({ border: color }); +}); + +function renderFlagMessage(props: Partial<FCProps<typeof FlagMessage>> = {}) { + return render( + <FlagMessage ariaLabel="label" variant="error" {...props}> + This is an error! + </FlagMessage> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLikeMergeInformation-test.tsx b/server/sonar-web/design-system/src/components/icons/FlagErrorIcon.tsx index 32f1bbedf4b..519b9a387a0 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLikeMergeInformation-test.tsx +++ b/server/sonar-web/design-system/src/components/icons/FlagErrorIcon.tsx @@ -17,26 +17,18 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockMainBranch, mockPullRequest } from '../../../../../../helpers/mocks/branch-like'; -import { - CurrentBranchLikeMergeInformation, - CurrentBranchLikeMergeInformationProps, -} from '../CurrentBranchLikeMergeInformation'; +import { useTheme } from '@emotion/react'; +import { themeColor } from '../../helpers/theme'; +import { CustomIcon, IconProps } from './Icon'; -it('should render correctly', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); -}); - -it('should not render for non-pull-request branch like', () => { - const wrapper = shallowRender({ currentBranchLike: mockMainBranch() }); - expect(wrapper.type()).toBeNull(); -}); - -function shallowRender(props?: Partial<CurrentBranchLikeMergeInformationProps>) { - return shallow( - <CurrentBranchLikeMergeInformation currentBranchLike={mockPullRequest()} {...props} /> +export function FlagErrorIcon({ fill = 'iconError', ...iconProps }: IconProps) { + const theme = useTheme(); + return ( + <CustomIcon {...iconProps}> + <path + d="M7.364 1.707a1 1 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414 0L1.707 8.778a1 1 0 0 1 0-1.414l5.657-5.657ZM7 5a1 1 0 0 1 2 0v3a1 1 0 1 1-2 0V5Zm1 5a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z" + style={{ fill: themeColor(fill)({ theme }) }} + /> + </CustomIcon> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavWarnings-test.tsx b/server/sonar-web/design-system/src/components/icons/FlagInfoIcon.tsx index c19a6d65b56..3aef3045e4a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavWarnings-test.tsx +++ b/server/sonar-web/design-system/src/components/icons/FlagInfoIcon.tsx @@ -17,20 +17,18 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockTaskWarning } from '../../../../../helpers/mocks/tasks'; -import ComponentNavWarnings from '../ComponentNavWarnings'; +import { useTheme } from '@emotion/react'; +import { themeColor } from '../../helpers/theme'; +import { CustomIcon, IconProps } from './Icon'; -it('should render', () => { - const wrapper = shallow( - <ComponentNavWarnings - componentKey="foo" - isBranch={true} - onWarningDismiss={jest.fn()} - warnings={[mockTaskWarning({ message: 'warning 1' })]} - /> +export function FlagInfoIcon({ fill = 'iconInfo', ...iconProps }: IconProps) { + const theme = useTheme(); + return ( + <CustomIcon {...iconProps}> + <path + d="M14 8A6 6 0 1 1 2 8a6 6 0 0 1 12 0Zm-5 3a1 1 0 1 1-2 0V8a1 1 0 0 1 2 0v3ZM8 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" + style={{ fill: themeColor(fill)({ theme }) }} + /> + </CustomIcon> ); - wrapper.setState({ modal: true }); - expect(wrapper).toMatchSnapshot(); -}); +} diff --git a/server/sonar-web/design-system/src/components/icons/FlagSuccessIcon.tsx b/server/sonar-web/design-system/src/components/icons/FlagSuccessIcon.tsx new file mode 100644 index 00000000000..748b8a5bc31 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/FlagSuccessIcon.tsx @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { useTheme } from '@emotion/react'; +import { themeColor } from '../../helpers/theme'; +import { CustomIcon, IconProps } from './Icon'; + +export function FlagSuccessIcon({ fill = 'iconSuccess', ...iconProps }: IconProps) { + const theme = useTheme(); + return ( + <CustomIcon {...iconProps}> + <path + d="M8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12Zm3.207-6.793a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l3.5-3.5Z" + style={{ fill: themeColor(fill)({ theme }) }} + /> + </CustomIcon> + ); +} diff --git a/server/sonar-web/design-system/src/components/icons/FlagWarningIcon.tsx b/server/sonar-web/design-system/src/components/icons/FlagWarningIcon.tsx new file mode 100644 index 00000000000..0550bbb9c96 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/FlagWarningIcon.tsx @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { useTheme } from '@emotion/react'; +import { themeColor } from '../../helpers/theme'; +import { CustomIcon, IconProps } from './Icon'; + +export function FlagWarningIcon({ fill = 'iconWarning', ...iconProps }: IconProps) { + const theme = useTheme(); + return ( + <CustomIcon {...iconProps}> + <path + d="M14.41 12.55a1 1 0 0 1-.893 1.45H2.625a1 1 0 0 1-.892-1.45L7.178 1.766a1 1 0 0 1 1.786 0l5.445 10.782ZM7 6a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0V6Zm1 5a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z" + style={{ fill: themeColor(fill)({ theme }) }} + /> + </CustomIcon> + ); +} diff --git a/server/sonar-web/design-system/src/components/icons/HomeFillIcon.tsx b/server/sonar-web/design-system/src/components/icons/HomeFillIcon.tsx new file mode 100644 index 00000000000..72d7cd65059 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/HomeFillIcon.tsx @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { useTheme } from '@emotion/react'; +import { themeColor } from '../../helpers/theme'; +import { CustomIcon, IconProps } from './Icon'; + +export default function HomeFillIcon({ fill = 'iconFavorite', ...iconProps }: IconProps) { + const theme = useTheme(); + const fillColor = themeColor(fill)({ theme }); + return ( + <CustomIcon {...iconProps}> + <path + d="M6.9995 0.280296C6.602 0.280296 6.21634 0.415622 5.906 0.664003L0.657 4.864C0.242 5.196 0 5.699 0 6.23V13.25C0 13.7141 0.184374 14.1593 0.512563 14.4874C0.840752 14.8156 1.28587 15 1.75 15H5.25C5.44891 15 5.63968 14.921 5.78033 14.7803C5.92098 14.6397 6 14.4489 6 14.25V9H8V14.25C8 14.4489 8.07902 14.6397 8.21967 14.7803C8.36032 14.921 8.55109 15 8.75 15H12.25C12.7141 15 13.1592 14.8156 13.4874 14.4874C13.8156 14.1593 14 13.7141 14 13.25V6.231C14 5.699 13.758 5.196 13.343 4.864L8.093 0.664003C7.78266 0.415622 7.397 0.280296 6.9995 0.280296Z" + fill={fillColor} + /> + </CustomIcon> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.css b/server/sonar-web/design-system/src/components/icons/HomeIcon.tsx index 93e8cf71968..a4a6d07ba01 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.css +++ b/server/sonar-web/design-system/src/components/icons/HomeIcon.tsx @@ -17,10 +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. */ -.header-meta-warnings .alert { - margin-bottom: 5px; -} +import { HomeIcon } from '@primer/octicons-react'; +import { OcticonHoc } from './Icon'; -.header-meta-warnings .alert-content { - padding: 6px 8px; -} +export default OcticonHoc(HomeIcon); diff --git a/server/sonar-web/design-system/src/components/icons/index.ts b/server/sonar-web/design-system/src/components/icons/index.ts index 8b30b791711..3b681fbe1b8 100644 --- a/server/sonar-web/design-system/src/components/icons/index.ts +++ b/server/sonar-web/design-system/src/components/icons/index.ts @@ -18,6 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ export { default as ClockIcon } from './ClockIcon'; +export { FlagErrorIcon } from './FlagErrorIcon'; +export { FlagInfoIcon } from './FlagInfoIcon'; +export { FlagSuccessIcon } from './FlagSuccessIcon'; +export { FlagWarningIcon } from './FlagWarningIcon'; +export { default as HomeFillIcon } from './HomeFillIcon'; +export { default as HomeIcon } from './HomeIcon'; export { default as MenuHelpIcon } from './MenuHelpIcon'; export { default as MenuSearchIcon } from './MenuSearchIcon'; export { default as OpenNewTabIcon } from './OpenNewTabIcon'; diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index e7bdcf4ca80..452c570433e 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -24,6 +24,7 @@ export { default as DeferredSpinner } from './DeferredSpinner'; export { default as Dropdown } from './Dropdown'; export * from './DropdownMenu'; export { default as DropdownToggler } from './DropdownToggler'; +export { FlagMessage } from './FlagMessage'; export * from './GenericAvatar'; export * from './icons'; export { default as InputSearch } from './InputSearch'; diff --git a/server/sonar-web/scripts/build-design-system.js b/server/sonar-web/scripts/build-design-system.js index 737357a2c37..7c4241f1a29 100644 --- a/server/sonar-web/scripts/build-design-system.js +++ b/server/sonar-web/scripts/build-design-system.js @@ -36,8 +36,8 @@ function buildDesignSystem(callback) { console.log(chalk.red.bold(data.toString())); }); - build.on('exit', (code) => { - if (code === 0) { + build.on('exit', function (code) { + if (code === 0 && callback) { callback(); } }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx new file mode 100644 index 00000000000..00b8bda2c62 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx @@ -0,0 +1,90 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { Link } from 'design-system'; +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { useLocation } from 'react-router-dom'; +import { hasMessage, translate } from '../../../../helpers/l10n'; +import { getComponentBackgroundTaskUrl } from '../../../../helpers/urls'; +import { Task } from '../../../../types/tasks'; +import { Component } from '../../../../types/types'; + +interface Props { + component: Component; + currentTask: Task; + currentTaskOnSameBranch?: boolean; + onLeave: () => void; +} + +export function AnalysisErrorMessage(props: Props) { + const { component, currentTask, currentTaskOnSameBranch } = props; + + const location = useLocation(); + + const backgroundTaskUrl = getComponentBackgroundTaskUrl(component.key); + const canSeeBackgroundTasks = component.configuration?.showBackgroundTasks; + const isOnBackgroundTaskPage = location.pathname === backgroundTaskUrl.pathname; + + const branch = + currentTask.branch ?? + `${currentTask.pullRequest ?? ''}${ + currentTask.pullRequestTitle ? ' - ' + currentTask.pullRequestTitle : '' + }`; + + let messageKey; + if (currentTaskOnSameBranch === false && branch) { + messageKey = 'component_navigation.status.failed_branch'; + } else { + messageKey = 'component_navigation.status.failed'; + } + + let type; + if (hasMessage('background_task.type', currentTask.type)) { + messageKey += '_X'; + type = translate('background_task.type', currentTask.type); + } + + let url; + let stacktrace; + if (canSeeBackgroundTasks) { + messageKey += '.admin'; + + if (isOnBackgroundTaskPage) { + messageKey += '.help'; + stacktrace = translate('background_tasks.show_stacktrace'); + } else { + messageKey += '.link'; + url = ( + <Link onClick={props.onLeave} to={backgroundTaskUrl}> + {translate('background_tasks.page')} + </Link> + ); + } + } + + return ( + <FormattedMessage + defaultMessage={translate(messageKey)} + id={messageKey} + values={{ branch, url, stacktrace, type }} + /> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx new file mode 100644 index 00000000000..46dc0f75861 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { ButtonLink } from '../../../../components/controls/buttons'; +import Modal from '../../../../components/controls/Modal'; +import { hasMessage, translate } from '../../../../helpers/l10n'; +import { Task } from '../../../../types/tasks'; +import { Component } from '../../../../types/types'; +import { AnalysisErrorMessage } from './AnalysisErrorMessage'; +import { AnalysisLicenseError } from './AnalysisLicenseError'; + +interface Props { + component: Component; + currentTask: Task; + currentTaskOnSameBranch?: boolean; + onClose: () => void; +} + +export function AnalysisErrorModal(props: Props) { + const { component, currentTask, currentTaskOnSameBranch } = props; + + const header = translate('error'); + + const licenseError = + currentTask.errorType && + hasMessage('license.component_navigation.button', currentTask.errorType); + + return ( + <Modal contentLabel={header} onRequestClose={props.onClose}> + <header className="modal-head"> + <h2>{header}</h2> + </header> + + <div className="modal-body modal-container"> + {licenseError ? ( + <AnalysisLicenseError currentTask={currentTask} /> + ) : ( + <AnalysisErrorMessage + component={component} + currentTask={currentTask} + currentTaskOnSameBranch={currentTaskOnSameBranch} + onLeave={props.onClose} + /> + )} + </div> + + <footer className="modal-foot"> + <ButtonLink className="js-modal-close" onClick={props.onClose}> + {translate('close')} + </ButtonLink> + </footer> + </Modal> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx new file mode 100644 index 00000000000..0c4e6f66c85 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import Link from '../../../../components/common/Link'; +import { translate, translateWithParameters } from '../../../../helpers/l10n'; +import { ComponentQualifier } from '../../../../types/component'; +import { Task } from '../../../../types/tasks'; +import { AppStateContext } from '../../app-state/AppStateContext'; +import { useLicenseIsValid } from './useLicenseIsValid'; + +interface Props { + currentTask: Task; +} + +export function AnalysisLicenseError(props: Props) { + const { currentTask } = props; + const appState = React.useContext(AppStateContext); + const [licenseIsValid, loading] = useLicenseIsValid(); + + if (loading || !currentTask.errorType) { + return null; + } + + if (licenseIsValid && currentTask.errorType !== 'LICENSING_LOC') { + return ( + <> + {translateWithParameters( + 'component_navigation.status.last_blocked_due_to_bad_license_X', + translate('qualifier', currentTask.componentQualifier ?? ComponentQualifier.Project) + )} + </> + ); + } + + return ( + <> + <span className="little-spacer-right">{currentTask.errorMessage}</span> + {appState.canAdmin ? ( + <Link to="/admin/extension/license/app"> + {translate('license.component_navigation.button', currentTask.errorType)}. + </Link> + ) : ( + translate('please_contact_administrator') + )} + </> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx new file mode 100644 index 00000000000..c08700a3702 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx @@ -0,0 +1,119 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * 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, FlagMessage, Link } from 'design-system'; +import * as React from 'react'; +import AnalysisWarningsModal from '../../../../components/common/AnalysisWarningsModal'; +import { translate } from '../../../../helpers/l10n'; +import { Task, TaskStatuses, TaskWarning } from '../../../../types/tasks'; +import { Component } from '../../../../types/types'; +import { AnalysisErrorModal } from './AnalysisErrorModal'; + +export interface HeaderMetaProps { + currentTask?: Task; + currentTaskOnSameBranch?: boolean; + component: Component; + isInProgress?: boolean; + isPending?: boolean; + onWarningDismiss: () => void; + warnings: TaskWarning[]; +} + +export function AnalysisStatus(props: HeaderMetaProps) { + const { component, currentTask, currentTaskOnSameBranch, isInProgress, isPending, warnings } = + props; + + const [modalIsVisible, setDisplayModal] = React.useState(false); + const openModal = React.useCallback(() => { + setDisplayModal(true); + }, [setDisplayModal]); + const closeModal = React.useCallback(() => { + setDisplayModal(false); + }, [setDisplayModal]); + + if (isInProgress || isPending) { + return ( + <div className="sw-flex sw-items-center"> + <DeferredSpinner timeout={0} /> + <span className="sw-ml-1"> + {isInProgress + ? translate('project_navigation.analysis_status.in_progress') + : translate('project_navigation.analysis_status.pending')} + </span> + </div> + ); + } + + if (currentTask?.status === TaskStatuses.Failed) { + return ( + <> + <FlagMessage ariaLabel={translate('alert.tooltip.error')} variant="error"> + <span>{translate('project_navigation.analysis_status.failed')}</span> + <Link + className="sw-ml-1" + blurAfterClick={true} + onClick={openModal} + preventDefault={true} + to={{}} + > + {translate('project_navigation.analysis_status.details_link')} + </Link> + </FlagMessage> + {modalIsVisible && ( + <AnalysisErrorModal + component={component} + currentTask={currentTask} + currentTaskOnSameBranch={currentTaskOnSameBranch} + onClose={closeModal} + /> + )} + </> + ); + } + + if (warnings.length > 0) { + return ( + <> + <FlagMessage ariaLabel={translate('alert.tooltip.warning')} variant="warning"> + <span>{translate('project_navigation.analysis_status.warnings')}</span> + <Link + className="sw-ml-1" + blurAfterClick={true} + onClick={openModal} + preventDefault={true} + to={{}} + > + {translate('project_navigation.analysis_status.details_link')} + </Link> + </FlagMessage> + {modalIsVisible && ( + <AnalysisWarningsModal + componentKey={component.key} + onClose={closeModal} + taskId={currentTask?.id} + onWarningDismiss={props.onWarningDismiss} + warnings={warnings} + /> + )} + </> + ); + } + + return null; +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx index 1c65589ead7..8ca22c7edfb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx @@ -27,11 +27,10 @@ import { } from '../../../../types/alm-settings'; import { BranchLike } from '../../../../types/branch-like'; import { ComponentQualifier } from '../../../../types/component'; -import { Task, TaskStatuses, TaskWarning } from '../../../../types/tasks'; +import { Task, TaskWarning } from '../../../../types/tasks'; import { Component } from '../../../../types/types'; import { rawSizes } from '../../../theme'; import RecentHistory from '../../RecentHistory'; -import ComponentNavBgTaskNotif from './ComponentNavBgTaskNotif'; import ComponentNavProjectBindingErrorNotif from './ComponentNavProjectBindingErrorNotif'; import Header from './Header'; import HeaderMeta from './HeaderMeta'; @@ -91,20 +90,6 @@ export default function ComponentNav(props: ComponentNavProps) { let contextNavHeight = contextNavHeightRaw; - let bgTaskNotifComponent; - if (isInProgress || isPending || (currentTask && currentTask.status === TaskStatuses.Failed)) { - bgTaskNotifComponent = ( - <ComponentNavBgTaskNotif - component={component} - currentTask={currentTask} - currentTaskOnSameBranch={currentTaskOnSameBranch} - isInProgress={isInProgress} - isPending={isPending} - /> - ); - contextNavHeight += ALERT_HEIGHT; - } - let prDecoNotifComponent; if (projectBindingErrors !== undefined) { prDecoNotifComponent = <ComponentNavProjectBindingErrorNotif component={component} />; @@ -120,12 +105,7 @@ export default function ComponentNav(props: ComponentNavProps) { height={contextNavHeight} id="context-navigation" label={translate('qualifier', component.qualifier)} - notif={ - <> - {bgTaskNotifComponent} - {prDecoNotifComponent} - </> - } + notif={<>{prDecoNotifComponent}</>} > <div className={classNames('display-flex-center display-flex-space-between', { @@ -142,6 +122,10 @@ export default function ComponentNav(props: ComponentNavProps) { <HeaderMeta branchLike={currentBranchLike} component={component} + currentTask={currentTask} + currentTaskOnSameBranch={currentTaskOnSameBranch} + isInProgress={isInProgress} + isPending={isPending} onWarningDismiss={props.onWarningDismiss} warnings={warnings} /> @@ -152,7 +136,9 @@ export default function ComponentNav(props: ComponentNavProps) { component={component} isInProgress={isInProgress} isPending={isPending} - onToggleProjectInfo={() => setDisplayProjectInfo(!displayProjectInfo)} + onToggleProjectInfo={() => { + setDisplayProjectInfo(!displayProjectInfo); + }} projectInfoDisplayed={displayProjectInfo} /> <InfoDrawer diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx deleted file mode 100644 index 50070da16da..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { STATUSES } from '../../../../apps/background-tasks/constants'; -import Link from '../../../../components/common/Link'; -import { Location, withRouter } from '../../../../components/hoc/withRouter'; -import { Alert } from '../../../../components/ui/Alert'; -import { hasMessage, translate } from '../../../../helpers/l10n'; -import { getComponentBackgroundTaskUrl } from '../../../../helpers/urls'; -import { Task, TaskStatuses } from '../../../../types/tasks'; -import { Component } from '../../../../types/types'; -import ComponentNavLicenseNotif from './ComponentNavLicenseNotif'; - -interface Props { - component: Component; - currentTask?: Task; - currentTaskOnSameBranch?: boolean; - isInProgress?: boolean; - isPending?: boolean; - location: Location; -} - -export class ComponentNavBgTaskNotif extends React.PureComponent<Props> { - renderMessage(messageKey: string, status?: string, branch?: string) { - const { component, currentTask, location } = this.props; - const backgroundTaskUrl = getComponentBackgroundTaskUrl(component.key, status); - const canSeeBackgroundTasks = - component.configuration && component.configuration.showBackgroundTasks; - const isOnBackgroundTaskPage = location.pathname === backgroundTaskUrl.pathname; - - let type; - if (currentTask && hasMessage('background_task.type', currentTask.type)) { - messageKey += '_X'; - type = translate('background_task.type', currentTask.type); - } - - let url; - let stacktrace; - if (canSeeBackgroundTasks) { - messageKey += '.admin'; - - if (isOnBackgroundTaskPage) { - messageKey += '.help'; - stacktrace = translate('background_tasks.show_stacktrace'); - } else { - messageKey += '.link'; - url = <Link to={backgroundTaskUrl}>{translate('background_tasks.page')}</Link>; - } - } - - return ( - <FormattedMessage - defaultMessage={translate(messageKey)} - id={messageKey} - values={{ branch, url, stacktrace, type }} - /> - ); - } - - render() { - const { currentTask, currentTaskOnSameBranch, isInProgress, isPending } = this.props; - if (isInProgress) { - return ( - <Alert display="banner" variant="info"> - {this.renderMessage('component_navigation.status.in_progress')} - </Alert> - ); - } else if (isPending) { - return ( - <Alert display="banner" variant="info"> - {this.renderMessage('component_navigation.status.pending', STATUSES.ALL)} - </Alert> - ); - } else if (currentTask && currentTask.status === TaskStatuses.Failed) { - if ( - currentTask.errorType && - hasMessage('license.component_navigation.button', currentTask.errorType) - ) { - return <ComponentNavLicenseNotif currentTask={currentTask} />; - } - const branch = - currentTask.branch || - `${currentTask.pullRequest}${ - currentTask.pullRequestTitle ? ' - ' + currentTask.pullRequestTitle : '' - }`; - let message; - if (currentTaskOnSameBranch === false && branch) { - message = this.renderMessage( - 'component_navigation.status.failed_branch', - undefined, - branch - ); - } else { - message = this.renderMessage('component_navigation.status.failed'); - } - - return ( - <Alert className="null-spacer-bottom" display="banner" variant="error"> - {message} - </Alert> - ); - } - return null; - } -} - -export default withRouter(ComponentNavBgTaskNotif); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx deleted file mode 100644 index 54be3310d51..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { isValidLicense } from '../../../../api/editions'; -import Link from '../../../../components/common/Link'; -import { Alert } from '../../../../components/ui/Alert'; -import { translate, translateWithParameters } from '../../../../helpers/l10n'; -import { AppState } from '../../../../types/appstate'; -import { ComponentQualifier } from '../../../../types/component'; -import { Task } from '../../../../types/tasks'; -import withAppStateContext from '../../app-state/withAppStateContext'; - -interface Props { - appState: AppState; - currentTask?: Task; -} - -interface State { - isValidLicense?: boolean; - loading: boolean; -} - -export class ComponentNavLicenseNotif extends React.PureComponent<Props, State> { - mounted = false; - state: State = { loading: false }; - - componentDidMount() { - this.mounted = true; - this.fetchIsValidLicense(); - } - - componentWillUnmount() { - this.mounted = false; - } - - fetchIsValidLicense = () => { - this.setState({ loading: true }); - isValidLicense().then( - ({ isValidLicense }) => { - if (this.mounted) { - this.setState({ isValidLicense, loading: false }); - } - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); - }; - - render() { - const { currentTask, appState } = this.props; - const { isValidLicense, loading } = this.state; - - if (loading || !currentTask || !currentTask.errorType) { - return null; - } - - if (isValidLicense && currentTask.errorType !== 'LICENSING_LOC') { - return ( - <Alert display="banner" variant="error"> - {translateWithParameters( - 'component_navigation.status.last_blocked_due_to_bad_license_X', - translate('qualifier', currentTask.componentQualifier || ComponentQualifier.Project) - )} - </Alert> - ); - } - - return ( - <Alert display="banner" variant="error"> - <span className="little-spacer-right">{currentTask.errorMessage}</span> - {appState.canAdmin ? ( - <Link to="/admin/extension/license/app"> - {translate('license.component_navigation.button', currentTask.errorType)}. - </Link> - ) : ( - translate('please_contact_administrator') - )} - </Alert> - ); - } -} - -export default withAppStateContext(ComponentNavLicenseNotif); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx deleted file mode 100644 index 1ef62b391d3..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import AnalysisWarningsModal from '../../../../components/common/AnalysisWarningsModal'; -import { Alert } from '../../../../components/ui/Alert'; -import { translate } from '../../../../helpers/l10n'; -import { TaskWarning } from '../../../../types/tasks'; - -interface Props { - componentKey: string; - isBranch: boolean; - onWarningDismiss: () => void; - warnings: TaskWarning[]; -} - -interface State { - modal: boolean; -} - -export default class ComponentNavWarnings extends React.PureComponent<Props, State> { - state: State = { modal: false }; - - handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => { - event.preventDefault(); - event.currentTarget.blur(); - this.setState({ modal: true }); - }; - - handleCloseModal = () => { - this.setState({ modal: false }); - }; - - render() { - return ( - <> - <Alert className="js-component-analysis-warnings flex-1" display="inline" variant="warning"> - <FormattedMessage - defaultMessage={translate('component_navigation.last_analysis_had_warnings')} - id="component_navigation.last_analysis_had_warnings" - values={{ - branchType: this.props.isBranch - ? translate('branches.branch') - : translate('branches.pr'), - warnings: ( - <a href="#" onClick={this.handleClick}> - <FormattedMessage - defaultMessage={translate('component_navigation.x_warnings')} - id="component_navigation.x_warnings" - values={{ - warningsCount: this.props.warnings.length, - }} - /> - </a> - ), - }} - /> - </Alert> - {this.state.modal && ( - <AnalysisWarningsModal - componentKey={this.props.componentKey} - onClose={this.handleCloseModal} - onWarningDismiss={this.props.onWarningDismiss} - warnings={this.props.warnings} - /> - )} - </> - ); - } -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx index 4362e6c0191..1cff50e993b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx @@ -18,119 +18,68 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { useIntl } from 'react-intl'; -import BranchStatus from '../../../../components/common/BranchStatus'; -import Link from '../../../../components/common/Link'; import HomePageSelect from '../../../../components/controls/HomePageSelect'; -import { formatterOption } from '../../../../components/intl/DateTimeFormatter'; import { isBranch, isPullRequest } from '../../../../helpers/branch-like'; -import { translate, translateWithParameters } from '../../../../helpers/l10n'; +import { translateWithParameters } from '../../../../helpers/l10n'; import { BranchLike } from '../../../../types/branch-like'; -import { ComponentQualifier } from '../../../../types/component'; -import { TaskWarning } from '../../../../types/tasks'; +import { Task, TaskWarning } from '../../../../types/tasks'; import { Component } from '../../../../types/types'; -import { CurrentUser, HomePage, isLoggedIn } from '../../../../types/users'; +import { CurrentUser, isLoggedIn } from '../../../../types/users'; import withCurrentUserContext from '../../current-user/withCurrentUserContext'; -import ComponentNavWarnings from './ComponentNavWarnings'; -import './HeaderMeta.css'; +import { AnalysisStatus } from './AnalysisStatus'; +import CurrentBranchLikeMergeInformation from './branch-like/CurrentBranchLikeMergeInformation'; +import { getCurrentPage } from './utils'; export interface HeaderMetaProps { branchLike?: BranchLike; - currentUser: CurrentUser; component: Component; + currentUser: CurrentUser; + currentTask?: Task; + currentTaskOnSameBranch?: boolean; + isInProgress?: boolean; + isPending?: boolean; onWarningDismiss: () => void; warnings: TaskWarning[]; } export function HeaderMeta(props: HeaderMetaProps) { - const { branchLike, component, currentUser, warnings } = props; + const { + branchLike, + component, + currentUser, + currentTask, + currentTaskOnSameBranch, + isInProgress, + isPending, + warnings, + } = props; const isABranch = isBranch(branchLike); const currentPage = getCurrentPage(component, branchLike); - const displayVersion = component.version !== undefined && isABranch; - const lastAnalysisDate = useIntl().formatDate(component.analysisDate, formatterOption); return ( - <> - <div className="display-flex-center flex-0 small"> - {warnings.length > 0 && ( - <span className="header-meta-warnings"> - <ComponentNavWarnings - isBranch={isABranch} - componentKey={component.key} - onWarningDismiss={props.onWarningDismiss} - warnings={warnings} - /> - </span> - )} - {component.analysisDate && ( - <span - title={translateWithParameters( - 'overview.project.last_analysis.date_time', - lastAnalysisDate - )} - className="spacer-left nowrap note" - > - {lastAnalysisDate} - </span> - )} - {displayVersion && ( - <span className="spacer-left nowrap note">{`${translate('version')} ${ - component.version - }`}</span> - )} - {isLoggedIn(currentUser) && currentPage !== undefined && !isPullRequest(branchLike) && ( - <HomePageSelect className="spacer-left" currentPage={currentPage} /> - )} - </div> - {isPullRequest(branchLike) && ( - <div className="navbar-context-meta-secondary display-inline-flex-center"> - {branchLike.url !== undefined && ( - <Link - className="link-no-underline big-spacer-right" - to={branchLike.url} - target="_blank" - size={12} - > - {translate('branches.see_the_pr')} - </Link> - )} - <BranchStatus branchLike={branchLike} component={component} /> - </div> + <div className="sw-flex sw-items-center sw-flex-shrink sw-min-w-0"> + <AnalysisStatus + component={component} + currentTask={currentTask} + currentTaskOnSameBranch={currentTaskOnSameBranch} + isInProgress={isInProgress} + isPending={isPending} + onWarningDismiss={props.onWarningDismiss} + warnings={warnings} + /> + {branchLike && <CurrentBranchLikeMergeInformation currentBranchLike={branchLike} />} + {component.version !== undefined && isABranch && ( + <span className="sw-ml-4 sw-whitespace-nowrap"> + {translateWithParameters('version_x', component.version)} + </span> )} - </> + {isLoggedIn(currentUser) && currentPage !== undefined && !isPullRequest(branchLike) && ( + <HomePageSelect className="sw-ml-4" currentPage={currentPage} /> + )} + </div> ); } -export function getCurrentPage(component: Component, branchLike: BranchLike | undefined) { - let currentPage: HomePage | undefined; - - const branch = isBranch(branchLike) && !branchLike.isMain ? branchLike.name : undefined; - - switch (component.qualifier) { - case ComponentQualifier.Portfolio: - case ComponentQualifier.SubPortfolio: - currentPage = { type: 'PORTFOLIO', component: component.key }; - break; - case ComponentQualifier.Application: - currentPage = { - type: 'APPLICATION', - component: component.key, - branch, - }; - break; - case ComponentQualifier.Project: - // when home page is set to the default branch of a project, its name is returned as `undefined` - currentPage = { - type: 'PROJECT', - component: component.key, - branch, - }; - break; - } - - return currentPage; -} - export default withCurrentUserContext(HeaderMeta); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx new file mode 100644 index 00000000000..e3be72d3fef --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx @@ -0,0 +1,82 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { screen } from '@testing-library/react'; +import * as React from 'react'; +import { mockComponent } from '../../../../../helpers/mocks/component'; +import { mockTask } from '../../../../../helpers/mocks/tasks'; +import { renderApp } from '../../../../../helpers/testReactTestingUtils'; +import { AnalysisErrorMessage } from '../AnalysisErrorMessage'; + +it('should work when error is on a different branch', () => { + renderAnalysisErrorMessage({ + currentTask: mockTask({ branch: 'branch-1.2' }), + currentTaskOnSameBranch: false, + }); + + expect(screen.getByText(/component_navigation.status.failed_branch_X/)).toBeInTheDocument(); + expect(screen.getByText(/branch-1\.2/)).toBeInTheDocument(); +}); + +it('should work for errors on Pull Requests', () => { + renderAnalysisErrorMessage({ + currentTask: mockTask({ pullRequest: '2342', pullRequestTitle: 'Fix stuff' }), + currentTaskOnSameBranch: true, + }); + + expect(screen.getByText(/component_navigation.status.failed_X/)).toBeInTheDocument(); + expect(screen.getByText(/2342 - Fix stuff/)).toBeInTheDocument(); +}); + +it('should provide a link to admins', () => { + renderAnalysisErrorMessage({ + component: mockComponent({ configuration: { showBackgroundTasks: true } }), + }); + + expect(screen.getByText(/component_navigation.status.failed_X.admin.link/)).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'background_tasks.page' })).toBeInTheDocument(); +}); + +it('should explain to admins how to get the staktrace', () => { + renderAnalysisErrorMessage( + { + component: mockComponent({ configuration: { showBackgroundTasks: true } }), + }, + 'project/background_tasks' + ); + + expect(screen.getByText(/component_navigation.status.failed_X.admin.help/)).toBeInTheDocument(); + expect(screen.queryByRole('link', { name: 'background_tasks.page' })).not.toBeInTheDocument(); +}); + +function renderAnalysisErrorMessage( + overrides: Partial<Parameters<typeof AnalysisErrorMessage>[0]> = {}, + location = '/' +) { + return renderApp( + location, + <AnalysisErrorMessage + component={mockComponent()} + currentTask={mockTask()} + onLeave={jest.fn()} + currentTaskOnSameBranch={true} + {...overrides} + /> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx new file mode 100644 index 00000000000..5405df2ec92 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx @@ -0,0 +1,78 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { screen } from '@testing-library/react'; +import * as React from 'react'; +import { isValidLicense } from '../../../../../api/editions'; +import { mockTask } from '../../../../../helpers/mocks/tasks'; +import { mockAppState } from '../../../../../helpers/testMocks'; +import { renderApp } from '../../../../../helpers/testReactTestingUtils'; +import { AnalysisLicenseError } from '../AnalysisLicenseError'; + +jest.mock('../../../../../api/editions', () => ({ + isValidLicense: jest.fn().mockResolvedValue({ isValidLicense: true }), +})); + +it('should handle a valid license', async () => { + renderAnalysisLicenseError({ + currentTask: mockTask({ errorType: 'ANY_TYPE' }), + }); + + expect( + await screen.findByText( + 'component_navigation.status.last_blocked_due_to_bad_license_X.qualifier.TRK' + ) + ).toBeInTheDocument(); +}); + +it('should send user to contact the admin', async () => { + const errorMessage = 'error message'; + renderAnalysisLicenseError({ + currentTask: mockTask({ errorMessage, errorType: 'LICENSING_LOC' }), + }); + + expect(await screen.findByText('please_contact_administrator')).toBeInTheDocument(); + expect(screen.getByText(errorMessage)).toBeInTheDocument(); +}); + +it('should send provide a link to the admin', async () => { + jest.mocked(isValidLicense).mockResolvedValueOnce({ isValidLicense: false }); + + const errorMessage = 'error message'; + renderAnalysisLicenseError( + { + currentTask: mockTask({ errorMessage, errorType: 'error-type' }), + }, + true + ); + + expect( + await screen.findByText('license.component_navigation.button.error-type.') + ).toBeInTheDocument(); + expect(screen.getByText(errorMessage)).toBeInTheDocument(); +}); + +function renderAnalysisLicenseError( + overrides: Partial<Parameters<typeof AnalysisLicenseError>[0]> = {}, + canAdmin = false +) { + return renderApp('/', <AnalysisLicenseError currentTask={mockTask()} {...overrides} />, { + appState: mockAppState({ canAdmin }), + }); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx index e7235f579e6..81b2ea7fc3b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx @@ -30,28 +30,28 @@ import ComponentNav, { ComponentNavProps } from '../ComponentNav'; it('renders correctly when there are warnings', () => { renderComponentNav({ warnings: [mockTaskWarning()] }); expect( - screen.getByText('component_navigation.last_analysis_had_warnings', { exact: false }) + screen.getByText('project_navigation.analysis_status.warnings', { exact: false }) ).toBeInTheDocument(); }); it('renders correctly when there is a background task in progress', () => { renderComponentNav({ isInProgress: true }); expect( - screen.getByText('component_navigation.status.in_progress', { exact: false }) + screen.getByText('project_navigation.analysis_status.in_progress', { exact: false }) ).toBeInTheDocument(); }); it('renders correctly when there is a background task pending', () => { renderComponentNav({ isPending: true }); expect( - screen.getByText('component_navigation.status.pending', { exact: false }) + screen.getByText('project_navigation.analysis_status.pending', { exact: false }) ).toBeInTheDocument(); }); it('renders correctly when there is a failing background task', () => { renderComponentNav({ currentTask: mockTask({ status: TaskStatuses.Failed }) }); expect( - screen.getByText('component_navigation.status.failed_X', { exact: false }) + screen.getByText('project_navigation.analysis_status.failed', { exact: false }) ).toBeInTheDocument(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBgTaskNotif-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBgTaskNotif-test.tsx deleted file mode 100644 index b9386f3ea1f..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBgTaskNotif-test.tsx +++ /dev/null @@ -1,341 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Alert } from '../../../../../components/ui/Alert'; -import { hasMessage } from '../../../../../helpers/l10n'; -import { mockComponent } from '../../../../../helpers/mocks/component'; -import { mockTask } from '../../../../../helpers/mocks/tasks'; -import { mockLocation } from '../../../../../helpers/testMocks'; -import { Task, TaskStatuses, TaskTypes } from '../../../../../types/tasks'; -import { ComponentNavBgTaskNotif } from '../ComponentNavBgTaskNotif'; - -jest.mock('../../../../../helpers/l10n', () => ({ - ...jest.requireActual('../../../../../helpers/l10n'), - hasMessage: jest.fn().mockReturnValue(true), -})); - -const UNKNOWN_TASK_TYPE: TaskTypes = 'UNKOWN' as TaskTypes; - -it('renders correctly', () => { - expect(shallowRender()).toMatchSnapshot('default'); - expect( - shallowRender({ - currentTask: mockTask({ - status: TaskStatuses.Failed, - errorType: 'LICENSING', - errorMessage: 'Foo', - }), - }) - ).toMatchSnapshot('license issue'); - expect(shallowRender({ currentTask: undefined }).type()).toBeNull(); // No task. -}); - -it.each([ - // failed - [ - 'component_navigation.status.failed', - 'error', - mockTask({ status: TaskStatuses.Failed, type: UNKNOWN_TASK_TYPE }), - false, - false, - false, - false, - ], - [ - 'component_navigation.status.failed_X', - 'error', - mockTask({ status: TaskStatuses.Failed }), - false, - false, - false, - false, - ], - [ - 'component_navigation.status.failed.admin.link', - 'error', - mockTask({ status: TaskStatuses.Failed, type: UNKNOWN_TASK_TYPE }), - false, - false, - true, - false, - ], - [ - 'component_navigation.status.failed_X.admin.link', - 'error', - mockTask({ status: TaskStatuses.Failed }), - false, - false, - true, - false, - ], - [ - 'component_navigation.status.failed.admin.help', - 'error', - mockTask({ status: TaskStatuses.Failed, type: UNKNOWN_TASK_TYPE }), - false, - false, - true, - true, - ], - [ - 'component_navigation.status.failed_X.admin.help', - 'error', - mockTask({ status: TaskStatuses.Failed }), - false, - false, - true, - true, - ], - // failed_branch - [ - 'component_navigation.status.failed_branch', - 'error', - mockTask({ status: TaskStatuses.Failed, branch: 'foo', type: UNKNOWN_TASK_TYPE }), - false, - false, - false, - false, - ], - [ - 'component_navigation.status.failed_branch_X', - 'error', - mockTask({ status: TaskStatuses.Failed, branch: 'foo' }), - false, - false, - false, - false, - ], - [ - 'component_navigation.status.failed_branch.admin.link', - 'error', - mockTask({ status: TaskStatuses.Failed, branch: 'foo', type: UNKNOWN_TASK_TYPE }), - false, - false, - true, - false, - ], - [ - 'component_navigation.status.failed_branch_X.admin.link', - 'error', - mockTask({ status: TaskStatuses.Failed, branch: 'foo' }), - false, - false, - true, - false, - ], - [ - 'component_navigation.status.failed_branch.admin.help', - 'error', - mockTask({ status: TaskStatuses.Failed, branch: 'foo', type: UNKNOWN_TASK_TYPE }), - false, - false, - true, - true, - ], - [ - 'component_navigation.status.failed_branch_X.admin.help', - 'error', - mockTask({ status: TaskStatuses.Failed, branch: 'foo' }), - false, - false, - true, - true, - ], - // pending - [ - 'component_navigation.status.pending', - 'info', - mockTask({ type: UNKNOWN_TASK_TYPE }), - true, - false, - false, - false, - ], - ['component_navigation.status.pending_X', 'info', mockTask(), true, false, false, false], - [ - 'component_navigation.status.pending.admin.link', - 'info', - mockTask({ type: UNKNOWN_TASK_TYPE }), - true, - false, - true, - false, - ], - [ - 'component_navigation.status.pending_X.admin.link', - 'info', - mockTask(), - true, - false, - true, - false, - ], - [ - 'component_navigation.status.pending.admin.help', - 'info', - mockTask({ type: UNKNOWN_TASK_TYPE }), - true, - false, - true, - true, - ], - [ - 'component_navigation.status.pending_X.admin.help', - 'info', - mockTask({ status: TaskStatuses.Failed }), - true, - false, - true, - true, - ], - // in_progress - [ - 'component_navigation.status.in_progress', - 'info', - mockTask({ type: UNKNOWN_TASK_TYPE }), - true, - true, - false, - false, - ], - ['component_navigation.status.in_progress_X', 'info', mockTask(), true, true, false, false], - [ - 'component_navigation.status.in_progress.admin.link', - 'info', - mockTask({ type: UNKNOWN_TASK_TYPE }), - true, - true, - true, - false, - ], - [ - 'component_navigation.status.in_progress_X.admin.link', - 'info', - mockTask(), - true, - true, - true, - false, - ], - [ - 'component_navigation.status.in_progress.admin.help', - 'info', - mockTask({ type: UNKNOWN_TASK_TYPE }), - true, - true, - true, - true, - ], - [ - 'component_navigation.status.in_progress_X.admin.help', - 'info', - mockTask({ status: TaskStatuses.Failed }), - true, - true, - true, - true, - ], -])( - 'should render the expected message=%p', - ( - expectedMessage: string, - alertVariant: string, - currentTask: Task, - isPending: boolean, - isInProgress: boolean, - showBackgroundTasks: boolean, - onBackgroudTaskPage: boolean - ) => { - if (currentTask.type === UNKNOWN_TASK_TYPE) { - (hasMessage as jest.Mock).mockReturnValueOnce(false); - } - - const wrapper = shallowRender({ - component: mockComponent({ configuration: { showBackgroundTasks } }), - currentTask, - currentTaskOnSameBranch: !currentTask.branch, - isPending, - isInProgress, - location: mockLocation({ - pathname: onBackgroudTaskPage ? '/project/background_tasks' : '/foo/bar', - }), - }); - const messageProps = wrapper.find(FormattedMessage).props(); - - // Translation key. - expect(messageProps.defaultMessage).toBe(expectedMessage); - - // Alert variant. - expect(wrapper.find(Alert).props().variant).toBe(alertVariant); - - // Formatted message values prop. - // eslint-disable-next-line jest/no-conditional-in-test - if (/_X/.test(expectedMessage)) { - // eslint-disable-next-line jest/no-conditional-expect - expect(messageProps.values?.type).toBe(`background_task.type.${currentTask.type}`); - } else { - // eslint-disable-next-line jest/no-conditional-expect - expect(messageProps.values?.type).toBeUndefined(); - } - - // eslint-disable-next-line jest/no-conditional-in-test - if (currentTask.branch) { - // eslint-disable-next-line jest/no-conditional-expect - expect(messageProps.values?.branch).toBe(currentTask.branch); - } else { - // eslint-disable-next-line jest/no-conditional-expect - expect(messageProps.values?.branch).toBeUndefined(); - } - - // eslint-disable-next-line jest/no-conditional-in-test - if (showBackgroundTasks) { - // eslint-disable-next-line jest/no-conditional-in-test - if (onBackgroudTaskPage) { - // eslint-disable-next-line jest/no-conditional-expect - expect(messageProps.values?.url).toBeUndefined(); - // eslint-disable-next-line jest/no-conditional-expect - expect(messageProps.values?.stacktrace).toBe('background_tasks.show_stacktrace'); - } else { - // eslint-disable-next-line jest/no-conditional-expect - expect(messageProps.values?.url).toBeDefined(); - // eslint-disable-next-line jest/no-conditional-expect - expect(messageProps.values?.stacktrace).toBeUndefined(); - } - } else { - // eslint-disable-next-line jest/no-conditional-expect - expect(messageProps.values?.url).toBeUndefined(); - // eslint-disable-next-line jest/no-conditional-expect - expect(messageProps.values?.stacktrace).toBeUndefined(); - } - } -); - -function shallowRender(props: Partial<ComponentNavBgTaskNotif['props']> = {}) { - return shallow<ComponentNavBgTaskNotif>( - <ComponentNavBgTaskNotif - component={mockComponent()} - currentTask={mockTask({ status: TaskStatuses.Failed })} - location={mockLocation()} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx deleted file mode 100644 index 103ca94a5af..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { isValidLicense } from '../../../../../api/editions'; -import { mockTask } from '../../../../../helpers/mocks/tasks'; -import { mockAppState } from '../../../../../helpers/testMocks'; -import { waitAndUpdate } from '../../../../../helpers/testUtils'; -import { TaskStatuses } from '../../../../../types/tasks'; -import { ComponentNavLicenseNotif } from '../ComponentNavLicenseNotif'; - -jest.mock('../../../../../helpers/l10n', () => ({ - ...jest.requireActual('../../../../../helpers/l10n'), - hasMessage: jest.fn().mockReturnValue(true), -})); - -jest.mock('../../../../../api/editions', () => ({ - isValidLicense: jest.fn().mockResolvedValue({ isValidLicense: false }), -})); - -beforeEach(() => { - (isValidLicense as jest.Mock<any>).mockClear(); -}); - -it('renders background task license info correctly', async () => { - let wrapper = getWrapper({ - currentTask: mockTask({ - status: TaskStatuses.Failed, - errorType: 'LICENSING', - errorMessage: 'Foo', - }), - }); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); - - wrapper = getWrapper({ - appState: mockAppState({ canAdmin: false }), - currentTask: mockTask({ - status: TaskStatuses.Failed, - errorType: 'LICENSING', - errorMessage: 'Foo', - }), - }); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); -}); - -it('renders a different message if the license is valid', async () => { - (isValidLicense as jest.Mock<any>).mockResolvedValueOnce({ isValidLicense: true }); - const wrapper = getWrapper({ - currentTask: mockTask({ - status: TaskStatuses.Failed, - errorType: 'LICENSING', - errorMessage: 'Foo', - }), - }); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); -}); - -it('renders correctly for LICENSING_LOC error', async () => { - (isValidLicense as jest.Mock<any>).mockResolvedValueOnce({ isValidLicense: true }); - const wrapper = getWrapper({ - currentTask: mockTask({ - status: TaskStatuses.Failed, - errorType: 'LICENSING_LOC', - errorMessage: 'Foo', - }), - }); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); -}); - -function getWrapper(props: Partial<ComponentNavLicenseNotif['props']> = {}) { - return shallow( - <ComponentNavLicenseNotif - appState={mockAppState({ canAdmin: true })} - currentTask={mockTask({ errorMessage: 'Foo', errorType: 'LICENSING' })} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx index 1dcf15bb587..cb92dda3fa1 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx @@ -17,95 +17,90 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import HomePageSelect from '../../../../../components/controls/HomePageSelect'; import { mockBranch, mockPullRequest } from '../../../../../helpers/mocks/branch-like'; import { mockComponent } from '../../../../../helpers/mocks/component'; -import { mockTaskWarning } from '../../../../../helpers/mocks/tasks'; -import { mockCurrentUser } from '../../../../../helpers/testMocks'; -import { ComponentQualifier } from '../../../../../types/component'; -import { getCurrentPage, HeaderMeta, HeaderMetaProps } from '../HeaderMeta'; - -jest.mock('react-intl', () => ({ - useIntl: jest.fn().mockImplementation(() => ({ - formatDate: jest.fn().mockImplementation(() => '2017-01-02T00:00:00.000Z'), - })), -})); - -it('should render correctly for a branch', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); +import { mockTask, mockTaskWarning } from '../../../../../helpers/mocks/tasks'; +import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks'; +import { renderApp } from '../../../../../helpers/testReactTestingUtils'; +import { TaskStatuses } from '../../../../../types/tasks'; +import { CurrentUser } from '../../../../../types/users'; +import HeaderMeta, { HeaderMetaProps } from '../HeaderMeta'; + +it('should render correctly for a branch with warnings', async () => { + const user = userEvent.setup(); + + renderHeaderMeta(); + + expect(screen.getByText('version_x.0.0.1')).toBeInTheDocument(); + + expect(screen.getByText('project_navigation.analysis_status.warnings')).toBeInTheDocument(); + + await user.click(screen.getByText('project_navigation.analysis_status.details_link')); + + expect(screen.getByRole('heading', { name: 'warnings' })).toBeInTheDocument(); }); -it('should render correctly for a main project branch', () => { - const wrapper = shallowRender({ - branchLike: mockBranch({ isMain: true }), - }); - expect(wrapper).toMatchSnapshot(); +it('should handle a branch with missing version and no warnings', () => { + renderHeaderMeta({ component: mockComponent({ version: undefined }), warnings: [] }); + + expect(screen.queryByText('version_x.0.0.1')).not.toBeInTheDocument(); + expect(screen.queryByText('project_navigation.analysis_status.warnings')).not.toBeInTheDocument(); }); -it('should render correctly for a portfolio', () => { - const wrapper = shallowRender({ - component: mockComponent({ key: 'foo', qualifier: ComponentQualifier.Portfolio }), +it('should render correctly with a failed analysis', async () => { + const user = userEvent.setup(); + + renderHeaderMeta({ + currentTask: mockTask({ + status: TaskStatuses.Failed, + errorMessage: 'this is the error message', + }), }); - expect(wrapper).toMatchSnapshot(); + + expect(screen.getByText('project_navigation.analysis_status.failed')).toBeInTheDocument(); + + await user.click(screen.getByText('project_navigation.analysis_status.details_link')); + + expect(screen.getByRole('heading', { name: 'error' })).toBeInTheDocument(); }); it('should render correctly for a pull request', () => { - const wrapper = shallowRender({ + renderHeaderMeta({ branchLike: mockPullRequest({ url: 'https://example.com/pull/1234', }), }); - expect(wrapper).toMatchSnapshot(); -}); -it('should render correctly when the user is not logged in', () => { - const wrapper = shallowRender({ currentUser: { isLoggedIn: false, dismissedNotices: {} } }); - expect(wrapper.find(HomePageSelect).exists()).toBe(false); + expect(screen.queryByText('version_x.0.0.1')).not.toBeInTheDocument(); + expect(screen.getByText('branch_like_navigation.for_merge_into_x_from_y')).toBeInTheDocument(); }); -describe('#getCurrentPage', () => { - it('should return a portfolio page', () => { - expect( - getCurrentPage( - mockComponent({ key: 'foo', qualifier: ComponentQualifier.Portfolio }), - undefined - ) - ).toEqual({ - type: 'PORTFOLIO', - component: 'foo', - }); - }); - - it('should return an application page', () => { - expect( - getCurrentPage( - mockComponent({ key: 'foo', qualifier: ComponentQualifier.Application }), - mockBranch({ name: 'develop' }) - ) - ).toEqual({ type: 'APPLICATION', component: 'foo', branch: 'develop' }); - }); - - it('should return a project page', () => { - expect(getCurrentPage(mockComponent(), mockBranch({ name: 'feature/foo' }))).toEqual({ - type: 'PROJECT', - component: 'my-project', - branch: 'feature/foo', - }); - }); +it('should render correctly when the user is not logged in', () => { + renderHeaderMeta({}, mockCurrentUser({ dismissedNotices: {} })); + expect(screen.queryByText('homepage.current.is_default')).not.toBeInTheDocument(); + expect(screen.queryByText('homepage.current')).not.toBeInTheDocument(); + expect(screen.queryByText('homepage.check')).not.toBeInTheDocument(); }); -function shallowRender(props: Partial<HeaderMetaProps> = {}) { - return shallow( +function renderHeaderMeta( + props: Partial<HeaderMetaProps> = {}, + currentUser: CurrentUser = mockLoggedInUser() +) { + return renderApp( + '/', <HeaderMeta branchLike={mockBranch()} - component={mockComponent({ analysisDate: '2017-01-02T00:00:00.000Z', version: '0.0.1' })} - currentUser={mockCurrentUser({ isLoggedIn: true })} + component={mockComponent({ version: '0.0.1' })} onWarningDismiss={jest.fn()} - warnings={[mockTaskWarning({ message: 'ERROR_1' }), mockTaskWarning({ message: 'ERROR_2' })]} + warnings={[ + mockTaskWarning({ key: '1', message: 'ERROR_1' }), + mockTaskWarning({ key: '2', message: 'ERROR_2' }), + ]} {...props} - /> + />, + { currentUser } ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap deleted file mode 100644 index 1b40f5351be..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap +++ /dev/null @@ -1,41 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders correctly: default 1`] = ` -<Alert - className="null-spacer-bottom" - display="banner" - variant="error" -> - <FormattedMessage - defaultMessage="component_navigation.status.failed_X" - id="component_navigation.status.failed_X" - values={ - { - "branch": undefined, - "stacktrace": undefined, - "type": "background_task.type.REPORT", - "url": undefined, - } - } - /> -</Alert> -`; - -exports[`renders correctly: license issue 1`] = ` -<withAppStateContext(ComponentNavLicenseNotif) - currentTask={ - { - "analysisId": "x123", - "componentKey": "foo", - "componentName": "Foo", - "componentQualifier": "TRK", - "errorMessage": "Foo", - "errorType": "LICENSING", - "id": "AXR8jg_0mF2ZsYr8Wzs2", - "status": "FAILED", - "submittedAt": "2020-09-11T11:45:35+0200", - "type": "REPORT", - } - } -/> -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavLicenseNotif-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavLicenseNotif-test.tsx.snap deleted file mode 100644 index 415d696f63e..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavLicenseNotif-test.tsx.snap +++ /dev/null @@ -1,62 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders a different message if the license is valid 1`] = ` -<Alert - display="banner" - variant="error" -> - component_navigation.status.last_blocked_due_to_bad_license_X.qualifier.TRK -</Alert> -`; - -exports[`renders background task license info correctly 1`] = ` -<Alert - display="banner" - variant="error" -> - <span - className="little-spacer-right" - > - Foo - </span> - <ForwardRef(Link) - to="/admin/extension/license/app" - > - license.component_navigation.button.LICENSING - . - </ForwardRef(Link)> -</Alert> -`; - -exports[`renders background task license info correctly 2`] = ` -<Alert - display="banner" - variant="error" -> - <span - className="little-spacer-right" - > - Foo - </span> - please_contact_administrator -</Alert> -`; - -exports[`renders correctly for LICENSING_LOC error 1`] = ` -<Alert - display="banner" - variant="error" -> - <span - className="little-spacer-right" - > - Foo - </span> - <ForwardRef(Link) - to="/admin/extension/license/app" - > - license.component_navigation.button.LICENSING_LOC - . - </ForwardRef(Link)> -</Alert> -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap deleted file mode 100644 index a09a740facb..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render 1`] = ` -<Fragment> - <Alert - className="js-component-analysis-warnings flex-1" - display="inline" - variant="warning" - > - <FormattedMessage - defaultMessage="component_navigation.last_analysis_had_warnings" - id="component_navigation.last_analysis_had_warnings" - values={ - { - "branchType": "branches.branch", - "warnings": <a - href="#" - onClick={[Function]} - > - <FormattedMessage - defaultMessage="component_navigation.x_warnings" - id="component_navigation.x_warnings" - values={ - { - "warningsCount": 1, - } - } - /> - </a>, - } - } - /> - </Alert> - <withCurrentUserContext(AnalysisWarningsModal) - componentKey="foo" - onClose={[Function]} - onWarningDismiss={[MockFunction]} - warnings={ - [ - { - "dismissable": false, - "key": "foo", - "message": "warning 1", - }, - ] - } - /> -</Fragment> -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap deleted file mode 100644 index 2977276f8b6..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap +++ /dev/null @@ -1,235 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly for a branch 1`] = ` -<Fragment> - <div - className="display-flex-center flex-0 small" - > - <span - className="header-meta-warnings" - > - <ComponentNavWarnings - componentKey="my-project" - isBranch={true} - onWarningDismiss={[MockFunction]} - warnings={ - [ - { - "dismissable": false, - "key": "foo", - "message": "ERROR_1", - }, - { - "dismissable": false, - "key": "foo", - "message": "ERROR_2", - }, - ] - } - /> - </span> - <span - className="spacer-left nowrap note" - title="overview.project.last_analysis.date_time.2017-01-02T00:00:00.000Z" - > - 2017-01-02T00:00:00.000Z - </span> - <span - className="spacer-left nowrap note" - > - version 0.0.1 - </span> - <withCurrentUserContext(HomePageSelect) - className="spacer-left" - currentPage={ - { - "branch": "branch-6.7", - "component": "my-project", - "type": "PROJECT", - } - } - /> - </div> -</Fragment> -`; - -exports[`should render correctly for a main project branch 1`] = ` -<Fragment> - <div - className="display-flex-center flex-0 small" - > - <span - className="header-meta-warnings" - > - <ComponentNavWarnings - componentKey="my-project" - isBranch={true} - onWarningDismiss={[MockFunction]} - warnings={ - [ - { - "dismissable": false, - "key": "foo", - "message": "ERROR_1", - }, - { - "dismissable": false, - "key": "foo", - "message": "ERROR_2", - }, - ] - } - /> - </span> - <span - className="spacer-left nowrap note" - title="overview.project.last_analysis.date_time.2017-01-02T00:00:00.000Z" - > - 2017-01-02T00:00:00.000Z - </span> - <span - className="spacer-left nowrap note" - > - version 0.0.1 - </span> - <withCurrentUserContext(HomePageSelect) - className="spacer-left" - currentPage={ - { - "branch": undefined, - "component": "my-project", - "type": "PROJECT", - } - } - /> - </div> -</Fragment> -`; - -exports[`should render correctly for a portfolio 1`] = ` -<Fragment> - <div - className="display-flex-center flex-0 small" - > - <span - className="header-meta-warnings" - > - <ComponentNavWarnings - componentKey="foo" - isBranch={true} - onWarningDismiss={[MockFunction]} - warnings={ - [ - { - "dismissable": false, - "key": "foo", - "message": "ERROR_1", - }, - { - "dismissable": false, - "key": "foo", - "message": "ERROR_2", - }, - ] - } - /> - </span> - <withCurrentUserContext(HomePageSelect) - className="spacer-left" - currentPage={ - { - "component": "foo", - "type": "PORTFOLIO", - } - } - /> - </div> -</Fragment> -`; - -exports[`should render correctly for a pull request 1`] = ` -<Fragment> - <div - className="display-flex-center flex-0 small" - > - <span - className="header-meta-warnings" - > - <ComponentNavWarnings - componentKey="my-project" - isBranch={false} - onWarningDismiss={[MockFunction]} - warnings={ - [ - { - "dismissable": false, - "key": "foo", - "message": "ERROR_1", - }, - { - "dismissable": false, - "key": "foo", - "message": "ERROR_2", - }, - ] - } - /> - </span> - <span - className="spacer-left nowrap note" - title="overview.project.last_analysis.date_time.2017-01-02T00:00:00.000Z" - > - 2017-01-02T00:00:00.000Z - </span> - </div> - <div - className="navbar-context-meta-secondary display-inline-flex-center" - > - <ForwardRef(Link) - className="link-no-underline big-spacer-right" - size={12} - target="_blank" - to="https://example.com/pull/1234" - > - branches.see_the_pr - </ForwardRef(Link)> - <withBranchStatus(BranchStatus) - branchLike={ - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "1001", - "target": "master", - "title": "Foo Bar feature", - "url": "https://example.com/pull/1234", - } - } - component={ - { - "analysisDate": "2017-01-02T00:00:00.000Z", - "breadcrumbs": [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": [ - { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": [], - "version": "0.0.1", - } - } - /> - </div> -</Fragment> -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/utils-test.ts b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/utils-test.ts new file mode 100644 index 00000000000..c2c7896c919 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/utils-test.ts @@ -0,0 +1,67 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { mockBranch } from '../../../../../helpers/mocks/branch-like'; +import { mockComponent } from '../../../../../helpers/mocks/component'; +import { ComponentQualifier } from '../../../../../types/component'; +import { getCurrentPage } from '../utils'; + +describe('getCurrentPage', () => { + it('should return a portfolio page', () => { + expect( + getCurrentPage( + mockComponent({ key: 'foo', qualifier: ComponentQualifier.Portfolio }), + undefined + ) + ).toEqual({ + type: 'PORTFOLIO', + component: 'foo', + }); + }); + + it('should return a portfolio page for a subportfolio too', () => { + expect( + getCurrentPage( + mockComponent({ key: 'foo', qualifier: ComponentQualifier.SubPortfolio }), + undefined + ) + ).toEqual({ + type: 'PORTFOLIO', + component: 'foo', + }); + }); + + it('should return an application page', () => { + expect( + getCurrentPage( + mockComponent({ key: 'foo', qualifier: ComponentQualifier.Application }), + mockBranch({ name: 'develop' }) + ) + ).toEqual({ type: 'APPLICATION', component: 'foo', branch: 'develop' }); + }); + + it('should return a project page', () => { + expect(getCurrentPage(mockComponent(), mockBranch({ name: 'feature/foo' }))).toEqual({ + type: 'PROJECT', + component: 'my-project', + branch: 'feature/foo', + }); + }); +}); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx index e78d5686e11..393483e0fe1 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { isPullRequest } from '../../../../../helpers/branch-like'; -import { translate } from '../../../../../helpers/l10n'; +import { translate, translateWithParameters } from '../../../../../helpers/l10n'; import { BranchLike } from '../../../../../types/branch-like'; export interface CurrentBranchLikeMergeInformationProps { @@ -35,7 +35,14 @@ export function CurrentBranchLikeMergeInformation(props: CurrentBranchLikeMergeI } return ( - <span className="big-spacer-left flex-shrink note text-ellipsis"> + <span + className="sw-overflow-ellipsis sw-whitespace-nowrap sw-overflow-hidden sw-mx-1 sw-flex-shrink sw-min-w-0" + title={translateWithParameters( + 'branch_like_navigation.for_merge_into_x_from_y.title', + currentBranchLike.target, + currentBranchLike.branch + )} + > <FormattedMessage defaultMessage={translate('branch_like_navigation.for_merge_into_x_from_y')} id="branch_like_navigation.for_merge_into_x_from_y" diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLikeMergeInformation-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLikeMergeInformation-test.tsx.snap deleted file mode 100644 index 5e4d7cdfaae..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLikeMergeInformation-test.tsx.snap +++ /dev/null @@ -1,22 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<span - className="big-spacer-left flex-shrink note text-ellipsis" -> - <FormattedMessage - defaultMessage="branch_like_navigation.for_merge_into_x_from_y" - id="branch_like_navigation.for_merge_into_x_from_y" - values={ - { - "branch": <strong> - feature/foo/bar - </strong>, - "target": <strong> - master - </strong>, - } - } - /> -</span> -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts b/server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts new file mode 100644 index 00000000000..7353210fdd8 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import React, { useEffect } from 'react'; +import { isValidLicense } from '../../../../api/editions'; + +export function useLicenseIsValid(): [boolean, boolean] { + const [licenseIsValid, setLicenseIsValid] = React.useState(false); + const [loading, setLoading] = React.useState(true); + + useEffect(() => { + setLoading(true); + + isValidLicense() + .then(({ isValidLicense }) => { + setLicenseIsValid(isValidLicense); + setLoading(false); + }) + .catch(() => { + setLoading(false); + }); + }, []); + + return [licenseIsValid, loading]; +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/utils.ts b/server/sonar-web/src/main/js/app/components/nav/component/utils.ts new file mode 100644 index 00000000000..f8e36b76a23 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/utils.ts @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { isBranch } from '../../../../helpers/branch-like'; +import { BranchLike } from '../../../../types/branch-like'; +import { ComponentQualifier } from '../../../../types/component'; +import { Component } from '../../../../types/types'; +import { HomePage } from '../../../../types/users'; + +export function getCurrentPage(component: Component, branchLike: BranchLike | undefined) { + let currentPage: HomePage | undefined; + + const branch = isBranch(branchLike) && !branchLike.isMain ? branchLike.name : undefined; + + switch (component.qualifier) { + case ComponentQualifier.Portfolio: + case ComponentQualifier.SubPortfolio: + currentPage = { type: 'PORTFOLIO', component: component.key }; + break; + case ComponentQualifier.Application: + currentPage = { + type: 'APPLICATION', + component: component.key, + branch, + }; + break; + case ComponentQualifier.Project: + // when home page is set to the default branch of a project, its name is returned as `undefined` + currentPage = { + type: 'PROJECT', + component: component.key, + branch, + }; + break; + } + + return currentPage; +} diff --git a/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx b/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx index fec78c2f835..cb65d92527a 100644 --- a/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx +++ b/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx @@ -18,13 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; +import { BareButton, HomeFillIcon, HomeIcon, Tooltip } from 'design-system'; import * as React from 'react'; import { setHomePage } from '../../api/users'; import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext'; import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; -import { ButtonLink } from '../../components/controls/buttons'; -import Tooltip from '../../components/controls/Tooltip'; -import HomeIcon from '../../components/icons/HomeIcon'; import { translate } from '../../helpers/l10n'; import { isSameHomePage } from '../../helpers/users'; import { HomePage, isLoggedIn } from '../../types/users'; @@ -38,19 +36,13 @@ interface Props export const DEFAULT_HOMEPAGE: HomePage = { type: 'PROJECTS' }; export class HomePageSelect extends React.PureComponent<Props> { - buttonNode?: HTMLElement | null; - async setCurrentUserHomepage(homepage: HomePage) { const { currentUser } = this.props; - if (currentUser && isLoggedIn(currentUser)) { + if (isLoggedIn(currentUser)) { await setHomePage(homepage); this.props.updateCurrentUserHomepage(homepage); - - if (this.buttonNode) { - this.buttonNode.focus(); - } } } @@ -84,17 +76,16 @@ export class HomePageSelect extends React.PureComponent<Props> { className={classNames('display-inline-block', className)} role="img" > - <HomeIcon filled={isChecked} /> + <HomeFillIcon /> </span> ) : ( - <ButtonLink + <BareButton aria-label={tooltip} - className={classNames('link-no-underline', 'set-homepage-link', className)} + className={className} onClick={isChecked ? this.handleReset : this.handleClick} - innerRef={(node) => (this.buttonNode = node)} > - <HomeIcon filled={isChecked} /> - </ButtonLink> + {isChecked ? <HomeFillIcon /> : <HomeIcon />} + </BareButton> )} </Tooltip> ); diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/HomePageSelect-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/HomePageSelect-test.tsx index 3cefed0190a..77128773d20 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/HomePageSelect-test.tsx +++ b/server/sonar-web/src/main/js/components/controls/__tests__/HomePageSelect-test.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { setHomePage } from '../../../api/users'; import { mockLoggedInUser } from '../../../helpers/testMocks'; @@ -29,12 +30,13 @@ jest.mock('../../../api/users', () => ({ })); it('renders and behaves correctly', async () => { + const user = userEvent.setup(); const updateCurrentUserHomepage = jest.fn(); renderHomePageSelect({ updateCurrentUserHomepage }); const button = screen.getByRole('button'); expect(button).toBeInTheDocument(); - button.click(); + await user.click(button); await new Promise(setImmediate); expect(setHomePage).toHaveBeenCalledWith({ type: 'MY_PROJECTS' }); expect(updateCurrentUserHomepage).toHaveBeenCalled(); @@ -42,11 +44,13 @@ it('renders and behaves correctly', async () => { }); it('renders correctly if user is on the homepage', async () => { + const user = userEvent.setup(); + renderHomePageSelect({ currentUser: mockLoggedInUser({ homepage: { type: 'MY_PROJECTS' } }) }); const button = screen.getByRole('button'); expect(button).toBeInTheDocument(); - button.click(); + await user.click(button); await new Promise(setImmediate); expect(setHomePage).toHaveBeenCalledWith(DEFAULT_HOMEPAGE); expect(button).toHaveFocus(); |