Explorar el Código

SONAR-18776 Migrating breadcrumb and branch selector to MIUI

tags/10.0.0.68432
Revanshu Paliwal hace 1 año
padre
commit
b211e45ffe
Se han modificado 45 ficheros con 1184 adiciones y 2490 borrados
  1. 98
    0
      server/sonar-web/design-system/src/components/Badge.tsx
  2. 3
    3
      server/sonar-web/design-system/src/components/InputSearch.tsx
  3. 182
    0
      server/sonar-web/design-system/src/components/QualityGateIndicator.tsx
  4. 13
    5
      server/sonar-web/design-system/src/components/Text.tsx
  5. 32
    0
      server/sonar-web/design-system/src/components/__tests__/Badge-test.tsx
  6. 1
    1
      server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx
  7. 67
    0
      server/sonar-web/design-system/src/components/__tests__/QualityGateIndicator-test.tsx
  8. 2
    2
      server/sonar-web/design-system/src/components/__tests__/Text-test.tsx
  9. 23
    0
      server/sonar-web/design-system/src/components/icons/BranchIcon.tsx
  10. 35
    0
      server/sonar-web/design-system/src/components/icons/HelperHintIcon.tsx
  11. 36
    0
      server/sonar-web/design-system/src/components/icons/MainBranchIcon.tsx
  12. 23
    0
      server/sonar-web/design-system/src/components/icons/PullRequestIcon.tsx
  13. 4
    0
      server/sonar-web/design-system/src/components/icons/index.ts
  14. 3
    0
      server/sonar-web/design-system/src/components/index.ts
  15. 1
    0
      server/sonar-web/design-system/src/index.ts
  16. 2
    2
      server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx
  17. 27
    66
      server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx
  18. 4
    14
      server/sonar-web/src/main/js/app/components/nav/component/Header.tsx
  19. 0
    73
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/Breadcrumb-test.tsx
  20. 165
    21
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/Header-test.tsx
  21. 0
    153
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Header-test.tsx.snap
  22. 130
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchHelpTooltip.tsx
  23. 0
    78
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.css
  24. 56
    42
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
  25. 11
    127
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx
  26. 38
    34
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx
  27. 26
    21
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx
  28. 23
    21
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx
  29. 83
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/PRLink.tsx
  30. 63
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/QualityGateStatus.tsx
  31. 0
    73
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx
  32. 0
    129
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLike-test.tsx
  33. 0
    116
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx
  34. 0
    56
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItem-test.tsx
  35. 0
    52
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItemList-test.tsx
  36. 0
    195
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap
  37. 0
    307
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap
  38. 0
    323
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap
  39. 0
    146
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap
  40. 0
    417
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItemList-test.tsx.snap
  41. 2
    2
      server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx
  42. 8
    6
      server/sonar-web/src/main/js/components/icons/BranchLikeIcon.tsx
  43. 2
    2
      server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/BranchLikeIcon-test.tsx.snap
  44. 17
    1
      server/sonar-web/src/main/js/helpers/mocks/branch-like.ts
  45. 4
    2
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 98
- 0
server/sonar-web/design-system/src/components/Badge.tsx Ver fichero

@@ -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`}
}
`;

+ 3
- 3
server/sonar-web/design-system/src/components/InputSearch.tsx Ver fichero

@@ -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>

+ 182
- 0
server/sonar-web/design-system/src/components/QualityGateIndicator.tsx Ver fichero

@@ -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>
);
}

+ 13
- 5
server/sonar-web/design-system/src/components/Text.tsx Ver fichero

@@ -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`

+ 32
- 0
server/sonar-web/design-system/src/components/__tests__/Badge-test.tsx Ver fichero

@@ -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');
});

+ 1
- 1
server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx Ver fichero

@@ -82,7 +82,7 @@ function setupWithProps(props: Partial<FCProps<typeof InputSearch>> = {}) {
onChange={jest.fn()}
placeholder="placeholder"
searchInputAriaLabel=""
tooShortText=""
tooShortText="too short"
value="foo"
{...props}
/>

+ 67
- 0
server/sonar-web/design-system/src/components/__tests__/QualityGateIndicator-test.tsx Ver fichero

@@ -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} />);
}

+ 2
- 2
server/sonar-web/design-system/src/components/__tests__/Text-test.tsx Ver fichero

@@ -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',

+ 23
- 0
server/sonar-web/design-system/src/components/icons/BranchIcon.tsx Ver fichero

@@ -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');

+ 35
- 0
server/sonar-web/design-system/src/components/icons/HelperHintIcon.tsx Ver fichero

@@ -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>
);
}

+ 36
- 0
server/sonar-web/design-system/src/components/icons/MainBranchIcon.tsx Ver fichero

@@ -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>
);
}

+ 23
- 0
server/sonar-web/design-system/src/components/icons/PullRequestIcon.tsx Ver fichero

@@ -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');

+ 4
- 0
server/sonar-web/design-system/src/components/icons/index.ts Ver fichero

@@ -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';

+ 3
- 0
server/sonar-web/design-system/src/components/index.ts Ver fichero

@@ -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';

+ 1
- 0
server/sonar-web/design-system/src/index.ts Ver fichero

@@ -21,3 +21,4 @@
export * from './components';
export * from './helpers';
export * from './theme';
export * from './types/theme';

+ 2
- 2
server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx Ver fichero

@@ -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 && (

+ 27
- 66
server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx Ver fichero

@@ -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>

+ 4
- 14
server/sonar-web/src/main/js/app/components/nav/component/Header.tsx Ver fichero

@@ -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>

+ 0
- 73
server/sonar-web/src/main/js/app/components/nav/component/__tests__/Breadcrumb-test.tsx Ver fichero

@@ -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}
/>
);
}

+ 165
- 21
server/sonar-web/src/main/js/app/components/nav/component/__tests__/Header-test.tsx Ver fichero

@@ -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 }
);
}

+ 0
- 153
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Header-test.tsx.snap Ver fichero

@@ -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>
`;

+ 130
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchHelpTooltip.tsx Ver fichero

@@ -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;
}

+ 0
- 78
server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.css Ver fichero

@@ -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);
}

+ 56
- 42
server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx Ver fichero

@@ -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>
);
}


+ 11
- 127
server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx Ver fichero

@@ -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>
);
}


+ 38
- 34
server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx Ver fichero

@@ -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>
);
}
}

+ 26
- 21
server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx Ver fichero

@@ -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>
);
}


+ 23
- 21
server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx Ver fichero

@@ -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))}
</>
)}

+ 83
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/PRLink.tsx Ver fichero

@@ -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>
);
}

+ 63
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/QualityGateStatus.tsx Ver fichero

@@ -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>
);
}

+ 0
- 73
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx Ver fichero

@@ -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}
/>
);
}

+ 0
- 129
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLike-test.tsx Ver fichero

@@ -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}
/>
);
}

+ 0
- 116
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx Ver fichero

@@ -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}
/>
);
}

+ 0
- 56
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItem-test.tsx Ver fichero

@@ -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}
/>
);
}

+ 0
- 52
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItemList-test.tsx Ver fichero

@@ -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}
/>
);
}

+ 0
- 195
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap Ver fichero

@@ -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>
`;

+ 0
- 307
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap Ver fichero

@@ -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>
`;

+ 0
- 323
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap Ver fichero

@@ -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>
`;

+ 0
- 146
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap Ver fichero

@@ -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>
`;

+ 0
- 417
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItemList-test.tsx.snap Ver fichero

@@ -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>
`;

+ 2
- 2
server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx Ver fichero

@@ -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();
}

+ 8
- 6
server/sonar-web/src/main/js/components/icons/BranchLikeIcon.tsx Ver fichero

@@ -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} />;
}

+ 2
- 2
server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/BranchLikeIcon-test.tsx.snap Ver fichero

@@ -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>"`;

+ 17
- 1
server/sonar-web/src/main/js/helpers/mocks/branch-like.ts Ver fichero

@@ -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' } }),
];
}

+ 4
- 2
sonar-core/src/main/resources/org/sonar/l10n/core.properties Ver fichero

@@ -3332,6 +3332,7 @@ overview.1_condition_failed=1 condition failed
overview.X_conditions_failed={0} conditions failed
overview.fix_failed_conditions_with_sonarlint=Fix issues before they fail your Quality Gate with {link} in your IDE. Power up with connected mode!
overview.quality_gate=Quality Gate Status
overview.quality_gate_x=Quality Gate: {0}
overview.quality_gate.help=A Quality Gate is a set of measure-based Boolean conditions. It helps you know immediately whether your project is production-ready. If your current status is not Passed, you'll see which measures caused the problem and the values required to pass.
overview.quality_gate_failed_with_x=with {0} errors
overview.quality_gate_code_clean=Your code is clean!
@@ -4260,6 +4261,7 @@ branches.branch=Branch
branches.main_branch=Main Branch
branches.pr=PR
branches.see_the_pr=See the PR
branches.see_the_pr_on_x=See the PR on {0}

#------------------------------------------------------------------------------
#
@@ -4516,8 +4518,8 @@ favorite.action.TRK.add=Add this project to favorites
favorite.action.TRK.remove=Remove this project from favorites
favorite.action.VW.add=Add this portfolio to favorites
favorite.action.VW.remove=Remove this portfolio from favorites
favorite.action.SVW.add=Add sub-this portfolio to favorites
favorite.action.SVW.remove=Remove sub-this portfolio from favorites
favorite.action.SVW.add=Add this sub-portfolio to favorites
favorite.action.SVW.remove=Remove this sub-portfolio from favorites
favorite.action.APP.add=Add this application to favorites
favorite.action.APP.remove=Remove this application from favorites
favorite.action.TRK.add_x=Add project {0} to favorites

Cargando…
Cancelar
Guardar