diff options
author | Revanshu Paliwal <revanshu.paliwal@sonarsource.com> | 2023-03-21 17:13:04 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-03-27 20:03:03 +0000 |
commit | b211e45ffe83cbf8eaa98c3539b2ee3a952162e2 (patch) | |
tree | 700099740e34c64a82bc4ed02041d7f2c5e51406 /server | |
parent | 54686faf91547cd918e84c8e081a722ce974c89d (diff) | |
download | sonarqube-b211e45ffe83cbf8eaa98c3539b2ee3a952162e2.tar.gz sonarqube-b211e45ffe83cbf8eaa98c3539b2ee3a952162e2.zip |
SONAR-18776 Migrating breadcrumb and branch selector to MIUI
Diffstat (limited to 'server')
44 files changed, 1180 insertions, 2488 deletions
diff --git a/server/sonar-web/design-system/src/components/Badge.tsx b/server/sonar-web/design-system/src/components/Badge.tsx new file mode 100644 index 00000000000..a343c76931e --- /dev/null +++ b/server/sonar-web/design-system/src/components/Badge.tsx @@ -0,0 +1,98 @@ +/* + * 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 tw from 'twin.macro'; +import { themeColor, themeContrast } from '../helpers/theme'; +import { ThemeColors } from '../types/theme'; + +type BadgeVariant = 'default' | 'new' | 'deleted' | 'counter'; + +const variantList: Record<BadgeVariant, ThemeColors> = { + default: 'badgeDefault', + new: 'badgeNew', + deleted: 'badgeDeleted', + counter: 'badgeCounter', +}; + +interface BadgeProps { + children: string | number; + className?: string; + title?: string; + variant?: BadgeVariant; +} + +export default function Badge({ className, children, title, variant = 'default' }: BadgeProps) { + const commonProps = { + 'aria-label': title ?? children.toString(), + className, + role: 'status', + title, + }; + if (variant === 'counter') { + return <StyledCounter {...commonProps}>{children}</StyledCounter>; + } + return ( + <StyledBadge variantInfo={variantList[variant]} {...commonProps}> + {children} + </StyledBadge> + ); +} + +const StyledBadge = styled.span<{ + variantInfo: ThemeColors; +}>` + ${tw`sw-text-[0.75rem]`}; + ${tw`sw-leading-[0.938rem]`}; + ${tw`sw-font-semibold`}; + ${tw`sw-inline-block`}; + ${tw`sw-whitespace-nowrap`}; + ${tw`sw-px-[0.125rem] sw-py-[0.03125rem]`}; + ${tw`sw-rounded-1/2`}; + + color: ${({ variantInfo }) => themeContrast(variantInfo)}; + background-color: ${({ variantInfo }) => themeColor(variantInfo)}; + text-transform: uppercase; + + &:empty { + ${tw`sw-hidden`} + } + + .page-actions & { + ${tw`sw-my-1`}; + ${tw`sw-mx-0`}; + } +`; + +const StyledCounter = styled.span` + ${tw`sw-text-[0.75rem]`}; + ${tw`sw-font-regular`}; + ${tw`sw-px-2`}; + ${tw`sw-inline-flex`}; + ${tw`sw-leading-[1.125rem]`}; + ${tw`sw-items-center sw-justify-center`}; + ${tw`sw-rounded-pill`}; + + color: ${themeContrast('badgeCounter')}; + background-color: ${themeColor('badgeCounter')}; + + &:empty { + ${tw`sw-hidden`} + } +`; diff --git a/server/sonar-web/design-system/src/components/InputSearch.tsx b/server/sonar-web/design-system/src/components/InputSearch.tsx index 304a1d3dd2c..1d86d98131d 100644 --- a/server/sonar-web/design-system/src/components/InputSearch.tsx +++ b/server/sonar-web/design-system/src/components/InputSearch.tsx @@ -49,7 +49,7 @@ interface Props { placeholder: string; searchInputAriaLabel: string; size?: InputSizeKeys; - tooShortText: string; + tooShortText?: string; value?: string; } @@ -129,7 +129,7 @@ export default function InputSearch({ id={id} onMouseDown={onMouseDown} style={{ '--inputSize': INPUT_SIZES[size] }} - title={tooShort && isDefined(minLength) ? tooShortText : ''} + title={tooShort && tooShortText && isDefined(minLength) ? tooShortText : ''} > <StyledInputWrapper className="sw-flex sw-items-center"> <input @@ -161,7 +161,7 @@ export default function InputSearch({ /> )} - {tooShort && isDefined(minLength) && ( + {tooShort && tooShortText && isDefined(minLength) && ( <StyledNote className="sw-ml-1" role="note"> {tooShortText} </StyledNote> diff --git a/server/sonar-web/design-system/src/components/QualityGateIndicator.tsx b/server/sonar-web/design-system/src/components/QualityGateIndicator.tsx new file mode 100644 index 00000000000..5c8ac70cc71 --- /dev/null +++ b/server/sonar-web/design-system/src/components/QualityGateIndicator.tsx @@ -0,0 +1,182 @@ +/* + * 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 React from 'react'; +import { theme as twTheme } from 'twin.macro'; +import { BasePlacement, PopupPlacement } from '../helpers/positioning'; +import { themeColor, themeContrast } from '../helpers/theme'; + +const SIZE = { + sm: twTheme('spacing.4'), + md: twTheme('spacing.6'), + xl: twTheme('spacing.16'), +}; + +type QGStatus = 'ERROR' | 'OK' | 'NONE' | 'NOT_COMPUTED'; + +interface Props { + ariaLabel?: string; + className?: string; + size?: keyof typeof SIZE; + status: QGStatus; + tooltipPlacement?: BasePlacement; + withTooltip?: boolean; +} + +const RX_4 = 4; +const RX_2 = 2; + +export default function QualityGateIndicator(props: Props) { + const { + className, + size = 'md', + status, + tooltipPlacement = PopupPlacement.Right, + withTooltip, + ariaLabel, + } = props; + const iconProps = { + className, + height: SIZE[size], + rx: size === 'xl' ? RX_4 : RX_2, + size, + tooltipPlacement, + width: SIZE[size], + withTooltip, + }; + let StatusComponent: React.ReactNode; + switch (status) { + case 'NONE': + case 'NOT_COMPUTED': + StatusComponent = <QGNotComputed {...iconProps} />; + break; + case 'OK': + StatusComponent = <QGPassed {...iconProps} />; + break; + case 'ERROR': + StatusComponent = <QGFailed {...iconProps} />; + break; + } + return <div aria-label={ariaLabel}>{StatusComponent}</div>; +} + +const COMMON_PROPS = { + fill: 'none', + role: 'status', + xmlns: 'http://www.w3.org/2000/svg', +}; + +interface IconProps { + className?: string; + height: string; + rx: number; + size: keyof typeof SIZE; + tooltipPlacement?: BasePlacement; + width: string; + withTooltip?: boolean; +} + +function QGNotComputed({ + className, + rx, + size, + tooltipPlacement, + withTooltip, + ...sizeProps +}: IconProps) { + const theme = useTheme(); + const contrastColor = themeContrast('qgIndicatorNotComputed')({ theme }); + return ( + <svg className={className} {...COMMON_PROPS} {...sizeProps}> + <rect fill={themeColor('qgIndicatorNotComputed')({ theme })} rx={rx} {...sizeProps} /> + { + { + xl: <path d="M42 31v3H22v-3z" fill={contrastColor} />, + md: <path d="M18 12v1.5H6V12z" fill={contrastColor} />, + sm: <path d="M12 8v1H4V8z" fill={contrastColor} />, + }[size] + } + </svg> + ); +} + +function QGPassed({ className, rx, size, tooltipPlacement, withTooltip, ...sizeProps }: IconProps) { + const theme = useTheme(); + const contrastColor = themeContrast('qgIndicatorPassed')({ theme }); + return ( + <svg className={className} {...COMMON_PROPS} {...sizeProps}> + <rect fill={themeColor('qgIndicatorPassed')({ theme })} rx={rx} {...sizeProps} /> + { + { + xl: ( + <> + <path d="M38.974 25 41 27.026 28.847 39.178l-2.025-2.025z" fill={contrastColor} /> + <path d="M30.974 37.153 28.95 39.18 22 32.229l2.026-2.025z" fill={contrastColor} /> + </> + ), + md: ( + <> + <path d="m16.95 7.5 1.308 1.307-7.84 7.84-1.308-1.306z" fill={contrastColor} /> + <path d="m11.79 15.34-1.307 1.307-4.484-4.483 1.307-1.306z" fill={contrastColor} /> + </> + ), + sm: ( + <> + <path d="m11.3 5 .871.87-5.227 5.228-.87-.871z" fill={contrastColor} /> + <path d="m7.86 10.227-.872.871L4 8.11l.871-.871z" fill={contrastColor} /> + </> + ), + }[size] + } + </svg> + ); +} + +function QGFailed({ className, rx, size, tooltipPlacement, withTooltip, ...sizeProps }: IconProps) { + const theme = useTheme(); + const contrastColor = themeContrast('qgIndicatorFailed')({ theme }); + return ( + <svg className={className} {...COMMON_PROPS} {...sizeProps}> + <rect fill={themeColor('qgIndicatorFailed')({ theme })} rx={rx} {...sizeProps} /> + { + { + xl: ( + <> + <path d="m37.153 25 2.026 2.026-12.153 12.152L25 37.153z" fill={contrastColor} /> + <path d="m39.178 37.153-2.025 2.026L25 27.026 27.026 25z" fill={contrastColor} /> + </> + ), + md: ( + <> + <path d="m15.34 7.5 1.307 1.307-7.84 7.84L7.5 15.34z" fill={contrastColor} /> + <path d="m16.647 15.34-1.307 1.307-7.84-7.84L8.806 7.5z" fill={contrastColor} /> + </> + ), + sm: ( + <> + <path d="m10.227 5 .871.871-5.227 5.227L5 10.227z" fill={contrastColor} /> + <path d="m11.098 10.227-.871.87L5 5.872 5.87 5z" fill={contrastColor} /> + </> + ), + }[size] + } + </svg> + ); +} diff --git a/server/sonar-web/design-system/src/components/Text.tsx b/server/sonar-web/design-system/src/components/Text.tsx index 277f5eca4b5..e83f2198f4d 100644 --- a/server/sonar-web/design-system/src/components/Text.tsx +++ b/server/sonar-web/design-system/src/components/Text.tsx @@ -21,25 +21,33 @@ import styled from '@emotion/styled'; import tw from 'twin.macro'; import { themeColor, themeContrast } from '../helpers/theme'; -interface MainTextProps { +interface TextBoldProps { + className?: string; match?: string; name: string; } -export function SearchText({ match, name }: MainTextProps) { +export function TextBold({ match, name, className }: TextBoldProps) { return match ? ( <StyledText + className={className} // Safe: comes from the search engine, that injects bold tags into component names // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={{ __html: match }} /> ) : ( - <StyledText title={name}>{name}</StyledText> + <StyledText className={className} title={name}> + {name} + </StyledText> ); } -export function TextMuted({ text }: { text: string }) { - return <StyledMutedText title={text}>{text}</StyledMutedText>; +export function TextMuted({ text, className }: { className?: string; text: string }) { + return ( + <StyledMutedText className={className} title={text}> + {text} + </StyledMutedText> + ); } export const StyledText = styled.span` diff --git a/server/sonar-web/design-system/src/components/__tests__/Badge-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Badge-test.tsx new file mode 100644 index 00000000000..384dcfde844 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/Badge-test.tsx @@ -0,0 +1,32 @@ +/* + * 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 Badge from '../Badge'; + +it('renders badge correctly', () => { + render(<Badge>foo</Badge>); + expect(screen.getByRole('status')).toBeInTheDocument(); +}); + +it('renders counter correctly', () => { + render(<Badge variant="counter">23</Badge>); + expect(screen.getByRole('status')).toHaveAttribute('aria-label', '23'); +}); diff --git a/server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx b/server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx index 96d24f7f7bc..6dde045207d 100644 --- a/server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx +++ b/server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx @@ -82,7 +82,7 @@ function setupWithProps(props: Partial<FCProps<typeof InputSearch>> = {}) { onChange={jest.fn()} placeholder="placeholder" searchInputAriaLabel="" - tooShortText="" + tooShortText="too short" value="foo" {...props} /> diff --git a/server/sonar-web/design-system/src/components/__tests__/QualityGateIndicator-test.tsx b/server/sonar-web/design-system/src/components/__tests__/QualityGateIndicator-test.tsx new file mode 100644 index 00000000000..0ab9b9a1225 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/QualityGateIndicator-test.tsx @@ -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 { screen } from '@testing-library/react'; +import { render } from '../../helpers/testUtils'; +import { FCProps } from '../../types/misc'; +import QualityGateIndicator from '../QualityGateIndicator'; + +const SIZE_VS_WIDTH = { + sm: '1rem', + md: '1.5rem', + xl: '4rem', +}; + +it.each([ + ['OK', 'sm'], + ['OK', 'md'], + ['OK', 'xl'], + ['ERROR', 'sm'], + ['ERROR', 'md'], + ['ERROR', 'xl'], + ['NONE', 'sm'], + ['NONE', 'md'], + ['NONE', 'xl'], +])( + 'render the %s status and %s size correctly', + (status: 'ERROR' | 'OK' | 'NONE' | 'NOT_COMPUTED', size: 'sm' | 'md' | 'xl') => { + setupWithProps({ status, size }); + + expect(screen.getByRole('status')).toHaveAttribute('width', SIZE_VS_WIDTH[size]); + } +); + +it('should display tooltip', () => { + const { rerender } = setupWithProps({ + status: 'NONE', + withTooltip: true, + ariaLabel: 'label-none', + }); + expect(screen.getByLabelText('label-none')).toBeInTheDocument(); + + rerender(<QualityGateIndicator ariaLabel="label-ok" status="OK" withTooltip={true} />); + expect(screen.getByLabelText('label-ok')).toBeInTheDocument(); + + rerender(<QualityGateIndicator ariaLabel="label-error" status="ERROR" withTooltip={true} />); + expect(screen.getByLabelText('label-error')).toBeInTheDocument(); +}); + +function setupWithProps(props: Partial<FCProps<typeof QualityGateIndicator>> = {}) { + return render(<QualityGateIndicator status="OK" {...props} />); +} diff --git a/server/sonar-web/design-system/src/components/__tests__/Text-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Text-test.tsx index 5743a92a7b0..7c366168bf2 100644 --- a/server/sonar-web/design-system/src/components/__tests__/Text-test.tsx +++ b/server/sonar-web/design-system/src/components/__tests__/Text-test.tsx @@ -22,10 +22,10 @@ import { screen } from '@testing-library/react'; import { render } from '../../helpers/testUtils'; -import { SearchText, TextMuted } from '../Text'; +import { TextBold, TextMuted } from '../Text'; it('should render SearchText', () => { - render(<SearchText match="hi" name="hiya" />); + render(<TextBold match="hi" name="hiya" />); expect(screen.getByText('hi')).toHaveStyle({ 'font-weight': '600', diff --git a/server/sonar-web/design-system/src/components/icons/BranchIcon.tsx b/server/sonar-web/design-system/src/components/icons/BranchIcon.tsx new file mode 100644 index 00000000000..532434ea26c --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/BranchIcon.tsx @@ -0,0 +1,23 @@ +/* + * 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 { GitBranchIcon } from '@primer/octicons-react'; +import { OcticonHoc } from './Icon'; + +export default OcticonHoc(GitBranchIcon, 'BranchIcon'); diff --git a/server/sonar-web/design-system/src/components/icons/HelperHintIcon.tsx b/server/sonar-web/design-system/src/components/icons/HelperHintIcon.tsx new file mode 100644 index 00000000000..0a5e6961634 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/HelperHintIcon.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, themeContrast } from '../../helpers/theme'; +import { CustomIcon, IconProps } from './Icon'; + +export default function HelperHintIcon({ fill = 'currentColor', ...iconProps }: IconProps) { + const theme = useTheme(); + return ( + <CustomIcon {...iconProps}> + <circle cx="8" cy="8" fill={themeColor('iconHelperHint')({ theme })} r="7" /> + <path + d="M6.82812 10.2301h1.61506v-.1449c.00852-.83094.30682-1.21872.98012-1.62355.7969-.47301 1.3168-1.09943 1.3168-2.10085C10.7401 4.86932 9.53835 4 7.84659 4 6.29972 4 5.03835 4.80966 5 6.5142h1.73864c.02556-.6946.54119-1.06534 1.09943-1.06534.57528 0 1.03977.38353 1.03977.97586 0 .55823-.40483.92897-.92898 1.26136-.71591.4517-1.11647.90767-1.12074 2.39912v.1449Zm.83949 2.7273c.54546 0 1.01847-.456 1.02273-1.0227-.00426-.5583-.47727-1.0142-1.02273-1.0142-.5625 0-1.02698.4559-1.02272 1.0142-.00426.5667.46022 1.0227 1.02272 1.0227Z" + fill={themeContrast('iconHelperHint')({ theme })} + /> + </CustomIcon> + ); +} diff --git a/server/sonar-web/design-system/src/components/icons/MainBranchIcon.tsx b/server/sonar-web/design-system/src/components/icons/MainBranchIcon.tsx new file mode 100644 index 00000000000..e954c1e74e0 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/MainBranchIcon.tsx @@ -0,0 +1,36 @@ +/* + * 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 MainBranchIcon({ fill = 'currentColor', ...iconProps }: IconProps) { + const theme = useTheme(); + return ( + <CustomIcon {...iconProps}> + <path + clipRule="evenodd" + d="M8.251 2.49932a.75003.75003 0 0 0-.75.75.75001.75001 0 1 0 .75-.75Zm-2.25.75A2.25004 2.25004 0 0 1 7.21713 1.2516a2.25 2.25 0 0 1 2.33319.16148 2.24917 2.24917 0 0 1 .76538.94287c.1639.37851.2206.79478.1639 1.20334a2.25026 2.25026 0 0 1-.48534 1.11323 2.25 2.25 0 0 1-.99326.6988v5.25598c.50069.177.92271.5252 1.1915.9832.2687.458.3669.9963.2771 1.5197a2.25092 2.25092 0 0 1-2.2186 1.8705 2.25092 2.25092 0 0 1-2.21861-1.8705 2.25115 2.25115 0 0 1 .27716-1.5197 2.2514 2.2514 0 0 1 1.19145-.9832V5.37132a2.24999 2.24999 0 0 1-1.5-2.122Zm2.25 8.74998a.74985.74985 0 0 0-.53033.2197.74987.74987 0 0 0-.21967.5303c0 .1989.07902.3897.21967.5304a.75017.75017 0 0 0 1.06066 0 .75023.75023 0 0 0 .21967-.5304.74983.74983 0 0 0-.21967-.5303.74981.74981 0 0 0-.53033-.2197Z" + fill={themeColor(fill)({ theme })} + fillRule="evenodd" + /> + </CustomIcon> + ); +} diff --git a/server/sonar-web/design-system/src/components/icons/PullRequestIcon.tsx b/server/sonar-web/design-system/src/components/icons/PullRequestIcon.tsx new file mode 100644 index 00000000000..4f058f00f00 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/PullRequestIcon.tsx @@ -0,0 +1,23 @@ +/* + * 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 { GitPullRequestIcon } from '@primer/octicons-react'; +import { OcticonHoc } from './Icon'; + +export default OcticonHoc(GitPullRequestIcon, 'PullRequestIcon'); 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 a8ba3597407..b0992dbdb91 100644 --- a/server/sonar-web/design-system/src/components/icons/index.ts +++ b/server/sonar-web/design-system/src/components/icons/index.ts @@ -17,15 +17,19 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +export { default as BranchIcon } from './BranchIcon'; export { default as ChevronDownIcon } from './ChevronDownIcon'; 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 HelperHintIcon } from './HelperHintIcon'; export { default as HomeFillIcon } from './HomeFillIcon'; export { default as HomeIcon } from './HomeIcon'; +export { default as MainBranchIcon } from './MainBranchIcon'; export { default as MenuHelpIcon } from './MenuHelpIcon'; export { default as MenuSearchIcon } from './MenuSearchIcon'; export { default as OpenNewTabIcon } from './OpenNewTabIcon'; +export { default as PullRequestIcon } from './PullRequestIcon'; export { default as StarIcon } from './StarIcon'; diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index 4cb65f8730c..c3fcbab1c4e 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -19,6 +19,7 @@ */ export * from './Avatar'; +export { default as Badge } from './Badge'; export * from './buttons'; export { default as DeferredSpinner } from './DeferredSpinner'; export { default as Dropdown } from './Dropdown'; @@ -29,12 +30,14 @@ export * from './GenericAvatar'; export * from './icons'; export { default as InputSearch } from './InputSearch'; export * from './InteractiveIcon'; +export * from './Link'; export { default as Link } from './Link'; export * from './MainAppBar'; export * from './MainMenu'; export * from './MainMenuItem'; export * from './NavBarTabs'; export * from './popups'; +export { default as QualityGateIndicator } from './QualityGateIndicator'; export * from './SonarQubeLogo'; export * from './Text'; export { default as Tooltip } from './Tooltip'; diff --git a/server/sonar-web/design-system/src/index.ts b/server/sonar-web/design-system/src/index.ts index cd4bd05a51b..c8e853ca7a1 100644 --- a/server/sonar-web/design-system/src/index.ts +++ b/server/sonar-web/design-system/src/index.ts @@ -21,3 +21,4 @@ export * from './components'; export * from './helpers'; export * from './theme'; +export * from './types/theme'; diff --git a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx index 982f207add5..fc86449221d 100644 --- a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx +++ b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; -import { ClockIcon, ItemLink, SearchText, TextMuted } from 'design-system'; +import { ClockIcon, ItemLink, TextBold, TextMuted } from 'design-system'; import * as React from 'react'; import FavoriteIcon from '../../../components/icons/FavoriteIcon'; import { translate } from '../../../helpers/l10n'; @@ -54,7 +54,7 @@ export default class GlobalSearchResult extends React.PureComponent<Props> { to={to} > <div className="sw-flex sw-justify-between sw-items-center sw-w-full"> - <SearchText match={component.match} name={component.name} /> + <TextBold match={component.match} name={component.name} /> <div className="sw-ml-2"> {component.isFavorite && <FavoriteIcon favorite={true} size={16} />} {!component.isFavorite && component.isRecentlyBrowsed && ( diff --git a/server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx b/server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx index 50c85a8c046..2dd18a0151a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx @@ -17,87 +17,48 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { last } from 'lodash'; +import { HoverLink, TextMuted } from 'design-system'; import * as React from 'react'; -import Link from '../../../../components/common/Link'; -import QualifierIcon from '../../../../components/icons/QualifierIcon'; -import { isMainBranch } from '../../../../helpers/branch-like'; +import Favorite from '../../../../components/controls/Favorite'; import { getComponentOverviewUrl } from '../../../../helpers/urls'; -import { BranchLike } from '../../../../types/branch-like'; import { Component } from '../../../../types/types'; -import { colors } from '../../../theme'; +import { CurrentUser, isLoggedIn } from '../../../../types/users'; export interface BreadcrumbProps { component: Component; - currentBranchLike: BranchLike | undefined; + currentUser: CurrentUser; } export function Breadcrumb(props: BreadcrumbProps) { - const { - component: { breadcrumbs }, - currentBranchLike, - } = props; - const lastBreadcrumbElement = last(breadcrumbs); - const isNotMainBranch = currentBranchLike && !isMainBranch(currentBranchLike); + const { component, currentUser } = props; return ( - <div className="big flex-shrink display-flex-center"> - {breadcrumbs.map((breadcrumbElement, i) => { - const isFirst = i === 0; - const isNotLast = i < breadcrumbs.length - 1; + <div className="sw-text-sm sw-flex sw-justify-center"> + {component.breadcrumbs.map((breadcrumbElement, i) => { + const isNotLast = i < component.breadcrumbs.length - 1; const isLast = !isNotLast; - const showQualifierIcon = isFirst && lastBreadcrumbElement; - - const name = - isNotMainBranch || isNotLast ? ( - <> - {showQualifierIcon && !isNotMainBranch && ( - <QualifierIcon - className="spacer-right" - qualifier={lastBreadcrumbElement.qualifier} - fill={colors.neutral800} - /> - )} - <Link - className="link-no-underline" - to={getComponentOverviewUrl(breadcrumbElement.key, breadcrumbElement.qualifier)} - > - {showQualifierIcon && isNotMainBranch && ( - <QualifierIcon - className="spacer-right" - qualifier={lastBreadcrumbElement.qualifier} - fill={colors.primary} - /> - )} - {breadcrumbElement.name} - </Link> - </> - ) : ( - <> - {showQualifierIcon && ( - <QualifierIcon - className="spacer-right" - qualifier={lastBreadcrumbElement.qualifier} - fill={colors.neutral800} - /> - )} - {breadcrumbElement.name} - </> - ); return ( - <span className="flex-shrink display-flex-center" key={breadcrumbElement.key}> - {isLast ? ( - <h1 className="text-ellipsis" title={breadcrumbElement.name}> - {name} - </h1> - ) : ( - <span className="text-ellipsis" title={breadcrumbElement.name}> - {name} - </span> + <div key={breadcrumbElement.key} className="sw-flex"> + {isLast && isLoggedIn(currentUser) && ( + <Favorite + className="sw-mr-2" + component={component.key} + favorite={Boolean(component.isFavorite)} + qualifier={component.qualifier} + /> )} - {isNotLast && <span className="slash-separator" />} - </span> + <HoverLink + blurAfterClick={true} + className="js-project-link sw-flex" + key={breadcrumbElement.name} + title={breadcrumbElement.name} + to={getComponentOverviewUrl(breadcrumbElement.key, breadcrumbElement.qualifier)} + > + <TextMuted text={breadcrumbElement.name} /> + </HoverLink> + {isNotLast && <span className="slash-separator sw-mx-2.5" />} + </div> ); })} </div> diff --git a/server/sonar-web/src/main/js/app/components/nav/component/Header.tsx b/server/sonar-web/src/main/js/app/components/nav/component/Header.tsx index 145ae9377dd..08f786d3a91 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/Header.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/Header.tsx @@ -18,14 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import Favorite from '../../../../components/controls/Favorite'; import { ProjectAlmBindingResponse } from '../../../../types/alm-settings'; import { BranchLike } from '../../../../types/branch-like'; import { Component } from '../../../../types/types'; -import { CurrentUser, isLoggedIn } from '../../../../types/users'; +import { CurrentUser } from '../../../../types/users'; import withCurrentUserContext from '../../current-user/withCurrentUserContext'; import BranchLikeNavigation from './branch-like/BranchLikeNavigation'; -import CurrentBranchLikeMergeInformation from './branch-like/CurrentBranchLikeMergeInformation'; import { Breadcrumb } from './Breadcrumb'; export interface HeaderProps { @@ -40,25 +38,17 @@ export function Header(props: HeaderProps) { const { branchLikes, component, currentBranchLike, currentUser, projectBinding } = props; return ( - <div className="display-flex-center flex-shrink"> - <Breadcrumb component={component} currentBranchLike={currentBranchLike} /> - {isLoggedIn(currentUser) && ( - <Favorite - className="spacer-left" - component={component.key} - favorite={Boolean(component.isFavorite)} - qualifier={component.qualifier} - /> - )} + <div className="sw-flex sw-flex-shrink sw-items-center"> + <Breadcrumb component={component} currentUser={currentUser} /> {currentBranchLike && ( <> + <span className="slash-separator sw-ml-2" /> <BranchLikeNavigation branchLikes={branchLikes} component={component} currentBranchLike={currentBranchLike} projectBinding={projectBinding} /> - <CurrentBranchLikeMergeInformation currentBranchLike={currentBranchLike} /> </> )} </div> diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Breadcrumb-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Breadcrumb-test.tsx deleted file mode 100644 index 25074c59040..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Breadcrumb-test.tsx +++ /dev/null @@ -1,73 +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 { screen } from '@testing-library/react'; -import * as React from 'react'; -import { mockBranch, mockMainBranch } from '../../../../../helpers/mocks/branch-like'; -import { mockComponent } from '../../../../../helpers/mocks/component'; -import { renderComponent } from '../../../../../helpers/testReactTestingUtils'; -import { ComponentQualifier } from '../../../../../types/component'; -import { Breadcrumb, BreadcrumbProps } from '../Breadcrumb'; - -it('should render correctly', () => { - renderBreadcrumb(); - expect(screen.getByRole('link', { name: 'Parent portfolio' })).toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'Child portfolio' })).toBeInTheDocument(); -}); - -it('should render correctly when not on a main branch', () => { - renderBreadcrumb({ - component: mockComponent({ - breadcrumbs: [ - { - key: 'project', - name: 'My Project', - qualifier: ComponentQualifier.Project, - }, - ], - }), - currentBranchLike: mockBranch(), - }); - expect( - screen.getByRole('link', { name: `qualifier.${ComponentQualifier.Project} My Project` }) - ).toBeInTheDocument(); -}); - -function renderBreadcrumb(props: Partial<BreadcrumbProps> = {}) { - return renderComponent( - <Breadcrumb - component={mockComponent({ - breadcrumbs: [ - { - key: 'parent-portfolio', - name: 'Parent portfolio', - qualifier: ComponentQualifier.Portfolio, - }, - { - key: 'child-portfolio', - name: 'Child portfolio', - qualifier: ComponentQualifier.SubPortfolio, - }, - ], - })} - currentBranchLike={mockMainBranch()} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Header-test.tsx index b1f3942d0b6..5add2af3e0e 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Header-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Header-test.tsx @@ -17,34 +17,178 @@ * 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 Favorite from '../../../../../components/controls/Favorite'; -import { mockSetOfBranchAndPullRequest } from '../../../../../helpers/mocks/branch-like'; +import { + mockMainBranch, + mockPullRequest, + mockSetOfBranchAndPullRequestForBranchSelector, +} from '../../../../../helpers/mocks/branch-like'; import { mockComponent } from '../../../../../helpers/mocks/component'; -import { mockCurrentUser } from '../../../../../helpers/testMocks'; +import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks'; +import { renderApp } from '../../../../../helpers/testReactTestingUtils'; +import { AlmKeys } from '../../../../../types/alm-settings'; +import { Feature } from '../../../../../types/features'; +import { BranchStatusContext } from '../../../branch-status/BranchStatusContext'; import { Header, HeaderProps } from '../Header'; -it('should render correctly', () => { - const wrapper = shallowRender({ currentUser: mockCurrentUser({ isLoggedIn: true }) }); - expect(wrapper).toMatchSnapshot(); +jest.mock('../../../../../api/favorites', () => ({ + addFavorite: jest.fn().mockResolvedValue({}), + removeFavorite: jest.fn().mockResolvedValue({}), +})); + +it('should render correctly when there is only 1 branch', () => { + renderHeader({ branchLikes: [mockMainBranch()] }); + expect(screen.getByText('project')).toBeInTheDocument(); + expect(screen.getByLabelText('help-tooltip')).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'branch-1 overview.quality_gate_x.OK' }) + ).toBeDisabled(); +}); + +it('should render correctly when there are multiple branch', async () => { + const user = userEvent.setup(); + renderHeader(); + expect(screen.getByRole('button', { name: 'branch-1 overview.quality_gate_x.OK' })).toBeEnabled(); + expect(screen.queryByLabelText('help-tooltip')).not.toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: 'branch-1 overview.quality_gate_x.OK' })); + expect(screen.getByText('branches.main_branch')).toBeInTheDocument(); + expect( + screen.getByRole('menuitem', { name: 'branch-2 overview.quality_gate_x.ERROR ERROR' }) + ).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: 'branch-3' })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: '1 – PR-1' })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: '2 – PR-2' })).toBeInTheDocument(); + + await user.click( + screen.getByRole('menuitem', { name: 'branch-2 overview.quality_gate_x.ERROR ERROR' }) + ); + expect(screen.getByText('/dashboard?branch=branch-2&id=my-project')).toBeInTheDocument(); }); -it('should not render favorite button if the user is not logged in', () => { - const wrapper = shallowRender(); - expect(wrapper.find(Favorite).exists()).toBe(false); +it('should show manage branch and pull request button for admin', async () => { + const user = userEvent.setup(); + renderHeader({ + currentUser: mockLoggedInUser(), + component: mockComponent({ + configuration: { showSettings: true }, + breadcrumbs: [{ name: 'project', key: 'project', qualifier: 'TRK' }], + }), + }); + await user.click(screen.getByRole('button', { name: 'branch-1 overview.quality_gate_x.OK' })); + + expect(screen.getByRole('link', { name: 'branch_like_navigation.manage' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'branch_like_navigation.manage' })).toHaveAttribute( + 'href', + '/project/branches?id=my-project' + ); }); -function shallowRender(props?: Partial<HeaderProps>) { - const branchLikes = mockSetOfBranchAndPullRequest(); - - return shallow( - <Header - branchLikes={branchLikes} - component={mockComponent()} - currentBranchLike={branchLikes[0]} - currentUser={mockCurrentUser()} - {...props} - /> +it('should render favorite button if the user is logged in', async () => { + const user = userEvent.setup(); + renderHeader({ currentUser: mockLoggedInUser() }); + expect(screen.getByRole('button', { name: 'favorite.action.TRK.add' })).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: 'favorite.action.TRK.add' })); + expect( + await screen.findByRole('button', { name: 'favorite.action.TRK.remove' }) + ).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: 'favorite.action.TRK.remove' })); + expect(screen.getByRole('button', { name: 'favorite.action.TRK.add' })).toBeInTheDocument(); +}); + +it.each([['github'], ['gitlab'], ['bitbucket'], ['azure']])( + 'should show correct %s links for a PR', + (alm: string) => { + renderHeader({ + currentUser: mockLoggedInUser(), + currentBranchLike: mockPullRequest({ + key: '1', + title: 'PR-1', + status: { qualityGateStatus: 'OK' }, + url: alm, + }), + branchLikes: [ + mockPullRequest({ + key: '1', + title: 'PR-1', + status: { qualityGateStatus: 'OK' }, + url: alm, + }), + ], + }); + const image = screen.getByAltText(alm); + expect(image).toBeInTheDocument(); + expect(image).toHaveAttribute('src', `/images/alm/${alm}.svg`); + } +); + +it('should show the correct help tooltip for applications', () => { + renderHeader({ + currentUser: mockLoggedInUser(), + component: mockComponent({ + configuration: { showSettings: true }, + breadcrumbs: [{ name: 'project', key: 'project', qualifier: 'APP' }], + qualifier: 'APP', + }), + branchLikes: [mockMainBranch()], + }); + expect(screen.getByText('application.branches.help')).toBeInTheDocument(); + expect(screen.getByText('application.branches.link')).toBeInTheDocument(); +}); + +it('should show the correct help tooltip when branch support is not enabled', () => { + renderHeader( + { + currentUser: mockLoggedInUser(), + projectBinding: { alm: AlmKeys.GitLab, key: 'key', monorepo: true }, + }, + [] + ); + expect(screen.getByText('branch_like_navigation.no_branch_support.title.mr')).toBeInTheDocument(); + expect( + screen.getByText('branch_like_navigation.no_branch_support.content_x.mr.alm.gitlab') + ).toBeInTheDocument(); +}); + +function renderHeader(props?: Partial<HeaderProps>, featureList = [Feature.BranchSupport]) { + const branchLikes = mockSetOfBranchAndPullRequestForBranchSelector(); + + return renderApp( + '/', + <BranchStatusContext.Provider + value={{ + branchStatusByComponent: { + 'my-project': { + 'branch-branch-1': { + status: 'OK', + }, + 'branch-branch-2': { + status: 'ERROR', + }, + }, + }, + fetchBranchStatus: () => { + /*noop*/ + }, + updateBranchStatus: () => { + /*noop*/ + }, + }} + > + <Header + branchLikes={branchLikes} + component={mockComponent({ + breadcrumbs: [{ name: 'project', key: 'project', qualifier: 'TRK' }], + })} + currentBranchLike={branchLikes[0]} + currentUser={mockCurrentUser()} + {...props} + /> + </BranchStatusContext.Provider>, + { featureList } ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Header-test.tsx.snap deleted file mode 100644 index bac9e18453c..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Header-test.tsx.snap +++ /dev/null @@ -1,153 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<div - className="display-flex-center flex-shrink" -> - <Breadcrumb - component={ - { - "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": [], - } - } - currentBranchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-11", - } - } - /> - <Favorite - className="spacer-left" - component="my-project" - favorite={false} - qualifier="TRK" - /> - <withAvailableFeaturesContext(Component) - branchLikes={ - [ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-11", - }, - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-1", - }, - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - }, - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "1", - "target": "master", - "title": "PR-1", - }, - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-12", - }, - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "2", - "target": "master", - "title": "PR-2", - }, - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-3", - }, - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-2", - }, - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "isOrphan": true, - "key": "2", - "target": "llb-100", - "title": "PR-2", - }, - ] - } - component={ - { - "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": [], - } - } - currentBranchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-11", - } - } - /> - <Memo(CurrentBranchLikeMergeInformation) - currentBranchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-11", - } - } - /> -</div> -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchHelpTooltip.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchHelpTooltip.tsx new file mode 100644 index 00000000000..160b46a5ccf --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchHelpTooltip.tsx @@ -0,0 +1,130 @@ +/* + * 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 { HelperHintIcon } from 'design-system'; +import React from 'react'; +import DocumentationTooltip from '../../../../../components/common/DocumentationTooltip'; +import Link from '../../../../../components/common/Link'; +import HelpTooltip from '../../../../../components/controls/HelpTooltip'; +import { translate, translateWithParameters } from '../../../../../helpers/l10n'; +import { getApplicationAdminUrl } from '../../../../../helpers/urls'; +import { ProjectAlmBindingResponse } from '../../../../../types/alm-settings'; +import { Component } from '../../../../../types/types'; + +interface Props { + component: Component; + isApplication: boolean; + projectBinding?: ProjectAlmBindingResponse; + hasManyBranches: boolean; + canAdminComponent?: boolean; + branchSupportEnabled: boolean; + isGitLab: boolean; +} + +export default function BranchHelpTooltip({ + component, + isApplication, + projectBinding, + hasManyBranches, + canAdminComponent, + branchSupportEnabled, + isGitLab, +}: Props) { + const helpIcon = <HelperHintIcon aria-label="help-tooltip" />; + + if (isApplication) { + if (!hasManyBranches && canAdminComponent) { + return ( + <HelpTooltip + overlay={ + <> + <p>{translate('application.branches.help')}</p> + <hr className="spacer-top spacer-bottom" /> + <Link to={getApplicationAdminUrl(component.key)}> + {translate('application.branches.link')} + </Link> + </> + } + > + {helpIcon} + </HelpTooltip> + ); + } + } else { + if (!branchSupportEnabled) { + return ( + <DocumentationTooltip + content={ + projectBinding !== undefined + ? translateWithParameters( + `branch_like_navigation.no_branch_support.content_x.${isGitLab ? 'mr' : 'pr'}`, + translate('alm', projectBinding.alm) + ) + : translate('branch_like_navigation.no_branch_support.content') + } + data-test="branches-support-disabled" + links={[ + { + href: 'https://www.sonarsource.com/plans-and-pricing/developer/', + label: translate('learn_more'), + doc: false, + }, + ]} + title={ + projectBinding !== undefined + ? translate('branch_like_navigation.no_branch_support.title', isGitLab ? 'mr' : 'pr') + : translate('branch_like_navigation.no_branch_support.title') + } + > + {helpIcon} + </DocumentationTooltip> + ); + } + + if (!hasManyBranches) { + return ( + <DocumentationTooltip + content={translate('branch_like_navigation.only_one_branch.content')} + data-test="only-one-branch-like" + links={[ + { + href: '/analyzing-source-code/branches/branch-analysis/', + label: translate('branch_like_navigation.only_one_branch.documentation'), + }, + { + href: '/analyzing-source-code/pull-request-analysis', + label: translate('branch_like_navigation.only_one_branch.pr_analysis'), + }, + { + href: `/tutorials?id=${component.key}`, + label: translate('branch_like_navigation.tutorial_for_ci'), + inPlace: true, + doc: false, + }, + ]} + title={translate('branch_like_navigation.only_one_branch.title')} + > + {helpIcon} + </DocumentationTooltip> + ); + } + } + + return null; +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.css b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.css deleted file mode 100644 index b96f85754b4..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.css +++ /dev/null @@ -1,78 +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. - */ -.branch-like-navigation-toggler { - padding: 4px 8px; - border: 1px solid transparent; - border-radius: 2px; -} - -.branch-like-navigation-toggler:hover { - border-color: var(--blacka38); - color: inherit !important; -} - -.branch-like-navigation-toggler:active, -.branch-like-navigation-toggler.open { - border-color: var(--primary); -} - -.branch-like-navigation-toggler-container { - height: 26px; -} - -.branch-like-navigation-toggler-container .popup { - min-width: 430px; - max-width: 650px; -} - -.branch-like-navigation-menu .search-box-container { - padding: var(--gridSize); -} - -.branch-like-navigation-menu .search-box-container .search-box, -.branch-like-navigation-menu .search-box-container .search-box-input { - max-width: initial !important; -} - -.branch-like-navigation-menu .item-list { - padding-bottom: var(--gridSize); - max-height: 300px; - overflow-y: auto; -} - -.branch-like-navigation-menu .item { - padding: calc(var(--gridSize) / 2) var(--gridSize); -} - -.branch-like-navigation-menu .item.header { - color: var(--secondFontColor); -} - -.branch-like-navigation-menu .item:not(.header):hover, -.branch-like-navigation-menu .item:not(.header).active { - background-color: var(--barBackgroundColor); - cursor: pointer; -} - -.branch-like-navigation-menu .hint-container { - padding: var(--gridSize); - background-color: var(--barBackgroundColor); - border-top: 1px solid var(--barBorderColor); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx index d4e761b60f5..65d4741665d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx @@ -17,20 +17,21 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import classNames from 'classnames'; +import { ButtonSecondary, PopupPlacement, PopupZLevel, PortalPopup } from 'design-system'; import * as React from 'react'; -import { ButtonPlain } from '../../../../../components/controls/buttons'; -import Toggler from '../../../../../components/controls/Toggler'; -import { ProjectAlmBindingResponse } from '../../../../../types/alm-settings'; +import OutsideClickHandler from '../../../../../components/controls/OutsideClickHandler'; +import { AlmKeys, ProjectAlmBindingResponse } from '../../../../../types/alm-settings'; import { BranchLike } from '../../../../../types/branch-like'; +import { ComponentQualifier } from '../../../../../types/component'; import { Feature } from '../../../../../types/features'; import { Component } from '../../../../../types/types'; import withAvailableFeatures, { WithAvailableFeaturesProps, } from '../../../available-features/withAvailableFeatures'; -import './BranchLikeNavigation.css'; +import BranchHelpTooltip from './BranchHelpTooltip'; import CurrentBranchLike from './CurrentBranchLike'; import Menu from './Menu'; +import PRLink from './PRLink'; export interface BranchLikeNavigationProps extends WithAvailableFeaturesProps { branchLikes: BranchLike[]; @@ -48,59 +49,72 @@ export function BranchLikeNavigation(props: BranchLikeNavigationProps) { projectBinding, } = props; + const isApplication = component.qualifier === ComponentQualifier.Application; + const isGitLab = projectBinding !== undefined && projectBinding.alm === AlmKeys.GitLab; + const [isMenuOpen, setIsMenuOpen] = React.useState(false); const branchSupportEnabled = props.hasFeature(Feature.BranchSupport); - - const canAdminComponent = configuration && configuration.showSettings; + const canAdminComponent = configuration?.showSettings; const hasManyBranches = branchLikes.length >= 2; const isMenuEnabled = branchSupportEnabled && hasManyBranches; const currentBranchLikeElement = ( - <CurrentBranchLike - branchesEnabled={branchSupportEnabled} - component={component} - currentBranchLike={currentBranchLike} - hasManyBranches={hasManyBranches} - projectBinding={projectBinding} - /> + <CurrentBranchLike component={component} currentBranchLike={currentBranchLike} /> ); return ( - <span - className={classNames( - 'big-spacer-left flex-0 branch-like-navigation-toggler-container display-flex-center', - { - dropdown: isMenuEnabled, - } - )} - > - {isMenuEnabled ? ( - <Toggler - onRequestClose={() => setIsMenuOpen(false)} - open={isMenuOpen} + <div className="sw-flex sw-items-center sw-ml-2 it__branch-like-navigation-toggler-container"> + <OutsideClickHandler + onClickOutside={() => { + setIsMenuOpen(false); + }} + > + <PortalPopup + allowResizing={true} overlay={ - <Menu - branchLikes={branchLikes} - canAdminComponent={canAdminComponent} - component={component} - currentBranchLike={currentBranchLike} - onClose={() => setIsMenuOpen(false)} - /> + isMenuOpen && ( + <Menu + branchLikes={branchLikes} + canAdminComponent={canAdminComponent} + component={component} + currentBranchLike={currentBranchLike} + onClose={() => { + setIsMenuOpen(false); + }} + /> + ) } + placement={PopupPlacement.BottomLeft} + zLevel={PopupZLevel.Global} > - <ButtonPlain - className={classNames('branch-like-navigation-toggler', { open: isMenuOpen })} - onClick={() => setIsMenuOpen(!isMenuOpen)} + <ButtonSecondary + className="sw-max-w-abs-350" + onClick={() => { + setIsMenuOpen(!isMenuOpen); + }} + disabled={!isMenuEnabled} aria-expanded={isMenuOpen} aria-haspopup="menu" > {currentBranchLikeElement} - </ButtonPlain> - </Toggler> - ) : ( - currentBranchLikeElement - )} - </span> + </ButtonSecondary> + </PortalPopup> + </OutsideClickHandler> + + <div className="sw-ml-2"> + <BranchHelpTooltip + component={component} + isApplication={isApplication} + projectBinding={projectBinding} + hasManyBranches={hasManyBranches} + canAdminComponent={canAdminComponent} + branchSupportEnabled={branchSupportEnabled} + isGitLab={isGitLab} + /> + </div> + + <PRLink currentBranchLike={currentBranchLike} component={component} /> + </div> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx index 589b1219abb..2de026da87e 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx @@ -17,147 +17,31 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ChevronDownIcon, TextMuted } from 'design-system'; import * as React from 'react'; -import DocumentationTooltip from '../../../../../components/common/DocumentationTooltip'; -import Link from '../../../../../components/common/Link'; -import HelpTooltip from '../../../../../components/controls/HelpTooltip'; import BranchLikeIcon from '../../../../../components/icons/BranchLikeIcon'; -import DropdownIcon from '../../../../../components/icons/DropdownIcon'; -import PlusCircleIcon from '../../../../../components/icons/PlusCircleIcon'; import { getBranchLikeDisplayName } from '../../../../../helpers/branch-like'; -import { translate, translateWithParameters } from '../../../../../helpers/l10n'; -import { getApplicationAdminUrl } from '../../../../../helpers/urls'; -import { AlmKeys, ProjectAlmBindingResponse } from '../../../../../types/alm-settings'; -import { BranchLike } from '../../../../../types/branch-like'; -import { ComponentQualifier } from '../../../../../types/component'; +import { BranchLike, BranchStatusData } from '../../../../../types/branch-like'; import { Component } from '../../../../../types/types'; -import { colors } from '../../../../theme'; +import QualityGateStatus from './QualityGateStatus'; -export interface CurrentBranchLikeProps { - branchesEnabled: boolean; +export interface CurrentBranchLikeProps extends Pick<BranchStatusData, 'status'> { component: Component; currentBranchLike: BranchLike; - hasManyBranches: boolean; - projectBinding?: ProjectAlmBindingResponse; } export function CurrentBranchLike(props: CurrentBranchLikeProps) { - const { - branchesEnabled, - component, - component: { configuration }, - currentBranchLike, - hasManyBranches, - projectBinding, - } = props; + const { component, currentBranchLike } = props; const displayName = getBranchLikeDisplayName(currentBranchLike); - const isApplication = component.qualifier === ComponentQualifier.Application; - const canAdminComponent = configuration && configuration.showSettings; - const isGitLab = projectBinding !== undefined && projectBinding.alm === AlmKeys.GitLab; - - const additionalIcon = () => { - if (branchesEnabled && hasManyBranches) { - return <DropdownIcon />; - } - - const plusIcon = <PlusCircleIcon fill={colors.info500} size={12} />; - - if (isApplication) { - if (!hasManyBranches && canAdminComponent) { - return ( - <HelpTooltip - overlay={ - <> - <p>{translate('application.branches.help')}</p> - <hr className="spacer-top spacer-bottom" /> - <Link to={getApplicationAdminUrl(component.key)}> - {translate('application.branches.link')} - </Link> - </> - } - > - {plusIcon} - </HelpTooltip> - ); - } - } else { - if (!branchesEnabled) { - return ( - <DocumentationTooltip - content={ - projectBinding !== undefined - ? translateWithParameters( - `branch_like_navigation.no_branch_support.content_x.${isGitLab ? 'mr' : 'pr'}`, - translate('alm', projectBinding.alm) - ) - : translate('branch_like_navigation.no_branch_support.content') - } - data-test="branches-support-disabled" - links={[ - { - href: 'https://www.sonarsource.com/plans-and-pricing/developer/', - label: translate('learn_more'), - doc: false, - }, - ]} - title={ - projectBinding !== undefined - ? translate( - 'branch_like_navigation.no_branch_support.title', - isGitLab ? 'mr' : 'pr' - ) - : translate('branch_like_navigation.no_branch_support.title') - } - > - {plusIcon} - </DocumentationTooltip> - ); - } - - if (!hasManyBranches) { - return ( - <DocumentationTooltip - content={translate('branch_like_navigation.only_one_branch.content')} - data-test="only-one-branch-like" - links={[ - { - href: '/analyzing-source-code/branches/branch-analysis/', - label: translate('branch_like_navigation.only_one_branch.documentation'), - }, - { - href: '/analyzing-source-code/pull-request-analysis', - label: translate('branch_like_navigation.only_one_branch.pr_analysis'), - }, - { - href: `/tutorials?id=${component.key}`, - label: translate('branch_like_navigation.tutorial_for_ci'), - inPlace: true, - doc: false, - }, - ]} - title={translate('branch_like_navigation.only_one_branch.title')} - > - {plusIcon} - </DocumentationTooltip> - ); - } - } - - return null; - }; return ( - <span className="display-flex-center flex-shrink text-ellipsis"> - <BranchLikeIcon branchLike={currentBranchLike} fill={colors.info500} /> - <span - className="spacer-left spacer-right flex-shrink text-ellipsis js-branch-like-name" - title={displayName} - > - {displayName} - </span> - {additionalIcon()} - </span> + <div className="sw-flex sw-items-center text-ellipsis"> + <BranchLikeIcon branchLike={currentBranchLike} /> + <TextMuted text={displayName} className="sw-ml-3" /> + <QualityGateStatus branchLike={currentBranchLike} component={component} className="sw-ml-4" /> + <ChevronDownIcon className="sw-ml-1" /> + </div> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx index e1eaf9af75a..bab191696bb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx @@ -17,10 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { DropdownMenu, InputSearch, ItemDivider, Link } from 'design-system'; import * as React from 'react'; -import Link from '../../../../../components/common/Link'; -import { DropdownOverlay } from '../../../../../components/controls/Dropdown'; -import SearchBox from '../../../../../components/controls/SearchBox'; import { Router, withRouter } from '../../../../../components/hoc/withRouter'; import { getBrancheLikesAsTree, @@ -156,43 +154,49 @@ export class Menu extends React.PureComponent<Props, State> { const { canAdminComponent, component, onClose } = this.props; const { branchLikesToDisplay, branchLikesToDisplayTree, query, selectedBranchLike } = this.state; - const showManageLink = component.qualifier === ComponentQualifier.Project && canAdminComponent; const hasResults = branchLikesToDisplay.length > 0; return ( - <DropdownOverlay className="branch-like-navigation-menu" noPadding={true}> - <div className="search-box-container"> - <SearchBox - autoFocus={true} - onChange={this.handleSearchChange} - onKeyDown={this.handleKeyDown} - placeholder={translate('branch_like_navigation.search_for_branch_like')} - value={query} - /> - </div> - - <div className="item-list-container"> - <MenuItemList - branchLikeTree={branchLikesToDisplayTree} - component={component} - hasResults={hasResults} - onSelect={this.handleOnSelect} - selectedBranchLike={selectedBranchLike} - /> - </div> - + <DropdownMenu + className="sw-overflow-y-auto sw-overflow-x-hidden it__branch-like-navigation-menu" + maxHeight="38rem" + size="auto" + > + <InputSearch + className="sw-mx-3 sw-my-2" + autoFocus={true} + onChange={this.handleSearchChange} + onKeyDown={this.handleKeyDown} + placeholder={translate('branch_like_navigation.search_for_branch_like')} + size="auto" + value={query} + searchInputAriaLabel={translate('search_verb')} + clearIconAriaLabel={translate('clear')} + /> + <MenuItemList + branchLikeTree={branchLikesToDisplayTree} + component={component} + hasResults={hasResults} + onSelect={this.handleOnSelect} + selectedBranchLike={selectedBranchLike} + /> {showManageLink && ( - <div className="hint-container text-right"> - <Link - onClick={() => onClose()} - to={{ pathname: '/project/branches', search: queryToSearch({ id: component.key }) }} - > - {translate('branch_like_navigation.manage')} - </Link> - </div> + <> + <ItemDivider /> + <li className="sw-px-3 sw-py-2"> + <Link + onClick={() => { + onClose(); + }} + to={{ pathname: '/project/branches', search: queryToSearch({ id: component.key }) }} + > + {translate('branch_like_navigation.manage')} + </Link> + </li> + </> )} - </DropdownOverlay> + </DropdownMenu> ); } } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx index 2cc14207c37..421addeaa93 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx @@ -18,52 +18,57 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; +import { Badge, ItemButton, TextBold, TextMuted } from 'design-system'; import * as React from 'react'; -import BranchStatus from '../../../../../components/common/BranchStatus'; import BranchLikeIcon from '../../../../../components/icons/BranchLikeIcon'; import { getBranchLikeDisplayName, isMainBranch } from '../../../../../helpers/branch-like'; import { translate } from '../../../../../helpers/l10n'; import { BranchLike } from '../../../../../types/branch-like'; import { Component } from '../../../../../types/types'; +import QualityGateStatus from './QualityGateStatus'; export interface MenuItemProps { branchLike: BranchLike; component: Component; - indent?: boolean; onSelect: (branchLike: BranchLike) => void; selected: boolean; setSelectedNode?: (node: HTMLLIElement) => void; } export function MenuItem(props: MenuItemProps) { - const { branchLike, component, indent, setSelectedNode, onSelect, selected } = props; + const { branchLike, component, setSelectedNode, onSelect, selected } = props; const displayName = getBranchLikeDisplayName(branchLike); return ( - <li - className={classNames('item', { - active: selected, - })} - onClick={() => onSelect(branchLike)} - ref={selected ? setSelectedNode : undefined} + <ItemButton + className={classNames({ active: selected })} + innerRef={selected ? setSelectedNode : undefined} + onClick={() => { + onSelect(branchLike); + }} > - <div - className={classNames('display-flex-center display-flex-space-between', { - 'big-spacer-left': indent, - })} - > - <div className="item-name text-ellipsis" title={displayName}> + <div className="sw-flex sw-items-center sw-justify-between text-ellipsis sw-flex-1"> + <div className="sw-flex sw-items-center"> <BranchLikeIcon branchLike={branchLike} /> - <span className="spacer-left">{displayName}</span> + {isMainBranch(branchLike) && ( - <span className="badge spacer-left">{translate('branches.main_branch')}</span> + <> + <TextBold name={displayName} className="sw-ml-4 sw-mr-2" /> + <Badge variant="default">{translate('branches.main_branch')}</Badge> + </> + )} + {!isMainBranch(branchLike) && ( + <TextMuted text={displayName} className="sw-ml-3 sw-mr-2" /> )} </div> - <div className="spacer-left"> - <BranchStatus branchLike={branchLike} component={component} /> - </div> + <QualityGateStatus + branchLike={branchLike} + component={component} + className="sw-flex sw-items-center sw-w-24" + showStatusText={true} + /> </div> - </li> + </ItemButton> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx index f6c749f926a..ee21a7a07bc 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx @@ -17,11 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { HelperHintIcon, ItemDivider, ItemHeader } from 'design-system'; import * as React from 'react'; import HelpTooltip from '../../../../../components/controls/HelpTooltip'; import { getBranchLikeKey, isSameBranchLike } from '../../../../../helpers/branch-like'; import { translate } from '../../../../../helpers/l10n'; -import { scrollToElement } from '../../../../../helpers/scrolling'; import { isDefined } from '../../../../../helpers/types'; import { BranchLike, BranchLikeTree } from '../../../../../types/branch-like'; import { Component } from '../../../../../types/types'; @@ -36,22 +36,21 @@ export interface MenuItemListProps { } export function MenuItemList(props: MenuItemListProps) { - let listNode: HTMLUListElement | null = null; let selectedNode: HTMLLIElement | null = null; React.useEffect(() => { - if (listNode && selectedNode) { - scrollToElement(selectedNode, { parent: listNode, smooth: false }); + if (selectedNode) { + selectedNode.scrollIntoView({ block: 'center' }); + selectedNode.focus(); } }); const { branchLikeTree, component, hasResults, onSelect, selectedBranchLike } = props; - const renderItem = (branchLike: BranchLike, indent?: boolean) => ( + const renderItem = (branchLike: BranchLike) => ( <MenuItem branchLike={branchLike} component={component} - indent={indent} key={getBranchLikeKey(branchLike)} onSelect={onSelect} selected={isSameBranchLike(branchLike, selectedBranchLike)} @@ -60,11 +59,11 @@ export function MenuItemList(props: MenuItemListProps) { ); return ( - <ul className="item-list" ref={(node) => (listNode = node)}> + <ul className="item-list sw-overflow-scroll"> {!hasResults && ( - <li className="item"> - <span className="note">{translate('no_results')}</span> - </li> + <div className="sw-px-3 sw-py-2"> + <span>{translate('no_results')}</span> + </div> )} {/* BRANCHES & PR */} @@ -75,22 +74,21 @@ export function MenuItemList(props: MenuItemListProps) { {renderItem(tree.branch)} {tree.pullRequests.length > 0 && ( <> - <li className="item header"> - <span className="big-spacer-left"> - {translate('branch_like_navigation.pull_requests')} - </span> - </li> - {tree.pullRequests.map((pr) => renderItem(pr, true))} + <ItemDivider /> + <ItemHeader>{translate('branch_like_navigation.pull_requests')}</ItemHeader> + <ItemDivider /> + {tree.pullRequests.map((pr) => renderItem(pr))} </> )} - <hr /> </React.Fragment> ))} {/* PARENTLESS PR (for display during search) */} {branchLikeTree.parentlessPullRequests.length > 0 && ( <> - <li className="item header">{translate('branch_like_navigation.pull_requests')}</li> + <ItemDivider /> + <ItemHeader>{translate('branch_like_navigation.pull_requests')}</ItemHeader> + <ItemDivider /> {branchLikeTree.parentlessPullRequests.map((pr) => renderItem(pr))} </> )} @@ -98,13 +96,17 @@ export function MenuItemList(props: MenuItemListProps) { {/* ORPHAN PR */} {branchLikeTree.orphanPullRequests.length > 0 && ( <> - <li className="item header"> + <ItemDivider /> + <ItemHeader> {translate('branch_like_navigation.orphan_pull_requests')} <HelpTooltip className="little-spacer-left" overlay={translate('branch_like_navigation.orphan_pull_requests.tooltip')} - /> - </li> + > + <HelperHintIcon /> + </HelpTooltip> + </ItemHeader> + <ItemDivider /> {branchLikeTree.orphanPullRequests.map((pr) => renderItem(pr))} </> )} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/PRLink.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/PRLink.tsx new file mode 100644 index 00000000000..43b590e6ed7 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/PRLink.tsx @@ -0,0 +1,83 @@ +/* + * 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 { isPullRequest } from '../../../../../helpers/branch-like'; +import { translate, translateWithParameters } from '../../../../../helpers/l10n'; +import { getBaseUrl } from '../../../../../helpers/system'; +import { AlmKeys } from '../../../../../types/alm-settings'; +import { BranchLike } from '../../../../../types/branch-like'; +import { Component } from '../../../../../types/types'; + +function getPRUrlAlmKey(url = '') { + const lowerCaseUrl = url.toLowerCase(); + if (lowerCaseUrl.includes(AlmKeys.GitHub)) { + return AlmKeys.GitHub; + } else if (lowerCaseUrl.includes(AlmKeys.GitLab)) { + return AlmKeys.GitLab; + } else if (lowerCaseUrl.includes(AlmKeys.BitbucketServer)) { + return AlmKeys.BitbucketServer; + } else if ( + lowerCaseUrl.includes(AlmKeys.Azure) || + lowerCaseUrl.includes('microsoft') || + lowerCaseUrl.includes('visualstudio') + ) { + return AlmKeys.Azure; + } + return undefined; +} + +export default function PRLink({ + currentBranchLike, + component, +}: { + currentBranchLike: BranchLike; + component: Component; +}) { + if (!isPullRequest(currentBranchLike)) { + return null; + } + + const almKey = + component.alm?.key || + (isPullRequest(currentBranchLike) && getPRUrlAlmKey(currentBranchLike.url)); + return ( + <div> + {currentBranchLike.url !== undefined && ( + <Link + icon={ + almKey && ( + <img + alt={almKey} + height={16} + src={`${getBaseUrl()}/images/alm/${almKey}.svg`} + title={translateWithParameters('branches.see_the_pr_on_x', translate(almKey))} + /> + ) + } + key={currentBranchLike.key} + to={currentBranchLike.url} + > + {!almKey && translate('branches.see_the_pr')} + </Link> + )} + </div> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/QualityGateStatus.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/QualityGateStatus.tsx new file mode 100644 index 00000000000..66daeb57842 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/QualityGateStatus.tsx @@ -0,0 +1,63 @@ +/* + * 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 classNames from 'classnames'; +import { QualityGateIndicator } from 'design-system'; +import React, { useContext } from 'react'; +import { getBranchStatusByBranchLike } from '../../../../../helpers/branch-like'; +import { translateWithParameters } from '../../../../../helpers/l10n'; +import { formatMeasure } from '../../../../../helpers/measures'; +import { BranchLike } from '../../../../../types/branch-like'; +import { Component } from '../../../../../types/types'; +import { BranchStatusContext } from '../../../branch-status/BranchStatusContext'; + +interface Props { + component: Component; + branchLike: BranchLike; + className: string; + showStatusText?: boolean; +} + +export default function QualityGateStatus({ + component, + branchLike, + className, + showStatusText, +}: Props) { + const { branchStatusByComponent } = useContext(BranchStatusContext); + const branchStatus = getBranchStatusByBranchLike( + branchStatusByComponent, + component.key, + branchLike + ); + + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain, @typescript-eslint/no-unnecessary-condition + if (!branchStatus || !branchStatus.status) { + return null; + } + const { status } = branchStatus; + const formatted = formatMeasure(status, 'LEVEL'); + const ariaLabel = translateWithParameters('overview.quality_gate_x', formatted); + return ( + <div className={classNames(className, `it__level-${status}`)}> + <QualityGateIndicator status={status} className="sw-mr-2" ariaLabel={ariaLabel} /> + {showStatusText && <span>{formatted}</span>} + </div> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx deleted file mode 100644 index 8b3b4bd9c40..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx +++ /dev/null @@ -1,73 +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 { ButtonPlain } from '../../../../../../components/controls/buttons'; -import Toggler from '../../../../../../components/controls/Toggler'; -import { mockSetOfBranchAndPullRequest } from '../../../../../../helpers/mocks/branch-like'; -import { mockComponent } from '../../../../../../helpers/mocks/component'; -import { click } from '../../../../../../helpers/testUtils'; -import { BranchLikeNavigation, BranchLikeNavigationProps } from '../BranchLikeNavigation'; - -it('should render correctly', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); -}); - -it('should render the menu trigger if branches are enabled', () => { - const wrapper = shallowRender({ hasFeature: () => true }); - expect(wrapper).toMatchSnapshot(); -}); - -it('should properly toggle menu opening when clicking the anchor', () => { - const wrapper = shallowRender({ hasFeature: () => true }); - expect(wrapper.find(Toggler).props().open).toBe(false); - - click(wrapper.find(ButtonPlain)); - expect(wrapper.find(Toggler).props().open).toBe(true); - - click(wrapper.find(ButtonPlain)); - expect(wrapper.find(Toggler).props().open).toBe(false); -}); - -it('should properly close menu when toggler asks for', () => { - const wrapper = shallowRender({ hasFeature: () => true }); - expect(wrapper.find(Toggler).props().open).toBe(false); - - click(wrapper.find(ButtonPlain)); - expect(wrapper.find(Toggler).props().open).toBe(true); - - wrapper.find(Toggler).props().onRequestClose(); - expect(wrapper.find(Toggler).props().open).toBe(false); -}); - -function shallowRender(props?: Partial<BranchLikeNavigationProps>) { - const branchLikes = mockSetOfBranchAndPullRequest(); - - return shallow( - <BranchLikeNavigation - hasFeature={jest.fn().mockReturnValue(false)} - branchLikes={branchLikes} - component={mockComponent()} - currentBranchLike={branchLikes[0]} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLike-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLike-test.tsx deleted file mode 100644 index 973cdc0c4f2..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLike-test.tsx +++ /dev/null @@ -1,129 +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 { - mockProjectGithubBindingResponse, - mockProjectGitLabBindingResponse, -} from '../../../../../../helpers/mocks/alm-settings'; -import { mockMainBranch } from '../../../../../../helpers/mocks/branch-like'; -import { mockComponent } from '../../../../../../helpers/mocks/component'; -import { ComponentQualifier } from '../../../../../../types/component'; -import { CurrentBranchLike, CurrentBranchLikeProps } from '../CurrentBranchLike'; - -describe('applications', () => { - it('should render correctly when there is only one branch and the user can admin the application', () => { - const wrapper = shallowRender({ - component: mockComponent({ - configuration: { showSettings: true }, - qualifier: ComponentQualifier.Application, - }), - hasManyBranches: false, - }); - expect(wrapper).toMatchSnapshot(); - }); - - it("should render correctly when there is only one branch and the user CAN'T admin the application", () => { - const wrapper = shallowRender({ - component: mockComponent({ - configuration: { showSettings: false }, - qualifier: ComponentQualifier.Application, - }), - hasManyBranches: false, - }); - expect(wrapper).toMatchSnapshot(); - }); - - it('should render correctly when there are many branchlikes', () => { - const wrapper = shallowRender({ - branchesEnabled: true, - component: mockComponent({ - qualifier: ComponentQualifier.Application, - }), - hasManyBranches: true, - }); - expect(wrapper).toMatchSnapshot(); - }); -}); - -describe('projects', () => { - it('should render correctly when branches support is disabled', () => { - expect( - shallowRender({ - branchesEnabled: false, - component: mockComponent({ - qualifier: ComponentQualifier.Project, - }), - }) - ).toMatchSnapshot('default'); - expect( - shallowRender({ - branchesEnabled: false, - component: mockComponent({ - qualifier: ComponentQualifier.Project, - }), - projectBinding: mockProjectGithubBindingResponse(), - }) - ).toMatchSnapshot('alm with prs'); - expect( - shallowRender({ - branchesEnabled: false, - component: mockComponent({ - qualifier: ComponentQualifier.Project, - }), - projectBinding: mockProjectGitLabBindingResponse(), - }) - ).toMatchSnapshot('alm with mrs'); - }); - - it('should render correctly when there is only one branchlike', () => { - const wrapper = shallowRender({ - branchesEnabled: true, - component: mockComponent({ - qualifier: ComponentQualifier.Project, - }), - hasManyBranches: false, - }); - expect(wrapper).toMatchSnapshot(); - }); - - it('should render correctly when there are many branchlikes', () => { - const wrapper = shallowRender({ - branchesEnabled: true, - component: mockComponent({ - qualifier: ComponentQualifier.Project, - }), - hasManyBranches: true, - }); - expect(wrapper).toMatchSnapshot(); - }); -}); - -function shallowRender(props?: Partial<CurrentBranchLikeProps>) { - return shallow( - <CurrentBranchLike - branchesEnabled={false} - component={mockComponent()} - currentBranchLike={mockMainBranch()} - hasManyBranches={false} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx deleted file mode 100644 index 4a95f97830c..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx +++ /dev/null @@ -1,116 +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 Link from '../../../../../../components/common/Link'; -import SearchBox from '../../../../../../components/controls/SearchBox'; -import { KeyboardKeys } from '../../../../../../helpers/keycodes'; -import { - mockPullRequest, - mockSetOfBranchAndPullRequest, -} from '../../../../../../helpers/mocks/branch-like'; -import { mockComponent } from '../../../../../../helpers/mocks/component'; -import { mockRouter } from '../../../../../../helpers/testMocks'; -import { click, mockEvent } from '../../../../../../helpers/testUtils'; -import { queryToSearch } from '../../../../../../helpers/urls'; -import { Menu } from '../Menu'; -import { MenuItemList } from '../MenuItemList'; - -it('should render correctly', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); -}); - -it('should render correctly with no current branch like', () => { - const wrapper = shallowRender({ currentBranchLike: undefined }); - expect(wrapper).toMatchSnapshot(); -}); - -it('should close the menu when "manage branches" link is clicked', () => { - const onClose = jest.fn(); - const wrapper = shallowRender({ onClose }); - - click(wrapper.find(Link)); - expect(onClose).toHaveBeenCalled(); -}); - -it('should change url and close menu when an element is selected', () => { - const onClose = jest.fn(); - const push = jest.fn(); - const router = mockRouter({ push }); - const component = mockComponent(); - const pr = mockPullRequest(); - - const wrapper = shallowRender({ component, onClose, router }); - - wrapper.find(MenuItemList).props().onSelect(pr); - - expect(onClose).toHaveBeenCalled(); - expect(push).toHaveBeenCalledWith( - expect.objectContaining({ - search: queryToSearch({ - id: component.key, - pullRequest: pr.key, - }), - }) - ); -}); - -it('should filter branchlike list correctly', () => { - const wrapper = shallowRender(); - - wrapper.find(SearchBox).props().onChange('PR'); - - expect(wrapper.state().branchLikesToDisplay.length).toBe(3); -}); - -it('should handle keyboard shortcut correctly', () => { - const push = jest.fn(); - const router = mockRouter({ push }); - const wrapper = shallowRender({ currentBranchLike: branchLikes[1], router }); - - const { onKeyDown } = wrapper.find(SearchBox).props(); - - onKeyDown!(mockEvent({ nativeEvent: { key: KeyboardKeys.UpArrow } })); - expect(wrapper.state().selectedBranchLike).toBe(branchLikes[3]); - - onKeyDown!(mockEvent({ nativeEvent: { key: KeyboardKeys.DownArrow } })); - onKeyDown!(mockEvent({ nativeEvent: { key: KeyboardKeys.DownArrow } })); - expect(wrapper.state().selectedBranchLike).toBe(branchLikes[0]); - - onKeyDown!(mockEvent({ nativeEvent: { key: KeyboardKeys.Enter } })); - expect(push).toHaveBeenCalled(); -}); - -const branchLikes = mockSetOfBranchAndPullRequest(); - -function shallowRender(props?: Partial<Menu['props']>) { - return shallow<Menu>( - <Menu - branchLikes={branchLikes} - canAdminComponent={true} - component={mockComponent()} - currentBranchLike={branchLikes[2]} - onClose={jest.fn()} - router={mockRouter()} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItem-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItem-test.tsx deleted file mode 100644 index a5ce319f57f..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItem-test.tsx +++ /dev/null @@ -1,56 +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 { mockMainBranch, mockPullRequest } from '../../../../../../helpers/mocks/branch-like'; -import { mockComponent } from '../../../../../../helpers/mocks/component'; -import { click } from '../../../../../../helpers/testUtils'; -import { MenuItem, MenuItemProps } from '../MenuItem'; - -it('should render a main branch correctly', () => { - const wrapper = shallowRender({ branchLike: mockMainBranch() }); - expect(wrapper).toMatchSnapshot(); -}); - -it('should render a non-main branch, indented and selected item correctly', () => { - const wrapper = shallowRender({ branchLike: mockPullRequest(), indent: true, selected: true }); - expect(wrapper).toMatchSnapshot(); -}); - -it('should propagate click event correctly', () => { - const onSelect = jest.fn(); - const wrapper = shallowRender({ onSelect }); - - click(wrapper.find('li')); - expect(onSelect).toHaveBeenCalled(); -}); - -function shallowRender(props?: Partial<MenuItemProps>) { - return shallow( - <MenuItem - branchLike={mockMainBranch()} - component={mockComponent()} - onSelect={jest.fn()} - selected={false} - setSelectedNode={jest.fn()} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItemList-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItemList-test.tsx deleted file mode 100644 index d448b0af2aa..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItemList-test.tsx +++ /dev/null @@ -1,52 +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 { getBrancheLikesAsTree } from '../../../../../../helpers/branch-like'; -import { - mockPullRequest, - mockSetOfBranchAndPullRequest, -} from '../../../../../../helpers/mocks/branch-like'; -import { mockComponent } from '../../../../../../helpers/mocks/component'; -import { MenuItemList, MenuItemListProps } from '../MenuItemList'; - -it('should render correctly', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); -}); - -function shallowRender(props?: Partial<MenuItemListProps>) { - const branchLikes = [ - ...mockSetOfBranchAndPullRequest(), - mockPullRequest({ base: 'not-in-the-list' }), - ]; - const branchLikeTree = getBrancheLikesAsTree(branchLikes); - - return shallow( - <MenuItemList - branchLikeTree={branchLikeTree} - component={mockComponent()} - hasResults={false} - onSelect={jest.fn()} - selectedBranchLike={branchLikes[0]} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap deleted file mode 100644 index ba319b91b1d..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap +++ /dev/null @@ -1,195 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<span - className="big-spacer-left flex-0 branch-like-navigation-toggler-container display-flex-center" -> - <Memo(CurrentBranchLike) - branchesEnabled={false} - component={ - { - "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": [], - } - } - currentBranchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-11", - } - } - hasManyBranches={true} - /> -</span> -`; - -exports[`should render the menu trigger if branches are enabled 1`] = ` -<span - className="big-spacer-left flex-0 branch-like-navigation-toggler-container display-flex-center dropdown" -> - <Toggler - onRequestClose={[Function]} - open={false} - overlay={ - <withRouter(Menu) - branchLikes={ - [ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-11", - }, - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-1", - }, - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - }, - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "1", - "target": "master", - "title": "PR-1", - }, - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-12", - }, - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "2", - "target": "master", - "title": "PR-2", - }, - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-3", - }, - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-2", - }, - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "isOrphan": true, - "key": "2", - "target": "llb-100", - "title": "PR-2", - }, - ] - } - component={ - { - "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": [], - } - } - currentBranchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-11", - } - } - onClose={[Function]} - /> - } - > - <ButtonPlain - aria-expanded={false} - aria-haspopup="menu" - className="branch-like-navigation-toggler" - onClick={[Function]} - > - <Memo(CurrentBranchLike) - branchesEnabled={true} - component={ - { - "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": [], - } - } - currentBranchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-11", - } - } - hasManyBranches={true} - /> - </ButtonPlain> - </Toggler> -</span> -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap deleted file mode 100644 index 8026ce65c2b..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap +++ /dev/null @@ -1,307 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`applications should render correctly when there are many branchlikes 1`] = ` -<span - className="display-flex-center flex-shrink text-ellipsis" -> - <BranchLikeIcon - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - fill="#0271B9" - /> - <span - className="spacer-left spacer-right flex-shrink text-ellipsis js-branch-like-name" - title="master" - > - master - </span> - <DropdownIcon /> -</span> -`; - -exports[`applications should render correctly when there is only one branch and the user CAN'T admin the application 1`] = ` -<span - className="display-flex-center flex-shrink text-ellipsis" -> - <BranchLikeIcon - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - fill="#0271B9" - /> - <span - className="spacer-left spacer-right flex-shrink text-ellipsis js-branch-like-name" - title="master" - > - master - </span> -</span> -`; - -exports[`applications should render correctly when there is only one branch and the user can admin the application 1`] = ` -<span - className="display-flex-center flex-shrink text-ellipsis" -> - <BranchLikeIcon - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - fill="#0271B9" - /> - <span - className="spacer-left spacer-right flex-shrink text-ellipsis js-branch-like-name" - title="master" - > - master - </span> - <HelpTooltip - overlay={ - <React.Fragment> - <p> - application.branches.help - </p> - <hr - className="spacer-top spacer-bottom" - /> - <ForwardRef(Link) - to={ - { - "pathname": "/project/admin/extension/developer-server/application-console", - "search": "?id=my-project", - } - } - > - application.branches.link - </ForwardRef(Link)> - </React.Fragment> - } - > - <PlusCircleIcon - fill="#0271B9" - size={12} - /> - </HelpTooltip> -</span> -`; - -exports[`projects should render correctly when branches support is disabled: alm with mrs 1`] = ` -<span - className="display-flex-center flex-shrink text-ellipsis" -> - <BranchLikeIcon - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - fill="#0271B9" - /> - <span - className="spacer-left spacer-right flex-shrink text-ellipsis js-branch-like-name" - title="master" - > - master - </span> - <DocumentationTooltip - content="branch_like_navigation.no_branch_support.content_x.mr.alm.gitlab" - data-test="branches-support-disabled" - links={ - [ - { - "doc": false, - "href": "https://www.sonarsource.com/plans-and-pricing/developer/", - "label": "learn_more", - }, - ] - } - title="branch_like_navigation.no_branch_support.title.mr" - > - <PlusCircleIcon - fill="#0271B9" - size={12} - /> - </DocumentationTooltip> -</span> -`; - -exports[`projects should render correctly when branches support is disabled: alm with prs 1`] = ` -<span - className="display-flex-center flex-shrink text-ellipsis" -> - <BranchLikeIcon - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - fill="#0271B9" - /> - <span - className="spacer-left spacer-right flex-shrink text-ellipsis js-branch-like-name" - title="master" - > - master - </span> - <DocumentationTooltip - content="branch_like_navigation.no_branch_support.content_x.pr.alm.github" - data-test="branches-support-disabled" - links={ - [ - { - "doc": false, - "href": "https://www.sonarsource.com/plans-and-pricing/developer/", - "label": "learn_more", - }, - ] - } - title="branch_like_navigation.no_branch_support.title.pr" - > - <PlusCircleIcon - fill="#0271B9" - size={12} - /> - </DocumentationTooltip> -</span> -`; - -exports[`projects should render correctly when branches support is disabled: default 1`] = ` -<span - className="display-flex-center flex-shrink text-ellipsis" -> - <BranchLikeIcon - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - fill="#0271B9" - /> - <span - className="spacer-left spacer-right flex-shrink text-ellipsis js-branch-like-name" - title="master" - > - master - </span> - <DocumentationTooltip - content="branch_like_navigation.no_branch_support.content" - data-test="branches-support-disabled" - links={ - [ - { - "doc": false, - "href": "https://www.sonarsource.com/plans-and-pricing/developer/", - "label": "learn_more", - }, - ] - } - title="branch_like_navigation.no_branch_support.title" - > - <PlusCircleIcon - fill="#0271B9" - size={12} - /> - </DocumentationTooltip> -</span> -`; - -exports[`projects should render correctly when there are many branchlikes 1`] = ` -<span - className="display-flex-center flex-shrink text-ellipsis" -> - <BranchLikeIcon - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - fill="#0271B9" - /> - <span - className="spacer-left spacer-right flex-shrink text-ellipsis js-branch-like-name" - title="master" - > - master - </span> - <DropdownIcon /> -</span> -`; - -exports[`projects should render correctly when there is only one branchlike 1`] = ` -<span - className="display-flex-center flex-shrink text-ellipsis" -> - <BranchLikeIcon - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - fill="#0271B9" - /> - <span - className="spacer-left spacer-right flex-shrink text-ellipsis js-branch-like-name" - title="master" - > - master - </span> - <DocumentationTooltip - content="branch_like_navigation.only_one_branch.content" - data-test="only-one-branch-like" - links={ - [ - { - "href": "/analyzing-source-code/branches/branch-analysis/", - "label": "branch_like_navigation.only_one_branch.documentation", - }, - { - "href": "/analyzing-source-code/pull-request-analysis", - "label": "branch_like_navigation.only_one_branch.pr_analysis", - }, - { - "doc": false, - "href": "/tutorials?id=my-project", - "inPlace": true, - "label": "branch_like_navigation.tutorial_for_ci", - }, - ] - } - title="branch_like_navigation.only_one_branch.title" - > - <PlusCircleIcon - fill="#0271B9" - size={12} - /> - </DocumentationTooltip> -</span> -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap deleted file mode 100644 index 9afa94a156d..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap +++ /dev/null @@ -1,323 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<DropdownOverlay - className="branch-like-navigation-menu" - noPadding={true} -> - <div - className="search-box-container" - > - <SearchBox - autoFocus={true} - onChange={[Function]} - onKeyDown={[Function]} - placeholder="branch_like_navigation.search_for_branch_like" - value="" - /> - </div> - <div - className="item-list-container" - > - <Memo(MenuItemList) - branchLikeTree={ - { - "branchTree": [ - { - "branch": { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-1", - }, - "pullRequests": [], - }, - { - "branch": { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-11", - }, - "pullRequests": [], - }, - { - "branch": { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-12", - }, - "pullRequests": [], - }, - { - "branch": { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-2", - }, - "pullRequests": [], - }, - { - "branch": { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-3", - }, - "pullRequests": [], - }, - ], - "mainBranchTree": { - "branch": { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - }, - "pullRequests": [ - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "2", - "target": "master", - "title": "PR-2", - }, - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "1", - "target": "master", - "title": "PR-1", - }, - ], - }, - "orphanPullRequests": [ - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "isOrphan": true, - "key": "2", - "target": "llb-100", - "title": "PR-2", - }, - ], - "parentlessPullRequests": [], - } - } - component={ - { - "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": [], - } - } - hasResults={true} - onSelect={[Function]} - selectedBranchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - /> - </div> - <div - className="hint-container text-right" - > - <ForwardRef(Link) - onClick={[Function]} - to={ - { - "pathname": "/project/branches", - "search": "?id=my-project", - } - } - > - branch_like_navigation.manage - </ForwardRef(Link)> - </div> -</DropdownOverlay> -`; - -exports[`should render correctly with no current branch like 1`] = ` -<DropdownOverlay - className="branch-like-navigation-menu" - noPadding={true} -> - <div - className="search-box-container" - > - <SearchBox - autoFocus={true} - onChange={[Function]} - onKeyDown={[Function]} - placeholder="branch_like_navigation.search_for_branch_like" - value="" - /> - </div> - <div - className="item-list-container" - > - <Memo(MenuItemList) - branchLikeTree={ - { - "branchTree": [ - { - "branch": { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-1", - }, - "pullRequests": [], - }, - { - "branch": { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-11", - }, - "pullRequests": [], - }, - { - "branch": { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-12", - }, - "pullRequests": [], - }, - { - "branch": { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-2", - }, - "pullRequests": [], - }, - { - "branch": { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-3", - }, - "pullRequests": [], - }, - ], - "mainBranchTree": { - "branch": { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - }, - "pullRequests": [ - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "2", - "target": "master", - "title": "PR-2", - }, - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "1", - "target": "master", - "title": "PR-1", - }, - ], - }, - "orphanPullRequests": [ - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "isOrphan": true, - "key": "2", - "target": "llb-100", - "title": "PR-2", - }, - ], - "parentlessPullRequests": [], - } - } - component={ - { - "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": [], - } - } - hasResults={true} - onSelect={[Function]} - selectedBranchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-11", - } - } - /> - </div> - <div - className="hint-container text-right" - > - <ForwardRef(Link) - onClick={[Function]} - to={ - { - "pathname": "/project/branches", - "search": "?id=my-project", - } - } - > - branch_like_navigation.manage - </ForwardRef(Link)> - </div> -</DropdownOverlay> -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap deleted file mode 100644 index 57f5a7527b9..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap +++ /dev/null @@ -1,146 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render a main branch correctly 1`] = ` -<li - className="item" - onClick={[Function]} -> - <div - className="display-flex-center display-flex-space-between" - > - <div - className="item-name text-ellipsis" - title="master" - > - <BranchLikeIcon - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - /> - <span - className="spacer-left" - > - master - </span> - <span - className="badge spacer-left" - > - branches.main_branch - </span> - </div> - <div - className="spacer-left" - > - <withBranchStatus(BranchStatus) - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - component={ - { - "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": [], - } - } - /> - </div> - </div> -</li> -`; - -exports[`should render a non-main branch, indented and selected item correctly 1`] = ` -<li - className="item active" - onClick={[Function]} -> - <div - className="display-flex-center display-flex-space-between big-spacer-left" - > - <div - className="item-name text-ellipsis" - title="1001 – Foo Bar feature" - > - <BranchLikeIcon - branchLike={ - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "1001", - "target": "master", - "title": "Foo Bar feature", - } - } - /> - <span - className="spacer-left" - > - 1001 – Foo Bar feature - </span> - </div> - <div - className="spacer-left" - > - <withBranchStatus(BranchStatus) - branchLike={ - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "1001", - "target": "master", - "title": "Foo Bar feature", - } - } - component={ - { - "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": [], - } - } - /> - </div> - </div> -</li> -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItemList-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItemList-test.tsx.snap deleted file mode 100644 index 408f21b26a3..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItemList-test.tsx.snap +++ /dev/null @@ -1,417 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<ul - className="item-list" -> - <li - className="item" - > - <span - className="note" - > - no_results - </span> - </li> - <Memo(MenuItem) - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } - component={ - { - "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": [], - } - } - key="branch-master" - onSelect={[MockFunction]} - selected={false} - setSelectedNode={[Function]} - /> - <li - className="item header" - > - <span - className="big-spacer-left" - > - branch_like_navigation.pull_requests - </span> - </li> - <Memo(MenuItem) - branchLike={ - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "2", - "target": "master", - "title": "PR-2", - } - } - component={ - { - "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": [], - } - } - indent={true} - key="pull-request-2" - onSelect={[MockFunction]} - selected={false} - setSelectedNode={[Function]} - /> - <Memo(MenuItem) - branchLike={ - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "1", - "target": "master", - "title": "PR-1", - } - } - component={ - { - "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": [], - } - } - indent={true} - key="pull-request-1" - onSelect={[MockFunction]} - selected={false} - setSelectedNode={[Function]} - /> - <hr /> - <Memo(MenuItem) - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-1", - } - } - component={ - { - "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": [], - } - } - key="branch-branch-1" - onSelect={[MockFunction]} - selected={false} - setSelectedNode={[Function]} - /> - <hr /> - <Memo(MenuItem) - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-11", - } - } - component={ - { - "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": [], - } - } - key="branch-branch-11" - onSelect={[MockFunction]} - selected={true} - setSelectedNode={[Function]} - /> - <hr /> - <Memo(MenuItem) - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-12", - } - } - component={ - { - "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": [], - } - } - key="branch-branch-12" - onSelect={[MockFunction]} - selected={false} - setSelectedNode={[Function]} - /> - <hr /> - <Memo(MenuItem) - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-2", - } - } - component={ - { - "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": [], - } - } - key="branch-branch-2" - onSelect={[MockFunction]} - selected={false} - setSelectedNode={[Function]} - /> - <hr /> - <Memo(MenuItem) - branchLike={ - { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-3", - } - } - component={ - { - "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": [], - } - } - key="branch-branch-3" - onSelect={[MockFunction]} - selected={false} - setSelectedNode={[Function]} - /> - <hr /> - <li - className="item header" - > - branch_like_navigation.pull_requests - </li> - <Memo(MenuItem) - branchLike={ - { - "analysisDate": "2018-01-01", - "base": "not-in-the-list", - "branch": "feature/foo/bar", - "key": "1001", - "target": "master", - "title": "Foo Bar feature", - } - } - component={ - { - "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": [], - } - } - key="pull-request-1001" - onSelect={[MockFunction]} - selected={false} - setSelectedNode={[Function]} - /> - <li - className="item header" - > - branch_like_navigation.orphan_pull_requests - <HelpTooltip - className="little-spacer-left" - overlay="branch_like_navigation.orphan_pull_requests.tooltip" - /> - </li> - <Memo(MenuItem) - branchLike={ - { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "isOrphan": true, - "key": "2", - "target": "llb-100", - "title": "PR-2", - } - } - component={ - { - "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": [], - } - } - key="pull-request-2" - onSelect={[MockFunction]} - selected={false} - setSelectedNode={[Function]} - /> -</ul> -`; diff --git a/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx b/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx index 9c799d8921c..cd75a88aa40 100644 --- a/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx +++ b/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx @@ -34,7 +34,7 @@ export interface DocumentationTooltipProps { export default function DocumentationTooltip(props: DocumentationTooltipProps) { const nextSelectableNode = React.useRef<HTMLElement | undefined | null>(); - const linksRef = React.useRef<(HTMLAnchorElement | null)[]>([]); + const linksRef = React.useRef<Array<HTMLAnchorElement | null>>([]); const helpRef = React.useRef<HTMLElement>(null); const { className, children, content, links, title } = props; @@ -49,7 +49,7 @@ export default function DocumentationTooltip(props: DocumentationTooltipProps) { function handleTabPress(event: KeyboardEvent) { if (event.code === KeyboardKeys.Tab) { - if (event.shiftKey === true) { + if (event.shiftKey) { if (event.target === first(linksRef.current)) { helpRef.current?.focus(); } diff --git a/server/sonar-web/src/main/js/components/icons/BranchLikeIcon.tsx b/server/sonar-web/src/main/js/components/icons/BranchLikeIcon.tsx index 754acf2a4a6..a5d48bd0d62 100644 --- a/server/sonar-web/src/main/js/components/icons/BranchLikeIcon.tsx +++ b/server/sonar-web/src/main/js/components/icons/BranchLikeIcon.tsx @@ -17,20 +17,22 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { BranchIcon, MainBranchIcon, PullRequestIcon, ThemeColors } from 'design-system'; import * as React from 'react'; -import BranchIcon from '../../components/icons/BranchIcon'; import { IconProps } from '../../components/icons/Icon'; -import PullRequestIcon from '../../components/icons/PullRequestIcon'; -import { isPullRequest } from '../../helpers/branch-like'; +import { isMainBranch, isPullRequest } from '../../helpers/branch-like'; import { BranchLike } from '../../types/branch-like'; -export interface BranchLikeIconProps extends IconProps { +export interface BranchLikeIconProps extends Omit<IconProps, 'fill'> { branchLike: BranchLike; + fill?: ThemeColors; } export default function BranchLikeIcon({ branchLike, ...props }: BranchLikeIconProps) { if (isPullRequest(branchLike)) { - return <PullRequestIcon {...props} />; + return <PullRequestIcon fill="pageContentLight" {...props} />; + } else if (isMainBranch(branchLike)) { + return <MainBranchIcon fill="pageContentLight" {...props} />; } - return <BranchIcon {...props} />; + return <BranchIcon fill="pageContentLight" {...props} />; } diff --git a/server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/BranchLikeIcon-test.tsx.snap b/server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/BranchLikeIcon-test.tsx.snap index dfe0b95f619..561ef92174d 100644 --- a/server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/BranchLikeIcon-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/BranchLikeIcon-test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render branch icon correctly 1`] = `"<div><svg height="16" style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 1.41421;" version="1.1" viewBox="0 0 16 16" width="16" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"><path d="M12.5 6.5c0-1.1-.9-2-2-2s-2 .9-2 2c0 .8.5 1.5 1.2 1.8-.3.6-.7 1.1-1.2 1.4-.9.5-1.9.5-2.5.4V4c.9-.2 1.5-1 1.5-1.9 0-1.1-.9-2-2-2s-2 .9-2 2C3.5 3 4.1 3.8 5 4v8c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.7-1.5-1.9v-1c.2 0 .5.1.7.1.7 0 1.5-.1 2.2-.6.8-.5 1.4-1.2 1.7-2.1 1.1 0 1.9-.9 1.9-1.9zm-8-4.4c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.5-1-1zm2 11.9c0 .6-.4 1-1 1s-1-.4-1-1 .4-1 1-1 1 .4 1 1zm4-6.5c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1z" style="fill: #236a97;"></path></svg></div>"`; +exports[`should render branch icon correctly 1`] = `"<div><svg aria-hidden="true" focusable="false" role="img" class="octicon octicon-git-branch" viewBox="0 0 16 16" width="16" height="16" fill="rgb(106,117,144)" style="display: inline-block; user-select: none; vertical-align: middle; overflow: visible;"><path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"></path></svg></div>"`; -exports[`should render pull request icon correctly 1`] = `"<div><svg height="16" style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 1.41421;" version="1.1" viewBox="0 0 16 16" width="16" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"><path d="M13,11.9L13,5.5C13,5.4 13.232,1.996 7.9,2L9.1,0.8L8.5,0.1L5.9,2.6L8.5,5.1L9.2,4.4L7.905,3.008C12.256,2.99 12,5.4 12,5.5L12,11.9C11.1,12.1 10.5,12.9 10.5,13.8C10.5,14.9 11.4,15.8 12.5,15.8C13.6,15.8 14.5,14.9 14.5,13.8C14.5,12.9 13.9,12.2 13,11.9ZM4,11.9C4.9,12.2 5.5,12.9 5.5,13.8C5.5,14.9 4.6,15.8 3.5,15.8C2.4,15.8 1.5,14.9 1.5,13.8C1.5,12.9 2.1,12.1 3,11.9L3,4.1C2.1,3.9 1.5,3.1 1.5,2.2C1.5,1.1 2.4,0.2 3.5,0.2C4.6,0.2 5.5,1.1 5.5,2.2C5.5,3.1 4.9,3.9 4,4.1L4,11.9ZM12.5,14.9C11.9,14.9 11.5,14.5 11.5,13.9C11.5,13.3 11.9,12.9 12.5,12.9C13.1,12.9 13.5,13.3 13.5,13.9C13.5,14.5 13.1,14.9 12.5,14.9ZM3.5,14.9C2.9,14.9 2.5,14.5 2.5,13.9C2.5,13.3 2.9,12.9 3.5,12.9C4.1,12.9 4.5,13.3 4.5,13.9C4.5,14.5 4.1,14.9 3.5,14.9ZM2.5,2.2C2.5,1.6 2.9,1.2 3.5,1.2C4.1,1.2 4.5,1.6 4.5,2.2C4.5,2.8 4.1,3.2 3.5,3.2C2.9,3.2 2.5,2.8 2.5,2.2Z" style="fill: #236a97;"></path></svg></div>"`; +exports[`should render pull request icon correctly 1`] = `"<div><svg aria-hidden="true" focusable="false" role="img" class="octicon octicon-git-pull-request" viewBox="0 0 16 16" width="16" height="16" fill="rgb(106,117,144)" style="display: inline-block; user-select: none; vertical-align: middle; overflow: visible;"><path d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z"></path></svg></div>"`; diff --git a/server/sonar-web/src/main/js/helpers/mocks/branch-like.ts b/server/sonar-web/src/main/js/helpers/mocks/branch-like.ts index becee29b800..30cb65c1a7a 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/branch-like.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/branch-like.ts @@ -59,6 +59,22 @@ export function mockSetOfBranchAndPullRequest(): BranchLike[] { mockPullRequest({ key: '2', title: 'PR-2' }), mockBranch({ name: 'branch-3' }), mockBranch({ name: 'branch-2' }), - mockPullRequest({ key: '2', title: 'PR-2', target: 'llb-100', isOrphan: true }), + mockPullRequest({ + key: '2', + title: 'PR-2', + target: 'llb-100', + isOrphan: true, + }), + ]; +} + +export function mockSetOfBranchAndPullRequestForBranchSelector(): BranchLike[] { + return [ + mockBranch({ name: 'branch-1', status: { qualityGateStatus: 'OK' } }), + mockMainBranch(), + mockPullRequest({ key: '1', title: 'PR-1', status: { qualityGateStatus: 'OK' } }), + mockBranch({ name: 'branch-2', status: { qualityGateStatus: 'OK' } }), + mockPullRequest({ key: '2', title: 'PR-2', status: { qualityGateStatus: 'OK' } }), + mockBranch({ name: 'branch-3', status: { qualityGateStatus: 'OK' } }), ]; } |