]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20023 Modify issue details to reflect CCT
authorstanislavh <stanislav.honcharov@sonarsource.com>
Thu, 3 Aug 2023 09:36:55 +0000 (11:36 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 18 Aug 2023 20:02:47 +0000 (20:02 +0000)
17 files changed:
server/sonar-web/design-system/src/components/Pill.tsx
server/sonar-web/design-system/src/theme/light.ts
server/sonar-web/src/main/js/apps/issues/__tests__/IssueHeader-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
server/sonar-web/src/main/js/apps/issues/components/IssueHeaderMeta.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx
server/sonar-web/src/main/js/components/issue/components/DeprecatedFieldTooltip.tsx
server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx
server/sonar-web/src/main/js/components/issue/components/IssueBadges.tsx
server/sonar-web/src/main/js/components/issue/components/IssueMetaBar.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/components/IssueSeverity.tsx
server/sonar-web/src/main/js/components/issue/components/IssueTags.tsx
server/sonar-web/src/main/js/components/issue/components/IssueType.tsx
server/sonar-web/src/main/js/components/issue/components/IssueView.tsx
server/sonar-web/src/main/js/components/shared/CleanCodeAttributePill.tsx
server/sonar-web/src/main/js/components/shared/SoftwareImpactPill.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index bc28d26fd63b788721071074d64b119742b0a14b..6f576d55226c70bf3c9dfd3d3f8a68f9a39bceec 100644 (file)
@@ -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`};
index 8be0dc341c6b0731f09e35da728de4cad1c7ccf6..9eb9da4fc5f9658bbbf48ba31279a066202192c6 100644 (file)
@@ -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 (file)
index 0000000..54fdfeb
--- /dev/null
@@ -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>
+  );
+}
index 532873de5d940ba77cf58c8df686c2b7adce63c3..eaa850eb8536d6f7950bfd8f75ed6d323418a6dd 100644 (file)
@@ -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 (file)
index 0000000..72b2721
--- /dev/null
@@ -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')};
+`;
index d69e35f1b67b255e1f8f9c4ab311e3cb779cafd3..f5eb7e9df5aabfc1a8158fc19c780f95c83a819c 100644 (file)
@@ -86,7 +86,7 @@ export default function DocumentationTooltip(props: DocumentationTooltipProps) {
             </div>
           )}
 
-          {content && <p>{content}</p>}
+          {content && <div>{content}</div>}
 
           {links && (
             <>
index c705f69b6d3037db87b5cd14b18f9dff246a2b98..c251de9526cf23a2f2538f5fe466f7131d971b3a 100644 (file)
  * 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>
-          ),
-        }}
-      />
     </>
   );
 }
index 2b3e6a300a491e6f4d248517d47d5697b9fc964d..d92a6b0413190c8a755fdf45a2e841ec99573fd8 100644 (file)
  * 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')};
-`;
index 6fc6ff465d18446d87ad306178db43dc13ee1f70..e62255154bc74bf23a2f3bb15793c580661d320c 100644 (file)
@@ -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 (file)
index 0000000..14c5223
--- /dev/null
@@ -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')};
+`;
index 43a7f69021e31ff8fc79c77fd1a941ea2c30019e..c1b0755e2e48fda8b6c7c98286c53f76fa40932c 100644 (file)
  * 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>
   );
 }
index 88d7f6fbb1137d5153473b2d562c39bd96a66c38..51d01c81c92678cf36cf849e8e6e9144c805facb 100644 (file)
@@ -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}
       />
     );
index 12a1cd4cdd52b0667c8a982599e84c4a6853da84..37406bc8bde562787c12f17bd1a8a3e84b3e0dce 100644 (file)
  * 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>
   );
 }
index 1feec1505cd2b77306cffb59d8b6a94b82ae41b7..34826ec9fb4b9bb378dc4afc364cb5e8e755e101 100644 (file)
@@ -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>
index 3b83c2caf71a10e0f9d21574b5cc180af427e924..7bb55b92db6175e2b24c3843e2204cd249af5524 100644 (file)
  * 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>
   );
 }
index 7f6795a3aa39923c112a61e5c6851c0ebcb8b082..1a2d1f31cf57f88438ba6669ec5409694c621998 100644 (file)
  * 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>
   );
 }
index 7069f1ffbe96ecb44f4ccf760ba8843f80958a7b..0e7e5ffa12006b4bd42413a324cefca9039534b6 100644 (file)
@@ -974,7 +974,8 @@ issue.severity.deprecation.filter_by=You can now filter issues by:
 issue.severity.deprecation.documentation=Documentation
 issue.severity.new=The new severities
 
-issue.software_qualities=Software qualities
+issue.software_quality=Software Quality
+issue.software_qualities.label=Software qualities impacted:
 issue.software_quality.SECURITY=Security
 issue.software_quality.RELIABILITY=Reliability
 issue.software_quality.MAINTAINABILITY=Maintainability
@@ -998,21 +999,49 @@ issue.clean_code_attribute_category.RESPONSIBLE.title=This is a responsibility i
 issue.clean_code_attribute_category.RESPONSIBLE.advice=To be responsible, the code must take into account its ethical obligations on data and potential impact of societal norms.
 issue.clean_code_attribute_category.RESPONSIBLE.issue=Responsibility issue
 
-issue.clean_code_attributes=Clean Code attributes
+issue.clean_code_attribute=Clean Code Attribute
 issue.clean_code_attribute.CLEAR=Clear
+issue.clean_code_attribute.CLEAR.title=This is an intentionality issue, the code is not clear enough.
+issue.clean_code_attribute.CLEAR.advice=To be clear, the code needs to be self-explanatory and transparently communicate its functionality.
 issue.clean_code_attribute.COMPLETE=Complete
+issue.clean_code_attribute.COMPLETE.title=This is an intentionality issue, the code is not complete enough.
+issue.clean_code_attribute.COMPLETE.advice=To be complete, the code must be functional and achieve its implied goals through comprehensive, adequate, and thorough code constructs.
 issue.clean_code_attribute.CONVENTIONAL=Conventional
+issue.clean_code_attribute.CONVENTIONAL.title=This is a consistency issue, the code is not conventional enough.
+issue.clean_code_attribute.CONVENTIONAL.advice=To be conventional, the code needs to tasks with expected instructions, and adhere to a single choice across all instances when faced with equally good options.
 issue.clean_code_attribute.DISTINCT=Distinct
+issue.clean_code_attribute.DISTINCT.title=This is an adaptability issue, the code is not distinct enough.
+issue.clean_code_attribute.DISTINCT.advice=To be distinct, the code needs to have procedures and data that are unique and distinctive, without undue duplication.
 issue.clean_code_attribute.EFFICIENT=Efficient
+issue.clean_code_attribute.EFFICIENT.title=This is an intentionality issue, the code is not efficient enough.
+issue.clean_code_attribute.EFFICIENT.advice=To be efficient, the code needs to use resources without needless waste, and prioritize economical options when available.
 issue.clean_code_attribute.FOCUSED=Focused
+issue.clean_code_attribute.FOCUSED.title=This is an adaptability issue, the code is not focused enough.
+issue.clean_code_attribute.FOCUSED.advice=To be focused, the code needs to have a single, narrow, and specific scope. Each of its units should have only one concise purpose.
 issue.clean_code_attribute.FORMATTED=Formatted
+issue.clean_code_attribute.FORMATTED.title=This is a consistency issue, the code is not formatted enough.
+issue.clean_code_attribute.FORMATTED.advice=To be formatted, the code needs to be presented in a systematic and regular way, with the same non-semantic choices used over all the codebase.
 issue.clean_code_attribute.IDENTIFIABLE=Identifiable
+issue.clean_code_attribute.IDENTIFIABLE.title=This is a consistency issue, the code is not identifiable enough.
+issue.clean_code_attribute.IDENTIFIABLE.advice=To be identifiable, the code needs to include names and identifiers with a regular structure based on language conventions.
 issue.clean_code_attribute.LAWFUL=Lawful
+issue.clean_code_attribute.LAWFUL.title=This is a responsibility issue, the code is not lawful enough.
+issue.clean_code_attribute.LAWFUL.advice=To be lawful, the code needs to respect licensing and copyright regulation.
 issue.clean_code_attribute.LOGICAL=Logical
+issue.clean_code_attribute.LOGICAL.title=This is an intentionality issue, the code is not logical enough.
+issue.clean_code_attribute.LOGICAL.advice=To be logical, the code needs to have well-formed and sound instructions that work together, and be free of explicit errors.
 issue.clean_code_attribute.MODULAR=Modular
+issue.clean_code_attribute.MODULAR.title=This is an adaptability issue, the code is not modular enough.
+issue.clean_code_attribute.MODULAR.advice=To be modular, the code needs to be organized and distributed to emphasize the separation between its parts.
 issue.clean_code_attribute.RESPECTFUL=Respectful
+issue.clean_code_attribute.RESPECTFUL.title=This is a responsibility issue, the code is not respectful enough.
+issue.clean_code_attribute.RESPECTFUL.advice=To be respectful, the code needs to refrain from using discriminatory and offensive language.
 issue.clean_code_attribute.TESTED=Tested
+issue.clean_code_attribute.TESTED.title=This is an adaptability issue, the code is not tested enough.
+issue.clean_code_attribute.TESTED.advice=To be tested, the code needs to have automated functional tests that provide confidence in the functionality.
 issue.clean_code_attribute.TRUSTWORTHY=Trustworthy
+issue.clean_code_attribute.TRUSTWORTHY.title=This is a responsibility issue, the code is not trustworthy enough.
+issue.clean_code_attribute.TRUSTWORTHY.advice=To be trustworthy, the code needs to abstain from revealing or hard-coding private information.
 
 issue.status.REOPENED=Reopened
 issue.status.RESOLVED=Resolved
@@ -1039,8 +1068,13 @@ issue.resolution.badge.DEPRECATED=Rule deprecated
 issue.unresolved.description=Unresolved issues have not been addressed in any way.
 
 issue.action.permalink=Get permalink
-issue.effort=Effort:
+issue.line_affected=Line affected
+issue.introduced=Introduced
+issue.code_variants=Code variant
+issue.rule_status=Rule status
+issue.effort=Effort
 issue.x_effort={0} effort
+issue.ncloc_x.short=L{0}
 issue.1_code_variant=1 variant
 issue.x_code_variants={0} variants
 issue.filter_similar_issues=Filter Similar Issues