aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorstanislavh <stanislav.honcharov@sonarsource.com>2023-08-03 11:36:55 +0200
committersonartech <sonartech@sonarsource.com>2023-08-18 20:02:47 +0000
commitc27adc272a98942fa46e11a9a7732744b7940f22 (patch)
treebafebe188031db00f715f35bf84f8ee808901085 /server
parentf3028f632704711d23bd27ce35bb04bd56f0b984 (diff)
downloadsonarqube-c27adc272a98942fa46e11a9a7732744b7940f22.tar.gz
sonarqube-c27adc272a98942fa46e11a9a7732744b7940f22.zip
SONAR-20023 Modify issue details to reflect CCT
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/design-system/src/components/Pill.tsx1
-rw-r--r--server/sonar-web/design-system/src/theme/light.ts8
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssueHeader-it.tsx140
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx123
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssueHeaderMeta.tsx101
-rw-r--r--server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/DeprecatedFieldTooltip.tsx21
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx145
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueBadges.tsx113
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueMetaBar.tsx128
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueSeverity.tsx19
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTags.tsx5
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueType.tsx19
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueView.tsx20
-rw-r--r--server/sonar-web/src/main/js/components/shared/CleanCodeAttributePill.tsx55
-rw-r--r--server/sonar-web/src/main/js/components/shared/SoftwareImpactPill.tsx67
16 files changed, 643 insertions, 324 deletions
diff --git a/server/sonar-web/design-system/src/components/Pill.tsx b/server/sonar-web/design-system/src/components/Pill.tsx
index bc28d26fd63..6f576d55226 100644
--- a/server/sonar-web/design-system/src/components/Pill.tsx
+++ b/server/sonar-web/design-system/src/components/Pill.tsx
@@ -51,6 +51,7 @@ const StyledPill = styled.span<{
color: ThemeColors;
}>`
${tw`sw-cursor-pointer`};
+ ${tw`sw-body-sm`};
${tw`sw-w-fit`};
${tw`sw-inline-block`};
${tw`sw-whitespace-nowrap`};
diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts
index 8be0dc341c6..9eb9da4fc5f 100644
--- a/server/sonar-web/design-system/src/theme/light.ts
+++ b/server/sonar-web/design-system/src/theme/light.ts
@@ -278,8 +278,8 @@ export const lightTheme = {
// pills
pillDanger: COLORS.red[100],
- pillWarning: COLORS.yellowGreen[500],
- pillInfo: COLORS.indigo[100],
+ pillWarning: COLORS.yellow[100],
+ pillInfo: COLORS.blue[100],
pillNeutral: COLORS.blueGrey[50],
// input select
@@ -647,8 +647,8 @@ export const lightTheme = {
// pills
pillDanger: COLORS.red[800],
- pillWarning: COLORS.yellowGreen[900],
- pillInfo: COLORS.indigo[900],
+ pillWarning: COLORS.yellow[800],
+ pillInfo: COLORS.blue[800],
pillNeutral: COLORS.blueGrey[500],
// breadcrumbs
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueHeader-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueHeader-it.tsx
new file mode 100644
index 00000000000..54fdfeb64a4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueHeader-it.tsx
@@ -0,0 +1,140 @@
+/*
+ * 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 { WorkspaceContext } from '../../../components/workspace/context';
+import { mockIssue, mockRuleDetails } from '../../../helpers/testMocks';
+import { renderComponent } from '../../../helpers/testReactTestingUtils';
+import { byLabelText, byRole, byText } from '../../../helpers/testSelector';
+import { RuleStatus } from '../../../types/rules';
+import { Dict } from '../../../types/types';
+import IssueHeader from '../components/IssueHeader';
+
+jest.useFakeTimers();
+
+it('renders correctly', async () => {
+ const issue = mockIssue();
+ renderIssueHeader(
+ {
+ issue: {
+ ...issue,
+ codeVariants: ['first', 'second'],
+ effort: '5min',
+ quickFixAvailable: true,
+ ruleStatus: RuleStatus.Deprecated,
+ externalRuleEngine: 'eslint',
+ },
+ },
+ { eslint: 'yes' }
+ );
+
+ // Title
+ expect(byRole('heading', { name: issue.message }).get()).toBeInTheDocument();
+ expect(byRole('button', { name: 'permalink' }).get()).toHaveAttribute(
+ 'data-clipboard-text',
+ 'http://localhost/project/issues?issues=AVsae-CQS-9G3txfbFN2&open=AVsae-CQS-9G3txfbFN2&id=myproject'
+ );
+
+ // CCT attribute
+ const cctBadge = byText(
+ `issue.clean_code_attribute_category.${issue.cleanCodeAttributeCategory}.issue`
+ ).get();
+ expect(cctBadge).toBeInTheDocument();
+ await expect(cctBadge).toHaveATooltipWithContent(
+ `issue.clean_code_attribute.${issue.cleanCodeAttribute}`
+ );
+ jest.runOnlyPendingTimers();
+
+ // Software Qualities
+ const qualityBadge = byText(`issue.software_quality.${issue.impacts[0].softwareQuality}`).get();
+ expect(qualityBadge).toBeInTheDocument();
+ await expect(qualityBadge).toHaveATooltipWithContent('issue.software_quality');
+ jest.runOnlyPendingTimers();
+
+ // Deprecated type
+ const type = byText(`issue.type.${issue.type}`).get();
+ expect(type).toBeInTheDocument();
+ await expect(type).toHaveATooltipWithContent('issue.clean_code_attribute');
+ jest.runOnlyPendingTimers();
+
+ // Deprecated severity
+ const severity = byText(`severity.${issue.severity}`).get();
+ expect(severity).toBeInTheDocument();
+ await expect(severity).toHaveATooltipWithContent('issue.severity.new');
+
+ // Code variants
+ expect(byText('issue.code_variants').get()).toBeInTheDocument();
+
+ // Effort
+ expect(byText('issue.effort').get()).toBeInTheDocument();
+
+ // SonarLint badge
+ expect(byText('issue.quick_fix_available_with_sonarlint_no_link').get()).toBeInTheDocument();
+
+ // Rule status - Deprecated
+ expect(byLabelText(`issue.resolution.badge.${RuleStatus.Deprecated}`).get()).toBeInTheDocument();
+
+ // Rule external engine
+ expect(byText(/issue.resolution.badge/).get()).toBeInTheDocument();
+});
+
+it('renders correctly when some data is not provided', () => {
+ const issue = mockIssue();
+ renderIssueHeader({
+ issue,
+ });
+
+ // Code variants
+ expect(byText('issues.facet.code_variants').query()).not.toBeInTheDocument();
+
+ // Effort
+ expect(byText('issue.effort').query()).not.toBeInTheDocument();
+
+ // SonarLint badge
+ expect(
+ byText('issue.quick_fix_available_with_sonarlint_no_link').query()
+ ).not.toBeInTheDocument();
+
+ // Rule status deprecated
+ expect(
+ byLabelText(`issue.resolution.badge.${RuleStatus.Deprecated}`).query()
+ ).not.toBeInTheDocument();
+
+ // Rule external engine
+ expect(byText(/issue.resolution.badge/).query()).not.toBeInTheDocument();
+});
+
+function renderIssueHeader(
+ props: Partial<IssueHeader['props']> = {},
+ externalRules: Dict<string> = {}
+) {
+ return renderComponent(
+ <WorkspaceContext.Provider
+ value={{ openComponent: jest.fn(), externalRulesRepoNames: externalRules }}
+ >
+ <IssueHeader
+ issue={mockIssue()}
+ ruleDetails={mockRuleDetails()}
+ onIssueChange={jest.fn()}
+ {...props}
+ />
+ </WorkspaceContext.Provider>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
index 532873de5d9..eaa850eb853 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
@@ -18,6 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import {
+ Badge,
+ BasicSeparator,
ClipboardIconButton,
IssueMessageHighlighting,
Link,
@@ -29,7 +31,10 @@ import * as React from 'react';
import { setIssueAssignee } from '../../../api/issues';
import { updateIssue } from '../../../components/issue/actions';
import IssueActionsBar from '../../../components/issue/components/IssueActionsBar';
-import IssueTags from '../../../components/issue/components/IssueTags';
+import { RuleBadge } from '../../../components/issue/components/IssueBadges';
+import { CleanCodeAttributePill } from '../../../components/shared/CleanCodeAttributePill';
+import SoftwareImpactPill from '../../../components/shared/SoftwareImpactPill';
+import { WorkspaceContext } from '../../../components/workspace/context';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
import { KeyboardKeys } from '../../../helpers/keycodes';
@@ -38,7 +43,9 @@ import { getKeyboardShortcutEnabled } from '../../../helpers/preferences';
import { getComponentIssuesUrl, getPathUrlAsString, getRuleUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
import { IssueActions, IssueType } from '../../../types/issues';
+import { RuleStatus } from '../../../types/rules';
import { Issue, RuleDetails } from '../../../types/types';
+import IssueHeaderMeta from './IssueHeaderMeta';
interface Props {
issue: Issue;
@@ -103,9 +110,6 @@ export default class IssueHeader extends React.PureComponent<Props, State> {
} else if (event.key === KeyboardKeys.KeyM && this.props.issue.actions.includes('assign')) {
event.preventDefault();
return this.handleAssignement('_me');
- } else if (event.key === KeyboardKeys.KeyI) {
- event.preventDefault();
- return this.handleIssuePopupToggle('set-severity');
} else if (event.key === KeyboardKeys.KeyC) {
event.preventDefault();
return this.handleIssuePopupToggle('comment');
@@ -116,12 +120,44 @@ export default class IssueHeader extends React.PureComponent<Props, State> {
return true;
};
- render() {
+ renderRuleDescription = () => {
const {
issue,
ruleDetails: { key, name, isExternal },
- branchLike,
} = this.props;
+
+ return (
+ <Note>
+ <span className="sw-pr-1">{name}</span>
+ {isExternal ? (
+ <span>({key})</span>
+ ) : (
+ <Link to={getRuleUrl(key)} target="_blank">
+ {key}
+ </Link>
+ )}
+ <WorkspaceContext.Consumer>
+ {({ externalRulesRepoNames }) => {
+ const ruleEngine =
+ (issue.externalRuleEngine && externalRulesRepoNames[issue.externalRuleEngine]) ||
+ issue.externalRuleEngine;
+ if (ruleEngine) {
+ return <Badge className="sw-ml-1">{ruleEngine}</Badge>;
+ }
+
+ return null;
+ }}
+ </WorkspaceContext.Consumer>
+ {(issue.ruleStatus === RuleStatus.Deprecated ||
+ issue.ruleStatus === RuleStatus.Removed) && (
+ <RuleBadge ruleStatus={issue.ruleStatus} className="sw-ml-1" />
+ )}
+ </Note>
+ );
+ };
+
+ render() {
+ const { issue, branchLike } = this.props;
const { issuePopupName } = this.state;
const issueUrl = getComponentIssuesUrl(issue.project, {
...getBranchLikeQuery(branchLike),
@@ -132,46 +168,55 @@ export default class IssueHeader extends React.PureComponent<Props, State> {
const canSetTags = issue.actions.includes(IssueActions.SetTags);
return (
- <header className="sw-flex sw-flex-col sw-gap-3 sw-my-6">
- <div className="sw-flex sw-items-center">
- <PageContentFontWrapper className="sw-body-md-highlight" as="h1">
- <IssueMessageHighlighting
- message={issue.message}
- messageFormattings={issue.messageFormattings}
- />
- </PageContentFontWrapper>
- <ClipboardIconButton
- Icon={LinkIcon}
- aria-label={translate('permalink')}
- className="sw-ml-1 sw-align-bottom"
- copyValue={getPathUrlAsString(issueUrl, false)}
- discreet
+ <header className="sw-flex sw-mb-6">
+ <div className="sw-mr-8 sw-flex-1">
+ <CleanCodeAttributePill
+ cleanCodeAttributeCategory={issue.cleanCodeAttributeCategory}
+ cleanCodeAttribute={issue.cleanCodeAttribute}
/>
- </div>
- <div className="sw-flex sw-items-center sw-justify-between">
- <Note>
- <span className="sw-pr-1">{name}</span>
- {isExternal ? (
- <span>({key})</span>
- ) : (
- <Link to={getRuleUrl(key)} target="_blank">
- {key}
- </Link>
- )}
- </Note>
- <IssueTags
- canSetTags={canSetTags}
+ <div className="sw-flex sw-items-center sw-my-2">
+ <PageContentFontWrapper className="sw-body-md-highlight" as="h1">
+ <IssueMessageHighlighting
+ message={issue.message}
+ messageFormattings={issue.messageFormattings}
+ />
+ <ClipboardIconButton
+ Icon={LinkIcon}
+ aria-label={translate('permalink')}
+ className="sw-ml-1 sw-align-bottom"
+ copyValue={getPathUrlAsString(issueUrl, false)}
+ discreet
+ />
+ </PageContentFontWrapper>
+ </div>
+ <div className="sw-flex sw-items-center sw-justify-between sw-mb-4">
+ {this.renderRuleDescription()}
+ </div>
+ <div className="sw-flex sw-items-center">
+ <Note>{translate('issue.software_qualities.label')}</Note>
+ <ul className="sw-ml-1 sw-flex sw-gap-2">
+ {issue.impacts.map(({ severity, softwareQuality }) => (
+ <li key={softwareQuality}>
+ <SoftwareImpactPill severity={severity} quality={softwareQuality} />
+ </li>
+ ))}
+ </ul>
+ </div>
+ <BasicSeparator className="sw-my-3" />
+ <IssueActionsBar
+ currentPopup={issuePopupName}
issue={issue}
+ onAssign={this.handleAssignement}
onChange={this.props.onIssueChange}
- open={issuePopupName === 'edit-tags' && canSetTags}
togglePopup={this.handleIssuePopupToggle}
+ showSonarLintBadge
/>
</div>
- <IssueActionsBar
- currentPopup={issuePopupName}
+ <IssueHeaderMeta
issue={issue}
- onAssign={this.handleAssignement}
- onChange={this.props.onIssueChange}
+ canSetTags={canSetTags}
+ onIssueChange={this.props.onIssueChange}
+ tagsPopupOpen={issuePopupName === 'edit-tags' && canSetTags}
togglePopup={this.handleIssuePopupToggle}
/>
</header>
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueHeaderMeta.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueHeaderMeta.tsx
new file mode 100644
index 00000000000..72b27214348
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssueHeaderMeta.tsx
@@ -0,0 +1,101 @@
+/*
+ * 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 { BasicSeparator, LightLabel, themeBorder, Tooltip } from 'design-system';
+import React from 'react';
+import DateFromNow from '../../../components/intl/DateFromNow';
+import IssueTags from '../../../components/issue/components/IssueTags';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { Issue } from '../../../types/types';
+
+interface Props {
+ issue: Issue;
+ canSetTags: boolean;
+ onIssueChange: (issue: Issue) => void;
+ tagsPopupOpen?: boolean;
+ togglePopup: (popup: string, show?: boolean) => void;
+}
+
+export default function IssueHeaderMeta(props: Props) {
+ const { issue, canSetTags, tagsPopupOpen } = props;
+
+ const separator = <BasicSeparator className="sw-my-2" />;
+
+ return (
+ <StyledSection className="sw-flex sw-flex-col sw-pl-4 sw-min-w-abs-150 sw-max-w-abs-250">
+ <HotspotHeaderInfo title={translate('issue.tags')}>
+ <IssueTags
+ canSetTags={canSetTags}
+ issue={issue}
+ onChange={props.onIssueChange}
+ open={tagsPopupOpen}
+ togglePopup={props.togglePopup}
+ tagsToDisplay={1}
+ />
+ </HotspotHeaderInfo>
+ {separator}
+
+ {!!issue.codeVariants?.length && (
+ <>
+ <HotspotHeaderInfo title={translate('issue.code_variants')} className="sw-truncate">
+ <Tooltip overlay={issue.codeVariants.join(', ')}>
+ <span>{issue.codeVariants.join(', ')}</span>
+ </Tooltip>
+ </HotspotHeaderInfo>
+ {separator}
+ </>
+ )}
+
+ {issue.effort && (
+ <>
+ <HotspotHeaderInfo title={translate('issue.effort')}>
+ {translateWithParameters('issue.x_effort', issue.effort)}
+ </HotspotHeaderInfo>
+ {separator}
+ </>
+ )}
+
+ <HotspotHeaderInfo title={translate('issue.introduced')}>
+ <DateFromNow date={issue.creationDate} />
+ </HotspotHeaderInfo>
+ </StyledSection>
+ );
+}
+
+interface IssueHeaderMetaItemProps {
+ children: React.ReactNode;
+ title: string;
+ className?: string;
+}
+
+function HotspotHeaderInfo({ children, title, className }: IssueHeaderMetaItemProps) {
+ return (
+ <div className={className}>
+ <LightLabel as="div" className="sw-body-sm-highlight">
+ {title}
+ </LightLabel>
+ {children}
+ </div>
+ );
+}
+
+const StyledSection = styled.div`
+ border-left: ${themeBorder('default', 'pageBlockBorder')};
+`;
diff --git a/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx b/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx
index d69e35f1b67..f5eb7e9df5a 100644
--- a/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx
+++ b/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx
@@ -86,7 +86,7 @@ export default function DocumentationTooltip(props: DocumentationTooltipProps) {
</div>
)}
- {content && <p>{content}</p>}
+ {content && <div>{content}</div>}
{links && (
<>
diff --git a/server/sonar-web/src/main/js/components/issue/components/DeprecatedFieldTooltip.tsx b/server/sonar-web/src/main/js/components/issue/components/DeprecatedFieldTooltip.tsx
index c705f69b6d3..c251de9526c 100644
--- a/server/sonar-web/src/main/js/components/issue/components/DeprecatedFieldTooltip.tsx
+++ b/server/sonar-web/src/main/js/components/issue/components/DeprecatedFieldTooltip.tsx
@@ -17,22 +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.
*/
-import { Link } from 'design-system';
import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
import { translate } from '../../../helpers/l10n';
export interface DeprecatedTooltipProps {
- docUrl: string;
field: 'type' | 'severity';
}
const FILTERS_LIST = {
- type: ['issue.clean_code_attributes', 'issue.software_qualities'],
- severity: ['issue.software_qualities', 'issue.severity.new'],
+ type: ['issue.clean_code_attribute', 'issue.software_quality'],
+ severity: ['issue.software_quality', 'issue.severity.new'],
};
-export function DeprecatedFieldTooltip({ field, docUrl }: DeprecatedTooltipProps) {
+export function DeprecatedFieldTooltip({ field }: DeprecatedTooltipProps) {
return (
<>
<p className="sw-mb-4">{translate('issue', field, 'deprecation.title')}</p>
@@ -42,18 +39,6 @@ export function DeprecatedFieldTooltip({ field, docUrl }: DeprecatedTooltipProps
<li key={key}>{translate(key)}</li>
))}
</ul>
- <hr className="sw-w-full sw-mx-0 sw-my-4" />
- <FormattedMessage
- defaultMessage={translate('learn_more_x')}
- id="learn_more_x"
- values={{
- link: (
- <Link isExternal to={docUrl}>
- {translate('issue', field, 'deprecation.documentation')}
- </Link>
- ),
- }}
- />
</>
);
}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx
index 2b3e6a300a4..d92a6b04131 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx
@@ -18,21 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-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';
-import { isDefined } from '../../../helpers/types';
+import { translate } from '../../../helpers/l10n';
import { IssueActions, IssueResolution, IssueType as IssueTypeEnum } from '../../../types/issues';
-import { RuleStatus } from '../../../types/rules';
import { Issue } from '../../../types/types';
-import Tooltip from '../../controls/Tooltip';
-import DateFromNow from '../../intl/DateFromNow';
import SoftwareImpactPill from '../../shared/SoftwareImpactPill';
-import { WorkspaceContext } from '../../workspace/context';
import IssueAssign from './IssueAssign';
-import IssueBadges from './IssueBadges';
+import { SonarLintBadge } from './IssueBadges';
import IssueCommentAction from './IssueCommentAction';
import IssueSeverity from './IssueSeverity';
import IssueTransition from './IssueTransition';
@@ -44,9 +36,8 @@ interface Props {
onAssign: (login: string) => void;
onChange: (issue: Issue) => void;
togglePopup: (popup: string, show?: boolean) => void;
- className?: string;
- showComments?: boolean;
- showLine?: boolean;
+ showIssueImpact?: boolean;
+ showSonarLintBadge?: boolean;
}
interface State {
@@ -61,9 +52,8 @@ export default function IssueActionsBar(props: Props) {
onAssign,
onChange,
togglePopup,
- className,
- showComments,
- showLine,
+ showIssueImpact,
+ showSonarLintBadge,
} = props;
const [commentState, setCommentState] = React.useState<State>({
@@ -91,29 +81,12 @@ export default function IssueActionsBar(props: Props) {
}
};
- const { externalRulesRepoNames } = React.useContext(WorkspaceContext);
-
- const ruleEngine =
- (issue.externalRuleEngine && externalRulesRepoNames[issue.externalRuleEngine]) ||
- issue.externalRuleEngine;
-
const canAssign = issue.actions.includes(IssueActions.Assign);
const canComment = issue.actions.includes(IssueActions.Comment);
const hasTransitions = issue.transitions.length > 0;
- const hasComments = !!issue.comments?.length;
-
- const issueMetaListItemClassNames = classNames(
- className,
- 'sw-body-sm sw-overflow-hidden sw-whitespace-nowrap sw-max-w-abs-150'
- );
return (
- <div
- className={classNames(
- className,
- 'sw-flex sw-gap-2 sw-flex-wrap sw-items-center sw-justify-between'
- )}
- >
+ <div className="sw-flex sw-gap-3">
<ul className="it__issue-header-actions sw-flex sw-items-center sw-gap-3 sw-body-sm">
<li>
<IssueTransition
@@ -135,16 +108,25 @@ export default function IssueActionsBar(props: Props) {
/>
</li>
- <li className="sw-flex sw-gap-3">
- {issue.impacts.map(({ severity, softwareQuality }) => (
- <SoftwareImpactPill
- key={softwareQuality}
- severity={severity}
- quality={softwareQuality}
- />
- ))}
- </li>
+ {showIssueImpact && (
+ <li className="sw-flex sw-gap-3" data-guiding-id="issue-2">
+ {issue.impacts.map(({ severity, softwareQuality }) => (
+ <SoftwareImpactPill
+ key={softwareQuality}
+ severity={severity}
+ quality={softwareQuality}
+ />
+ ))}
+ </li>
+ )}
+ {showSonarLintBadge && issue.quickFixAvailable && (
+ <li>
+ <SonarLintBadge quickFixAvailable={issue.quickFixAvailable} />
+ </li>
+ )}
+ </ul>
+ <ul className="sw-flex sw-items-center sw-gap-3 sw-body-sm" data-guiding-id="issue-4">
<li>
<IssueType issue={issue} />
</li>
@@ -163,83 +145,6 @@ export default function IssueActionsBar(props: Props) {
toggleComment={toggleComment}
/>
)}
-
- <ul className="sw-flex sw-items-center sw-gap-2 sw-body-sm">
- <li className={issueMetaListItemClassNames}>
- <IssueBadges
- quickFixAvailable={issue.quickFixAvailable}
- ruleStatus={issue.ruleStatus as RuleStatus | undefined}
- />
- </li>
-
- {ruleEngine && (
- <li className={issueMetaListItemClassNames}>
- <Tooltip
- overlay={translateWithParameters('issue.from_external_rule_engine', ruleEngine)}
- >
- <span>
- <Badge>{ruleEngine}</Badge>
- </span>
- </Tooltip>
- </li>
- )}
-
- {!!issue.codeVariants?.length && (
- <>
- <IssueMetaListItem>
- <Tooltip overlay={issue.codeVariants.join(', ')}>
- <span>
- {issue.codeVariants.length > 1
- ? translateWithParameters('issue.x_code_variants', issue.codeVariants.length)
- : translate('issue.1_code_variant')}
- </span>
- </Tooltip>
- </IssueMetaListItem>
- <SeparatorCircleIcon aria-hidden as="li" />
- </>
- )}
-
- {showComments && hasComments && (
- <>
- <IssueMetaListItem className={issueMetaListItemClassNames}>
- <CommentIcon aria-label={translate('issue.comment.formlink')} />
- {issue.comments?.length}
- </IssueMetaListItem>
-
- <SeparatorCircleIcon aria-hidden as="li" />
- </>
- )}
-
- {showLine && isDefined(issue.textRange) && (
- <>
- <Tooltip overlay={translate('line_number')}>
- <IssueMetaListItem className={issueMetaListItemClassNames}>
- {translateWithParameters('issue.ncloc_x.short', issue.textRange.endLine)}
- </IssueMetaListItem>
- </Tooltip>
-
- <SeparatorCircleIcon aria-hidden as="li" />
- </>
- )}
-
- {issue.effort && (
- <>
- <IssueMetaListItem className={issueMetaListItemClassNames}>
- {translateWithParameters('issue.x_effort', issue.effort)}
- </IssueMetaListItem>
-
- <SeparatorCircleIcon aria-hidden as="li" />
- </>
- )}
-
- <IssueMetaListItem className={issueMetaListItemClassNames}>
- <DateFromNow date={issue.creationDate} />
- </IssueMetaListItem>
- </ul>
</div>
);
}
-
-const IssueMetaListItem = styled.li`
- color: ${themeColor('pageContentLight')};
-`;
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueBadges.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueBadges.tsx
index 6fc6ff465d1..e62255154bc 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueBadges.tsx
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueBadges.tsx
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import classNames from 'classnames';
import { Badge } from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
@@ -37,50 +38,74 @@ export default function IssueBadges(props: IssueBadgesProps) {
return (
<div className="sw-flex">
- {quickFixAvailable && (
- <Tooltip
- overlay={
- <FormattedMessage
- id="issue.quick_fix_available_with_sonarlint"
- defaultMessage={translate('issue.quick_fix_available_with_sonarlint')}
- values={{
- link: (
- <Link
- to="https://www.sonarqube.org/sonarlint/?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>
- )}
- {ruleStatus &&
- (ruleStatus === RuleStatus.Deprecated || ruleStatus === RuleStatus.Removed) && (
- <DocumentationTooltip
- className="sw-ml-2"
- content={translate('rules.status', ruleStatus, 'help')}
- links={[
- {
- href: '/user-guide/rules/overview/',
- label: translateWithParameters('see_x', translate('rules')),
- },
- ]}
- >
- <Badge variant="deleted">{translate('issue.resolution.badge', ruleStatus)}</Badge>
- </DocumentationTooltip>
- )}
+ <SonarLintBadge quickFixAvailable={quickFixAvailable} />
+ <span className={classNames({ 'sw-ml-2': quickFixAvailable })}>
+ <RuleBadge ruleStatus={ruleStatus} />
+ </span>
</div>
);
}
+
+export function RuleBadge({
+ ruleStatus,
+ className,
+}: {
+ ruleStatus?: RuleStatus;
+ className?: string;
+}) {
+ if (ruleStatus === RuleStatus.Deprecated || ruleStatus === RuleStatus.Removed) {
+ return (
+ <DocumentationTooltip
+ content={translate('rules.status', ruleStatus, 'help')}
+ links={[
+ {
+ href: '/user-guide/rules/overview/',
+ label: translateWithParameters('see_x', translate('rules')),
+ },
+ ]}
+ >
+ <Badge variant="deleted" className={className}>
+ {translate('issue.resolution.badge', ruleStatus)}
+ </Badge>
+ </DocumentationTooltip>
+ );
+ }
+
+ return null;
+}
+
+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.sonarqube.org/sonarlint/?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;
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMetaBar.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueMetaBar.tsx
new file mode 100644
index 00000000000..14c5223eaa5
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueMetaBar.tsx
@@ -0,0 +1,128 @@
+/*
+ * 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 { Badge, CommentIcon, SeparatorCircleIcon, themeColor } from 'design-system';
+import * as React from 'react';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { isDefined } from '../../../helpers/types';
+import { RuleStatus } from '../../../types/rules';
+import { Issue } from '../../../types/types';
+import Tooltip from '../../controls/Tooltip';
+import DateFromNow from '../../intl/DateFromNow';
+import { WorkspaceContext } from '../../workspace/context';
+import IssueBadges from './IssueBadges';
+
+interface Props {
+ issue: Issue;
+ showLine?: boolean;
+}
+
+export default function IssueMetaBar(props: Props) {
+ const { issue, showLine } = props;
+
+ const { externalRulesRepoNames } = React.useContext(WorkspaceContext);
+
+ const ruleEngine =
+ (issue.externalRuleEngine && externalRulesRepoNames[issue.externalRuleEngine]) ||
+ issue.externalRuleEngine;
+
+ const hasComments = !!issue.comments?.length;
+
+ const issueMetaListItemClassNames =
+ 'sw-body-sm 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}
+ ruleStatus={issue.ruleStatus as RuleStatus | undefined}
+ />
+ </li>
+
+ {ruleEngine && (
+ <li className={issueMetaListItemClassNames}>
+ <Tooltip overlay={translateWithParameters('issue.from_external_rule_engine', ruleEngine)}>
+ <span>
+ <Badge>{ruleEngine}</Badge>
+ </span>
+ </Tooltip>
+ </li>
+ )}
+
+ {!!issue.codeVariants?.length && (
+ <>
+ <IssueMetaListItem>
+ <Tooltip overlay={issue.codeVariants.join(', ')}>
+ <span>
+ {issue.codeVariants.length > 1
+ ? translateWithParameters('issue.x_code_variants', issue.codeVariants.length)
+ : translate('issue.1_code_variant')}
+ </span>
+ </Tooltip>
+ </IssueMetaListItem>
+ <SeparatorCircleIcon aria-hidden as="li" />
+ </>
+ )}
+
+ {hasComments && (
+ <>
+ <IssueMetaListItem className={issueMetaListItemClassNames}>
+ <CommentIcon aria-label={translate('issue.comment.formlink')} />
+ {issue.comments?.length}
+ </IssueMetaListItem>
+
+ <SeparatorCircleIcon aria-hidden as="li" />
+ </>
+ )}
+
+ {showLine && isDefined(issue.textRange) && (
+ <>
+ <Tooltip overlay={translate('line_number')}>
+ <IssueMetaListItem className={issueMetaListItemClassNames}>
+ {translateWithParameters('issue.ncloc_x.short', issue.textRange.endLine)}
+ </IssueMetaListItem>
+ </Tooltip>
+
+ <SeparatorCircleIcon aria-hidden as="li" />
+ </>
+ )}
+
+ {issue.effort && (
+ <>
+ <IssueMetaListItem className={issueMetaListItemClassNames}>
+ {translateWithParameters('issue.x_effort', issue.effort)}
+ </IssueMetaListItem>
+
+ <SeparatorCircleIcon aria-hidden as="li" />
+ </>
+ )}
+
+ <IssueMetaListItem className={issueMetaListItemClassNames}>
+ <DateFromNow date={issue.creationDate} />
+ </IssueMetaListItem>
+ </ul>
+ );
+}
+
+const IssueMetaListItem = styled.li`
+ color: ${themeColor('pageContentLight')};
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.tsx
index 43a7f69021e..c1b0755e2e4 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.tsx
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.tsx
@@ -18,12 +18,12 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { DisabledText, Tooltip } from 'design-system';
+import { DisabledText } from 'design-system';
import * as React from 'react';
-import { useDocUrl } from '../../../helpers/docs';
import { translate } from '../../../helpers/l10n';
import { IssueSeverity as IssueSeverityType } from '../../../types/issues';
import { Issue } from '../../../types/types';
+import DocumentationTooltip from '../../common/DocumentationTooltip';
import IssueSeverityIcon from '../../icon-mappers/IssueSeverityIcon';
import { DeprecatedFieldTooltip } from './DeprecatedFieldTooltip';
@@ -32,12 +32,15 @@ interface Props {
}
export default function IssueSeverity({ issue }: Props) {
- const docUrl = useDocUrl('/user-guide/clean-code');
-
return (
- <Tooltip
- mouseLeaveDelay={0.25}
- overlay={<DeprecatedFieldTooltip field="severity" docUrl={docUrl} />}
+ <DocumentationTooltip
+ content={<DeprecatedFieldTooltip field="severity" />}
+ links={[
+ {
+ href: '/user-guide/issues',
+ label: translate('learn_more'),
+ },
+ ]}
>
<DisabledText className="sw-flex sw-items-center sw-gap-1 sw-cursor-not-allowed">
<IssueSeverityIcon
@@ -47,6 +50,6 @@ export default function IssueSeverity({ issue }: Props) {
/>
{translate('severity', issue.severity)}
</DisabledText>
- </Tooltip>
+ </DocumentationTooltip>
);
}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTags.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueTags.tsx
index 88d7f6fbb11..51d01c81c92 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueTags.tsx
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueTags.tsx
@@ -35,6 +35,7 @@ interface Props extends ComponentContextShape {
onChange: (issue: Issue) => void;
open?: boolean;
togglePopup: (popup: string, show?: boolean) => void;
+ tagsToDisplay?: number;
}
export class IssueTags extends React.PureComponent<Props> {
@@ -59,7 +60,7 @@ export class IssueTags extends React.PureComponent<Props> {
};
render() {
- const { component, issue, open } = this.props;
+ const { component, issue, open, tagsToDisplay = 2 } = this.props;
const { tags = [] } = issue;
return (
@@ -74,7 +75,7 @@ export class IssueTags extends React.PureComponent<Props> {
overlay={<IssueTagsPopup selectedTags={tags} setTags={this.setTags} />}
popupPlacement={PopupPlacement.Bottom}
tags={tags}
- tagsToDisplay={2}
+ tagsToDisplay={tagsToDisplay}
tooltip={Tooltip}
/>
);
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueType.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueType.tsx
index 12a1cd4cdd5..37406bc8bde 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueType.tsx
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueType.tsx
@@ -18,11 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { DisabledText, Tooltip } from 'design-system';
+import { DisabledText } from 'design-system';
import * as React from 'react';
-import { useDocUrl } from '../../../helpers/docs';
import { translate } from '../../../helpers/l10n';
import { Issue } from '../../../types/types';
+import DocumentationTooltip from '../../common/DocumentationTooltip';
import IssueTypeIcon from '../../icon-mappers/IssueTypeIcon';
import { DeprecatedFieldTooltip } from './DeprecatedFieldTooltip';
@@ -31,17 +31,20 @@ interface Props {
}
export default function IssueType({ issue }: Props) {
- const docUrl = useDocUrl('/user-guide/clean-code');
-
return (
- <Tooltip
- mouseLeaveDelay={0.25}
- overlay={<DeprecatedFieldTooltip field="type" docUrl={docUrl} />}
+ <DocumentationTooltip
+ content={<DeprecatedFieldTooltip field="type" />}
+ links={[
+ {
+ href: '/user-guide/issues',
+ label: translate('learn_more'),
+ },
+ ]}
>
<DisabledText className="sw-flex sw-items-center sw-gap-1 sw-cursor-not-allowed">
<IssueTypeIcon fill="iconTypeDisabled" type={issue.type} aria-hidden />
{translate('issue.type', issue.type)}
</DisabledText>
- </Tooltip>
+ </DocumentationTooltip>
);
}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx
index 1feec1505cd..34826ec9fb4 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx
@@ -28,6 +28,7 @@ import { BranchLike } from '../../../types/branch-like';
import { Issue } from '../../../types/types';
import { updateIssue } from '../actions';
import IssueActionsBar from './IssueActionsBar';
+import IssueMetaBar from './IssueMetaBar';
import IssueTitleBar from './IssueTitleBar';
interface Props {
@@ -119,14 +120,17 @@ export default class IssueView extends React.PureComponent<Props> {
togglePopup={this.props.togglePopup}
/>
- <IssueActionsBar
- currentPopup={currentPopup}
- issue={issue}
- onAssign={this.props.onAssign}
- onChange={this.props.onChange}
- togglePopup={this.props.togglePopup}
- showComments
- />
+ <div className="sw-flex sw-gap-2 sw-flex-wrap sw-items-center sw-justify-between">
+ <IssueActionsBar
+ currentPopup={currentPopup}
+ issue={issue}
+ onAssign={this.props.onAssign}
+ onChange={this.props.onChange}
+ togglePopup={this.props.togglePopup}
+ showIssueImpact
+ />
+ <IssueMetaBar issue={issue} />
+ </div>
</div>
</div>
</IssueItem>
diff --git a/server/sonar-web/src/main/js/components/shared/CleanCodeAttributePill.tsx b/server/sonar-web/src/main/js/components/shared/CleanCodeAttributePill.tsx
index 3b83c2caf71..7bb55b92db6 100644
--- a/server/sonar-web/src/main/js/components/shared/CleanCodeAttributePill.tsx
+++ b/server/sonar-web/src/main/js/components/shared/CleanCodeAttributePill.tsx
@@ -18,53 +18,48 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
-import { Link, Pill } from 'design-system';
+import { Pill } from 'design-system';
import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { useDocUrl } from '../../helpers/docs';
import { translate } from '../../helpers/l10n';
-import { CleanCodeAttributeCategory } from '../../types/issues';
-import Tooltip from '../controls/Tooltip';
+import { CleanCodeAttribute, CleanCodeAttributeCategory } from '../../types/issues';
+import DocumentationTooltip from '../common/DocumentationTooltip';
export interface Props {
className?: string;
cleanCodeAttributeCategory: CleanCodeAttributeCategory;
+ cleanCodeAttribute?: CleanCodeAttribute;
}
export function CleanCodeAttributePill(props: Props) {
- const { className, cleanCodeAttributeCategory } = props;
+ const { className, cleanCodeAttributeCategory, cleanCodeAttribute } = props;
- const docUrl = useDocUrl('/user-guide/clean-code');
+ const translationKey = cleanCodeAttribute
+ ? `issue.clean_code_attribute.${cleanCodeAttribute}`
+ : `issue.clean_code_attribute_category.${cleanCodeAttributeCategory}`;
return (
- <Tooltip
- mouseLeaveDelay={0.25}
- overlay={
+ <DocumentationTooltip
+ content={
<>
- <p className="sw-mb-4">
- {translate('issue.clean_code_attribute_category', cleanCodeAttributeCategory, 'title')}
- </p>
- <p>
- {translate('issue.clean_code_attribute_category', cleanCodeAttributeCategory, 'advice')}
- </p>
- <hr className="sw-w-full sw-mx-0 sw-my-4" />
- <FormattedMessage
- defaultMessage={translate('learn_more_x')}
- id="learn_more_x"
- values={{
- link: (
- <Link isExternal to={docUrl}>
- {translate('issue.type.deprecation.documentation')}
- </Link>
- ),
- }}
- />
+ <p className="sw-mb-4">{translate(translationKey, 'title')}</p>
+ <p>{translate(translationKey, 'advice')}</p>
</>
}
+ links={[
+ {
+ href: '/user-guide/clean-code',
+ label: translate('learn_more'),
+ },
+ ]}
>
<Pill variant="neutral" className={classNames('sw-mr-2', className)}>
- {translate('issue.clean_code_attribute_category', cleanCodeAttributeCategory, 'issue')}
+ <span className={classNames({ 'sw-font-semibold': !!cleanCodeAttribute })}>
+ {translate('issue.clean_code_attribute_category', cleanCodeAttributeCategory, 'issue')}
+ </span>
+ {cleanCodeAttribute && (
+ <span> | {translate('issue.clean_code_attribute', cleanCodeAttribute)}</span>
+ )}
</Pill>
- </Tooltip>
+ </DocumentationTooltip>
);
}
diff --git a/server/sonar-web/src/main/js/components/shared/SoftwareImpactPill.tsx b/server/sonar-web/src/main/js/components/shared/SoftwareImpactPill.tsx
index 7f6795a3aa3..1a2d1f31cf5 100644
--- a/server/sonar-web/src/main/js/components/shared/SoftwareImpactPill.tsx
+++ b/server/sonar-web/src/main/js/components/shared/SoftwareImpactPill.tsx
@@ -18,13 +18,12 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
-import { Link, Pill } from 'design-system';
+import { Pill } from 'design-system';
import React from 'react';
import { FormattedMessage } from 'react-intl';
-import { useDocUrl } from '../../helpers/docs';
import { translate } from '../../helpers/l10n';
import { SoftwareImpactSeverity, SoftwareQuality } from '../../types/issues';
-import Tooltip from '../controls/Tooltip';
+import DocumentationTooltip from '../common/DocumentationTooltip';
import SoftwareImpactSeverityIcon from '../icons/SoftwareImpactSeverityIcon';
export interface Props {
@@ -36,8 +35,6 @@ export interface Props {
export default function SoftwareImpactPill(props: Props) {
const { className, severity, quality } = props;
- const docUrl = useDocUrl('/user-guide/clean-code');
-
const variant = {
[SoftwareImpactSeverity.High]: 'danger',
[SoftwareImpactSeverity.Medium]: 'warning',
@@ -45,42 +42,28 @@ export default function SoftwareImpactPill(props: Props) {
}[severity] as 'danger' | 'warning' | 'info';
return (
- <div>
- <Tooltip
- mouseLeaveDelay={0.25}
- overlay={
- <>
- <FormattedMessage
- id="issue.impact.severity.tooltip"
- defaultMessage={translate('issue.impact.severity.tooltip')}
- values={{
- severity: translate('severity', severity).toLowerCase(),
- quality: translate('issue.software_quality', quality).toLowerCase(),
- }}
- />
- <hr className="sw-w-full sw-mx-0 sw-my-4" />
- <FormattedMessage
- defaultMessage={translate('learn_more_x')}
- id="learn_more_x"
- values={{
- link: (
- <Link isExternal to={docUrl}>
- {translate('issue.type.deprecation.documentation')}
- </Link>
- ),
- }}
- />
- </>
- }
- >
- <Pill
- className={classNames('sw-flex sw-gap-1 sw-items-center', className)}
- variant={variant}
- >
- {translate('issue.software_quality', quality)}
- <SoftwareImpactSeverityIcon severity={severity} />
- </Pill>
- </Tooltip>
- </div>
+ <DocumentationTooltip
+ content={
+ <FormattedMessage
+ id="issue.impact.severity.tooltip"
+ defaultMessage={translate('issue.impact.severity.tooltip')}
+ values={{
+ severity: translate('severity', severity).toLowerCase(),
+ quality: translate('issue.software_quality', quality).toLowerCase(),
+ }}
+ />
+ }
+ links={[
+ {
+ href: '/user-guide/clean-code',
+ label: translate('learn_more'),
+ },
+ ]}
+ >
+ <Pill className={classNames('sw-flex sw-gap-1 sw-items-center', className)} variant={variant}>
+ {translate('issue.software_quality', quality)}
+ <SoftwareImpactSeverityIcon severity={severity} />
+ </Pill>
+ </DocumentationTooltip>
);
}