Browse Source

SONAR-21169 Rework issues page item layout and styling

tags/10.4.0.87286
7PH 6 months ago
parent
commit
1963d4aebc

+ 4
- 0
server/sonar-web/design-system/src/components/Link.tsx View File

@@ -180,6 +180,10 @@ export const HoverLink = styled(StyledBaseLink)`
--active: ${themeColor('linkTooltipActive')};
--borderActive: ${themeBorder('default', 'linkBorder')};
}

${ExternalIcon} {
color: ${themeColor('linkDiscreet')};
}
`;
HoverLink.displayName = 'HoverLink';


+ 7
- 3
server/sonar-web/design-system/src/components/Tags.tsx View File

@@ -38,6 +38,7 @@ interface Props {
overlay?: React.ReactNode;
popupPlacement?: PopupPlacement;
tags: string[];
tagsClassName?: string;
tagsToDisplay?: number;
tooltip?: React.ComponentType<React.PropsWithChildren<{ overlay: React.ReactNode }>>;
}
@@ -46,6 +47,7 @@ export function Tags({
allowUpdate = false,
ariaTagsListLabel,
className,
tagsClassName,
emptyText,
menuId = '',
overlay,
@@ -55,7 +57,7 @@ export function Tags({
tooltip,
open,
onClose,
}: Props) {
}: Readonly<Props>) {
const displayedTags = tags.slice(0, tagsToDisplay);
const extraTags = tags.slice(tagsToDisplay);
const Tooltip = tooltip || React.Fragment;
@@ -95,7 +97,10 @@ export function Tags({
>
{({ a11yAttrs, onToggleClick, open }) => (
<WrapperButton
className="sw-flex sw-items-center sw-gap-1 sw-p-0 sw-h-auto sw-rounded-0"
className={classNames(
'sw-flex sw-items-center sw-gap-1 sw-p-0 sw-h-auto sw-rounded-0',
tagsClassName,
)}
onClick={onToggleClick}
{...a11yAttrs}
>
@@ -115,7 +120,6 @@ const TagLabel = styled.span`
color: ${themeContrast('tag')};
background: ${themeColor('tag')};

${tw`sw-body-sm`}
${tw`sw-box-border`}
${tw`sw-truncate`}
${tw`sw-rounded-1/2`}

+ 5
- 5
server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx View File

@@ -392,16 +392,16 @@ describe('issue app', () => {
).toHaveAttribute('aria-current', 'true');
});

it('should show issue tags if applicable', async () => {
it('should show sonarlint badge if applicable', async () => {
const user = userEvent.setup();
issuesHandler.setIsAdmin(true);
renderIssueApp();

// Select an issue with an advanced rule
// Select an issue with quick fix available
await user.click(await ui.issueItemAction7.find());

await expect(
screen.getByText('issue.quick_fix_available_with_sonarlint_no_link'),
).toHaveATooltipWithContent('issue.quick_fix_available_with_sonarlint');
await expect(screen.getByText('issue.quick_fix')).toHaveATooltipWithContent(
'issue.quick_fix_available_with_sonarlint',
);
});
});

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/__tests__/IssueHeader-it.tsx View File

@@ -84,7 +84,7 @@ it('renders correctly', async () => {
expect(byText('issue.effort').get()).toBeInTheDocument();

// SonarLint badge
expect(byText('issue.quick_fix_available_with_sonarlint_no_link').get()).toBeInTheDocument();
expect(byText('issue.quick_fix').get()).toBeInTheDocument();

// Rule external engine
expect(byText('eslint').get()).toBeInTheDocument();

+ 15
- 15
server/sonar-web/src/main/js/apps/issues/components/IssueHeaderMeta.tsx View File

@@ -31,21 +31,7 @@ interface Props {

export default function IssueHeaderMeta({ issue }: Readonly<Props>) {
return (
<Note className="sw-flex sw-items-center sw-gap-2 sw-text-xs">
{!!issue.codeVariants?.length && (
<>
<div className="sw-flex sw-gap-1">
<span>{translate('issue.code_variants')}</span>
<Tooltip overlay={issue.codeVariants?.join(', ')}>
<span className="sw-font-semibold">
<LightLabel>{issue.codeVariants?.join(', ')}</LightLabel>
</span>
</Tooltip>
</div>
<SeparatorCircleIcon />
</>
)}

<Note className="sw-flex sw-flex-wrap sw-items-center sw-gap-2 sw-text-xs">
{typeof issue.line === 'number' && (
<>
<div className="sw-flex sw-gap-1">
@@ -76,6 +62,20 @@ export default function IssueHeaderMeta({ issue }: Readonly<Props>) {
</div>
<SeparatorCircleIcon />

{!!issue.codeVariants?.length && (
<>
<div className="sw-flex sw-gap-1">
<span>{translate('issue.code_variants')}</span>
<Tooltip overlay={issue.codeVariants?.join(', ')}>
<span className="sw-font-semibold">
<LightLabel>{issue.codeVariants?.join(', ')}</LightLabel>
</span>
</Tooltip>
</div>
<SeparatorCircleIcon />
</>
)}

<IssueType issue={issue} />
<SeparatorCircleIcon data-guiding-id="issue-4" />
<IssueSeverity issue={issue} />

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/components/IssueHeaderSide.tsx View File

@@ -31,7 +31,7 @@ interface Props {

export default function IssueHeaderSide({ issue }: Readonly<Props>) {
return (
<StyledSection className="sw-flex sw-flex-col sw-pl-4 sw-w-[200px]">
<StyledSection className="sw-flex sw-flex-col sw-pl-4 sw-max-w-[250px]">
<IssueHeaderInfo title={translate('issue.cct_attribute.label')} className="sw-mb-6">
<CleanCodeAttributePill
cleanCodeAttributeCategory={issue.cleanCodeAttributeCategory}

+ 1
- 0
server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCard.tsx View File

@@ -178,6 +178,7 @@ function renderFirstLine(
<>
<SeparatorCircleIcon className="sw-mx-1" />
<Tags
className="sw-body-sm"
emptyText={translate('issue.no_tag')}
ariaTagsListLabel={translate('issue.tags')}
tooltip={Tooltip}

+ 2
- 2
server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx View File

@@ -23,10 +23,10 @@ import * as React from 'react';
import { IssueActions } from '../../../types/issues';
import { Issue } from '../../../types/types';
import IssueAssign from './IssueAssign';
import { SonarLintBadge } from './IssueBadges';
import IssueCommentAction from './IssueCommentAction';
import IssueTags from './IssueTags';
import IssueTransition from './IssueTransition';
import SonarLintBadge from './SonarLintBadge';

interface Props {
issue: Issue;
@@ -104,7 +104,7 @@ export default function IssueActionsBar(props: Readonly<Props>) {

{showSonarLintBadge && issue.quickFixAvailable && (
<li>
<SonarLintBadge quickFixAvailable={issue.quickFixAvailable} />
<SonarLintBadge />
</li>
)}
</ul>

+ 0
- 75
server/sonar-web/src/main/js/components/issue/components/IssueBadges.tsx View File

@@ -1,75 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { translate } from '../../../helpers/l10n';
import Link from '../../common/Link';
import Tooltip from '../../controls/Tooltip';
import SonarLintIcon from '../../icons/SonarLintIcon';

export interface IssueBadgesProps {
quickFixAvailable?: boolean;
}

export default function IssueBadges(props: IssueBadgesProps) {
const { quickFixAvailable } = props;

return (
<div className="sw-flex">
<SonarLintBadge quickFixAvailable={quickFixAvailable} />
</div>
);
}

export function SonarLintBadge({ quickFixAvailable }: { quickFixAvailable?: boolean }) {
if (quickFixAvailable) {
return (
<Tooltip
overlay={
<FormattedMessage
id="issue.quick_fix_available_with_sonarlint"
defaultMessage={translate('issue.quick_fix_available_with_sonarlint')}
values={{
link: (
<Link
to="https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarqube-quick-fix"
target="_blank"
>
SonarLint
</Link>
),
}}
/>
}
mouseLeaveDelay={0.5}
>
<div className="sw-flex sw-items-center">
<SonarLintIcon
className="it__issues-sonarlint-quick-fix"
size={15}
description={translate('issue.quick_fix_available_with_sonarlint_no_link')}
/>
</div>
</Tooltip>
);
}

return null;
}

+ 38
- 15
server/sonar-web/src/main/js/components/issue/components/IssueMetaBar.tsx View File

@@ -19,6 +19,7 @@
*/

import styled from '@emotion/styled';
import classNames from 'classnames';
import { Badge, CommentIcon, SeparatorCircleIcon, themeColor } from 'design-system';
import * as React from 'react';
import { translate, translateWithParameters } from '../../../helpers/l10n';
@@ -27,14 +28,16 @@ import { Issue } from '../../../types/types';
import Tooltip from '../../controls/Tooltip';
import DateFromNow from '../../intl/DateFromNow';
import { WorkspaceContext } from '../../workspace/context';
import IssueBadges from './IssueBadges';
import IssueSeverity from './IssueSeverity';
import IssueType from './IssueType';
import SonarLintBadge from './SonarLintBadge';

interface Props {
issue: Issue;
showLine?: boolean;
}

export default function IssueMetaBar(props: Props) {
export default function IssueMetaBar(props: Readonly<Props>) {
const { issue, showLine } = props;

const { externalRulesRepoNames } = React.useContext(WorkspaceContext);
@@ -46,22 +49,32 @@ export default function IssueMetaBar(props: Props) {
const hasComments = !!issue.comments?.length;

const issueMetaListItemClassNames =
'sw-body-sm sw-overflow-hidden sw-whitespace-nowrap sw-max-w-abs-150';
'sw-body-xs sw-overflow-hidden sw-whitespace-nowrap sw-max-w-abs-150';

return (
<ul className="sw-flex sw-items-center sw-gap-2 sw-body-sm">
<li className={issueMetaListItemClassNames}>
<IssueBadges quickFixAvailable={issue.quickFixAvailable} />
</li>
<ul className="sw-flex sw-items-center sw-gap-2 sw-body-xs">
{issue.quickFixAvailable && (
<>
<li className={issueMetaListItemClassNames}>
<SonarLintBadge compact />
</li>
<SeparatorCircleIcon aria-hidden as="li" />
</>
)}

{ruleEngine && (
<li className={issueMetaListItemClassNames}>
<Tooltip overlay={translateWithParameters('issue.from_external_rule_engine', ruleEngine)}>
<span>
<Badge>{ruleEngine}</Badge>
</span>
</Tooltip>
</li>
<>
<li className={issueMetaListItemClassNames}>
<Tooltip
overlay={translateWithParameters('issue.from_external_rule_engine', ruleEngine)}
>
<span>
<Badge>{ruleEngine}</Badge>
</span>
</Tooltip>
</li>
<SeparatorCircleIcon aria-hidden as="li" />
</>
)}

{!!issue.codeVariants?.length && (
@@ -81,7 +94,9 @@ export default function IssueMetaBar(props: Props) {

{hasComments && (
<>
<IssueMetaListItem className={issueMetaListItemClassNames}>
<IssueMetaListItem
className={classNames(issueMetaListItemClassNames, 'sw-flex sw-gap-1')}
>
<CommentIcon aria-label={translate('issue.comment.formlink')} />
{issue.comments?.length}
</IssueMetaListItem>
@@ -115,6 +130,14 @@ export default function IssueMetaBar(props: Props) {
<IssueMetaListItem className={issueMetaListItemClassNames}>
<DateFromNow date={issue.creationDate} />
</IssueMetaListItem>

<SeparatorCircleIcon aria-hidden as="li" />

<IssueType issue={issue} />

<SeparatorCircleIcon data-guiding-id="issue-4" aria-hidden as="li" />

<IssueSeverity issue={issue} />
</ul>
);
}

+ 2
- 1
server/sonar-web/src/main/js/components/issue/components/IssueTags.tsx View File

@@ -67,7 +67,8 @@ export class IssueTags extends React.PureComponent<Props> {
<Tags
allowUpdate={this.props.canSetTags && !component?.needIssueSync}
ariaTagsListLabel={translate('issue.tags')}
className="js-issue-edit-tags"
className="js-issue-edit-tags sw-body-xs"
tagsClassName="sw-body-xs"
emptyText={translate('issue.no_tag')}
menuId="issue-tags-menu"
onClose={this.handleClose}

+ 9
- 28
server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx View File

@@ -20,49 +20,30 @@
import * as React from 'react';

import { BranchLike } from '../../../types/branch-like';
import { IssueActions } from '../../../types/issues';
import { Issue } from '../../../types/types';
import { CleanCodeAttributePill } from '../../shared/CleanCodeAttributePill';
import IssueMessage from './IssueMessage';
import IssueTags from './IssueTags';

export interface IssueTitleBarProps {
currentPopup?: string;
branchLike?: BranchLike;
displayWhyIsThisAnIssue?: boolean;
issue: Issue;
onChange: (issue: Issue) => void;
togglePopup: (popup: string, show?: boolean) => void;
}

export default function IssueTitleBar(props: IssueTitleBarProps) {
const { issue, displayWhyIsThisAnIssue, currentPopup } = props;
const canSetTags = issue.actions.includes(IssueActions.SetTags);
export default function IssueTitleBar(props: Readonly<IssueTitleBarProps>) {
const { issue, displayWhyIsThisAnIssue, branchLike } = props;

return (
<div className="sw-flex sw-items-end">
<div className="sw-w-full sw-flex sw-flex-col">
<CleanCodeAttributePill
className="sw-mb-2"
cleanCodeAttributeCategory={issue.cleanCodeAttributeCategory}
/>
<div className="sw-w-fit">
<IssueMessage
issue={issue}
branchLike={props.branchLike}
displayWhyIsThisAnIssue={displayWhyIsThisAnIssue}
/>
</div>
</div>
<div className="js-issue-tags sw-body-sm sw-grow-0 sw-whitespace-nowrap">
<IssueTags
canSetTags={canSetTags}
<div className="sw-mt-1 sw-flex sw-items-start sw-justify-between sw-gap-8">
<div className="sw-w-fit">
<IssueMessage
issue={issue}
onChange={props.onChange}
togglePopup={props.togglePopup}
open={currentPopup === 'edit-tags' && canSetTags}
branchLike={branchLike}
displayWhyIsThisAnIssue={displayWhyIsThisAnIssue}
/>
</div>

<CleanCodeAttributePill cleanCodeAttributeCategory={issue.cleanCodeAttributeCategory} />
</div>
);
}

+ 23
- 7
server/sonar-web/src/main/js/components/issue/components/IssueView.tsx View File

@@ -20,15 +20,18 @@

import styled from '@emotion/styled';
import classNames from 'classnames';
import { Checkbox, themeBorder } from 'design-system';
import { BasicSeparator, Checkbox, themeBorder } from 'design-system';
import * as React from 'react';
import { deleteIssueComment, editIssueComment } from '../../../api/issues';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { BranchLike } from '../../../types/branch-like';
import { IssueActions } from '../../../types/issues';
import { Issue } from '../../../types/types';
import SoftwareImpactPillList from '../../shared/SoftwareImpactPillList';
import { updateIssue } from '../actions';
import IssueActionsBar from './IssueActionsBar';
import IssueMetaBar from './IssueMetaBar';
import IssueTags from './IssueTags';
import IssueTitleBar from './IssueTitleBar';

interface Props {
@@ -80,6 +83,7 @@ export default class IssueView extends React.PureComponent<Props> {
const { issue, branchLike, checked, currentPopup, displayWhyIsThisAnIssue } = this.props;

const hasCheckbox = this.props.onCheck != null;
const canSetTags = issue.actions.includes(IssueActions.SetTags);

const issueClass = classNames('it__issue-item sw-p-3 sw-mb-4 sw-rounded-1 sw-bg-white', {
selected: this.props.selected,
@@ -93,9 +97,9 @@ export default class IssueView extends React.PureComponent<Props> {
aria-label={issue.message}
ref={(node) => (this.nodeRef = node)}
>
<div className="sw-flex sw-gap-4">
<div className="sw-flex sw-gap-3">
{hasCheckbox && (
<span className="sw-mt-7 sw-self-start">
<span className="sw-mt-1/2 sw-ml-1 sw-self-start">
<Checkbox
checked={checked ?? false}
onCheck={this.handleCheck}
@@ -105,16 +109,28 @@ export default class IssueView extends React.PureComponent<Props> {
</span>
)}

<div className="sw-flex sw-flex-col sw-grow sw-gap-4">
<div className="sw-flex sw-flex-col sw-grow sw-gap-3">
<IssueTitleBar
currentPopup={currentPopup}
branchLike={branchLike}
displayWhyIsThisAnIssue={displayWhyIsThisAnIssue}
issue={issue}
onChange={this.props.onChange}
togglePopup={this.props.togglePopup}
/>

<div className="sw-mt-1 sw-flex sw-items-start sw-justify-between">
<SoftwareImpactPillList data-guiding-id="issue-2" softwareImpacts={issue.impacts} />
<div className="sw-grow-0 sw-whitespace-nowrap">
<IssueTags
issue={issue}
onChange={this.props.onChange}
togglePopup={this.props.togglePopup}
canSetTags={canSetTags}
open={currentPopup === 'edit-tags' && canSetTags}
/>
</div>
</div>

<BasicSeparator />

<div className="sw-flex sw-gap-2 sw-flex-wrap sw-items-center sw-justify-between">
<IssueActionsBar
currentPopup={currentPopup}

+ 84
- 0
server/sonar-web/src/main/js/components/issue/components/SonarLintBadge.tsx View File

@@ -0,0 +1,84 @@
/*
* 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 { HoverLink } from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { translate } from '../../../helpers/l10n';
import Link from '../../common/Link';
import Tooltip from '../../controls/Tooltip';
import SonarLintIcon from '../../icons/SonarLintIcon';

const SONARLINT_URL =
'https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarqube-quick-fix';

interface Props {
compact?: boolean;
}

export default function SonarLintBadge({ compact }: Readonly<Props>) {
return compact ? <SonarLintBadgeCompact /> : <SonarLintBadgeFull />;
}

function SonarLintBadgeFull() {
return (
<Tooltip
overlay={translate('issue.quick_fix_available_with_sonarlint_no_link')}
mouseLeaveDelay={0.5}
>
<HoverLink to={SONARLINT_URL} className="sw-flex sw-items-center" isExternal showExternalIcon>
<SonarLintIcon
className="it__issues-sonarlint-quick-fix"
size={20}
description={translate('issue.quick_fix_available_with_sonarlint_no_link')}
/>
<span className="sw-ml-1">{translate('issue.quick_fix')}</span>
</HoverLink>
</Tooltip>
);
}

function SonarLintBadgeCompact() {
return (
<Tooltip
overlay={
<FormattedMessage
id="issue.quick_fix_available_with_sonarlint"
defaultMessage={translate('issue.quick_fix_available_with_sonarlint')}
values={{
link: (
<Link to={SONARLINT_URL} target="_blank">
SonarLint
</Link>
),
}}
/>
}
mouseLeaveDelay={0.5}
>
<div className="sw-flex sw-items-center">
<SonarLintIcon
className="it__issues-sonarlint-quick-fix"
size={15}
description={translate('issue.quick_fix_available_with_sonarlint_no_link')}
/>
</div>
</Tooltip>
);
}

+ 4
- 3
server/sonar-web/src/main/js/components/shared/SoftwareImpactPillList.tsx View File

@@ -28,10 +28,10 @@ interface SoftwareImpact {
severity: SoftwareImpactSeverity;
}

interface SoftwareImpactPillListProps
extends Pick<Parameters<typeof SoftwareImpactPill>[0], 'type'> {
interface SoftwareImpactPillListProps extends React.HTMLAttributes<HTMLUListElement> {
softwareImpacts: Array<SoftwareImpact>;
className?: string;
type?: Parameters<typeof SoftwareImpactPill>[0]['type'];
}

const severityMap = {
@@ -44,6 +44,7 @@ export default function SoftwareImpactPillList({
softwareImpacts,
type,
className,
...props
}: Readonly<SoftwareImpactPillListProps>) {
const getQualityLabel = (quality: SoftwareQuality) => translate('software_quality', quality);
const sortingFn = (a: SoftwareImpact, b: SoftwareImpact) => {
@@ -54,7 +55,7 @@ export default function SoftwareImpactPillList({
};

return (
<ul className={classNames('sw-flex sw-gap-2', className)}>
<ul className={classNames('sw-flex sw-gap-2', className)} {...props}>
{softwareImpacts
.slice()
.sort(sortingFn)

+ 2
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -900,6 +900,7 @@ issue.assign.assigned_to_x_click_to_change=Assigned to {0}, click to change
issue.assign.unassigned_click_to_assign=Unassigned, click to assign issue
issue.assign.formlink=Assign
issue.assign.to_me=to me
issue.quick_fix=Quick fix
issue.quick_fix_available_with_sonarlint=Quick fix available in {link}
issue.quick_fix_available_with_sonarlint_no_link=Quick fix available in SonarLint
issue.comment.add_comment=Add Comment
@@ -1082,7 +1083,7 @@ issue.unresolved.description=Unresolved issues have not been addressed in any wa
issue.action.permalink=Get permalink
issue.line_affected=Line affected:
issue.introduced=Introduced:
issue.code_variants=Code variant:
issue.code_variants=Variants:
issue.rule_status=Rule status
issue.effort=Effort:
issue.x_effort={0} effort

Loading…
Cancel
Save