]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21170 Rework issue detail page layout header and styling
author7PH <benjamin.raymond@sonarsource.com>
Mon, 4 Dec 2023 15:53:12 +0000 (16:53 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 7 Dec 2023 20:02:52 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
server/sonar-web/src/main/js/apps/issues/components/IssueHeaderMeta.tsx
server/sonar-web/src/main/js/apps/issues/components/IssueHeaderSide.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx
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
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 98efdce6df33072c48b8766f06b3f9acfcd63736..d2a1062966e5ab0ebd66bae5e64ee00c4c8f5358 100644 (file)
@@ -289,7 +289,7 @@ export default class RuleDetailsMeta extends React.PureComponent<Props> {
           <div className="sw-flex sw-items-center">
             {!!ruleDetails.impacts.length && (
               <div className="sw-flex sw-items-center sw-flex-1">
-                <Note>{translate('issue.software_qualities.label')}</Note>
+                <Note>{translate('coding_rules.software_qualities.label')}</Note>
                 <SoftwareImpactPillList
                   className="sw-ml-1"
                   softwareImpacts={ruleDetails.impacts}
index 650efba2ac3e198d2f75dd1e9bd8c33c9bd4b50d..c713516a7887d4f63220427d474367cd76222bfc 100644 (file)
@@ -31,8 +31,6 @@ import * as React from 'react';
 import { setIssueAssignee } from '../../../api/issues';
 import { updateIssue } from '../../../components/issue/actions';
 import IssueActionsBar from '../../../components/issue/components/IssueActionsBar';
-import { CleanCodeAttributePill } from '../../../components/shared/CleanCodeAttributePill';
-import SoftwareImpactPillList from '../../../components/shared/SoftwareImpactPillList';
 import { WorkspaceContext } from '../../../components/workspace/context';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
@@ -44,6 +42,7 @@ import { BranchLike } from '../../../types/branch-like';
 import { IssueActions, IssueType } from '../../../types/issues';
 import { Issue, RuleDetails } from '../../../types/types';
 import IssueHeaderMeta from './IssueHeaderMeta';
+import IssueHeaderSide from './IssueHeaderSide';
 import IssueNewStatusAndTransitionGuide from './IssueNewStatusAndTransitionGuide';
 
 interface Props {
@@ -166,53 +165,45 @@ export default class IssueHeader extends React.PureComponent<Props, State> {
 
     return (
       <header className="sw-flex sw-mb-6">
-        <div className="sw-mr-8 sw-flex-1">
-          <CleanCodeAttributePill
-            cleanCodeAttributeCategory={issue.cleanCodeAttributeCategory}
-            cleanCodeAttribute={issue.cleanCodeAttribute}
-          />
+        <div className="sw-mr-8 sw-flex-1 sw-flex sw-flex-col sw-gap-4">
+          <div className="sw-flex sw-flex-col sw-gap-2">
+            <div className="sw-flex sw-items-center">
+              <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-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>
-            <div data-guiding-id="issue-2">
-              <SoftwareImpactPillList className="sw-ml-1" softwareImpacts={issue.impacts} />
+            <div className="sw-flex sw-items-center sw-justify-between">
+              {this.renderRuleDescription()}
             </div>
           </div>
-          <BasicSeparator className="sw-my-3" />
+
+          <IssueHeaderMeta issue={issue} />
+
+          <BasicSeparator />
+
           <IssueActionsBar
             currentPopup={issuePopupName}
             issue={issue}
             onAssign={this.handleAssignement}
             onChange={this.props.onIssueChange}
             togglePopup={this.handleIssuePopupToggle}
+            canSetTags={canSetTags}
+            showTags
             showSonarLintBadge
           />
         </div>
-        <IssueHeaderMeta
-          issue={issue}
-          canSetTags={canSetTags}
-          onIssueChange={this.props.onIssueChange}
-          tagsPopupOpen={issuePopupName === 'edit-tags' && canSetTags}
-          togglePopup={this.handleIssuePopupToggle}
-        />
+        <IssueHeaderSide issue={issue} />
         <IssueNewStatusAndTransitionGuide
           run
           issues={[issue]}
index bc8d98d5d16ac5bbbd4520877010749267bdc12a..4feb23909d20688ef1021416f7d13b7d20bfde97 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 styled from '@emotion/styled';
-import { BasicSeparator, LightLabel, themeBorder, Tooltip } from 'design-system';
+import { LightLabel, Note, SeparatorCircleIcon, Tooltip } from 'design-system';
 import React from 'react';
 import DateFromNow from '../../../components/intl/DateFromNow';
-import IssueTags from '../../../components/issue/components/IssueTags';
+import IssueSeverity from '../../../components/issue/components/IssueSeverity';
+import IssueType from '../../../components/issue/components/IssueType';
 import { translate } 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" />;
-
+export default function IssueHeaderMeta({ issue }: Readonly<Props>) {
   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}
-
+    <Note className="sw-flex sw-items-center sw-gap-2 sw-text-xs">
       {!!issue.codeVariants?.length && (
         <>
-          <HotspotHeaderInfo title={translate('issue.code_variants')} className="sw-truncate">
-            <Tooltip overlay={issue.codeVariants.join(', ')}>
-              <span>{issue.codeVariants.join(', ')}</span>
+          <div className="sw-flex sw-gap-1">
+            <span>{translate('issue.code_variants')}</span>
+            <Tooltip overlay={issue.codeVariants?.join(', ')}>
+              <span className="sw-font-semibold">
+                <LightLabel>{issue.codeVariants?.join(', ')}</LightLabel>
+              </span>
             </Tooltip>
-          </HotspotHeaderInfo>
-          {separator}
+          </div>
+          <SeparatorCircleIcon />
         </>
       )}
 
-      {issue.effort && (
+      {typeof issue.line === 'number' && (
         <>
-          <HotspotHeaderInfo title={translate('issue.effort')}>{issue.effort}</HotspotHeaderInfo>
-          {separator}
+          <div className="sw-flex sw-gap-1">
+            <span>{translate('issue.line_affected')}</span>
+            <span className="sw-font-semibold">L{issue.line}</span>
+          </div>
+          <SeparatorCircleIcon />
         </>
       )}
 
-      <HotspotHeaderInfo title={translate('issue.introduced')}>
-        <DateFromNow date={issue.creationDate} />
-      </HotspotHeaderInfo>
-    </StyledSection>
-  );
-}
+      {issue.effort && (
+        <>
+          <div className="sw-flex sw-gap-1">
+            <span>{translate('issue.effort')}</span>
+            <span className="sw-font-semibold">{issue.effort}</span>
+          </div>
+          <SeparatorCircleIcon />
+        </>
+      )}
 
-interface IssueHeaderMetaItemProps {
-  children: React.ReactNode;
-  title: string;
-  className?: string;
-}
+      <div className="sw-flex sw-gap-1">
+        <span>{translate('issue.introduced')}</span>
+        <span className="sw-font-semibold">
+          <LightLabel>
+            <DateFromNow date={issue.creationDate} />
+          </LightLabel>
+        </span>
+      </div>
+      <SeparatorCircleIcon />
 
-function HotspotHeaderInfo({ children, title, className }: IssueHeaderMetaItemProps) {
-  return (
-    <div className={className}>
-      <LightLabel as="div" className="sw-body-sm-highlight">
-        {title}
-      </LightLabel>
-      {children}
-    </div>
+      <IssueType issue={issue} />
+      <SeparatorCircleIcon data-guiding-id="issue-4" />
+      <IssueSeverity issue={issue} />
+    </Note>
   );
 }
-
-const StyledSection = styled.div`
-  border-left: ${themeBorder('default', 'pageBlockBorder')};
-`;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueHeaderSide.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueHeaderSide.tsx
new file mode 100644 (file)
index 0000000..1617fd0
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * 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 { LightLabel, themeBorder } from 'design-system';
+import React from 'react';
+import { CleanCodeAttributePill } from '../../../components/shared/CleanCodeAttributePill';
+import SoftwareImpactPillList from '../../../components/shared/SoftwareImpactPillList';
+import { translate } from '../../../helpers/l10n';
+import { Issue } from '../../../types/types';
+
+interface Props {
+  issue: Issue;
+}
+
+export default function IssueHeaderSide({ issue }: Readonly<Props>) {
+  return (
+    <StyledSection className="sw-flex sw-flex-col sw-pl-4 sw-w-[200px]">
+      <IssueHeaderInfo title={translate('issue.cct_attribute.label')} className="sw-mb-6">
+        <CleanCodeAttributePill
+          cleanCodeAttributeCategory={issue.cleanCodeAttributeCategory}
+          cleanCodeAttribute={issue.cleanCodeAttribute}
+        />
+      </IssueHeaderInfo>
+
+      <IssueHeaderInfo
+        data-guiding-id="issue-2"
+        title={translate('issue.software_qualities.label')}
+      >
+        <SoftwareImpactPillList className="sw-flex-wrap" softwareImpacts={issue.impacts} />
+      </IssueHeaderInfo>
+    </StyledSection>
+  );
+}
+
+interface IssueHeaderMetaItemProps extends React.HTMLAttributes<HTMLDivElement> {
+  children: React.ReactNode;
+  title: string;
+  className?: string;
+}
+
+function IssueHeaderInfo({
+  children,
+  title,
+  className,
+  ...props
+}: Readonly<IssueHeaderMetaItemProps>) {
+  return (
+    <div className={className} {...props}>
+      <LightLabel as="div" className="sw-text-xs sw-font-semibold sw-mb-1">
+        {title}
+      </LightLabel>
+      {children}
+    </div>
+  );
+}
+
+const StyledSection = styled.div`
+  border-left: ${themeBorder('default', 'pageBlockBorder')};
+`;
index 28509e4fd7921f729f69eb18622bbc307c34c4e8..e03b4723437ee8be72331d3a6ed28d6bb5d07347 100644 (file)
@@ -22,13 +22,11 @@ import { HighlightRing } from 'design-system';
 import * as React from 'react';
 import { IssueActions } from '../../../types/issues';
 import { Issue } from '../../../types/types';
-import SoftwareImpactPillList from '../../shared/SoftwareImpactPillList';
 import IssueAssign from './IssueAssign';
 import { SonarLintBadge } from './IssueBadges';
 import IssueCommentAction from './IssueCommentAction';
-import IssueSeverity from './IssueSeverity';
+import IssueTags from './IssueTags';
 import IssueTransition from './IssueTransition';
-import IssueType from './IssueType';
 
 interface Props {
   issue: Issue;
@@ -36,19 +34,21 @@ interface Props {
   onAssign: (login: string) => void;
   onChange: (issue: Issue) => void;
   togglePopup: (popup: string, show?: boolean) => void;
-  showIssueImpact?: boolean;
   showSonarLintBadge?: boolean;
+  showTags?: boolean;
+  canSetTags?: boolean;
 }
 
-export default function IssueActionsBar(props: Props) {
+export default function IssueActionsBar(props: Readonly<Props>) {
   const {
     issue,
     currentPopup,
     onAssign,
     onChange,
     togglePopup,
-    showIssueImpact,
     showSonarLintBadge,
+    showTags,
+    canSetTags,
   } = props;
 
   const [commentPlaceholder, setCommentPlaceholder] = React.useState('');
@@ -61,6 +61,7 @@ export default function IssueActionsBar(props: Props) {
 
   const canAssign = issue.actions.includes(IssueActions.Assign);
   const canComment = issue.actions.includes(IssueActions.Comment);
+  const tagsPopupOpen = currentPopup === 'edit-tags' && canSetTags;
 
   return (
     <div className="sw-flex sw-gap-3">
@@ -88,9 +89,16 @@ export default function IssueActionsBar(props: Props) {
           />
         </li>
 
-        {showIssueImpact && (
-          <li data-guiding-id="issue-2">
-            <SoftwareImpactPillList className="sw-gap-3" softwareImpacts={issue.impacts} />
+        {showTags && (
+          <li>
+            <IssueTags
+              canSetTags={canSetTags}
+              issue={issue}
+              onChange={props.onChange}
+              open={tagsPopupOpen}
+              togglePopup={props.togglePopup}
+              tagsToDisplay={1}
+            />
           </li>
         )}
 
@@ -100,15 +108,6 @@ export default function IssueActionsBar(props: Props) {
           </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>
-
-        <li>
-          <IssueSeverity issue={issue} />
-        </li>
-      </ul>
 
       {canComment && (
         <IssueCommentAction
index d60550522a2a5a0c7cb4f58580d762803c9405de..1216d20d4a0a43417e034649d0441de8d5378bd6 100644 (file)
@@ -31,7 +31,7 @@ interface Props {
   issue: Pick<Issue, 'severity'>;
 }
 
-export default function IssueSeverity({ issue }: Props) {
+export default function IssueSeverity({ issue }: Readonly<Props>) {
   return (
     <DocumentationTooltip
       content={<DeprecatedFieldTooltip field="severity" />}
@@ -42,7 +42,7 @@ export default function IssueSeverity({ issue }: Props) {
         },
       ]}
     >
-      <TextSubdued className="sw-flex sw-items-center sw-gap-1">
+      <TextSubdued className="sw-flex sw-items-center sw-gap-1/2">
         <IssueSeverityIcon
           fill="iconSeverityDisabled"
           severity={issue.severity as IssueSeverityType}
index 987a663f9e8ab4f15e8437ee29936492f0f96b0f..fd5c14f1b8bcdf73091ddc617f8e52e9820af5b6 100644 (file)
@@ -30,7 +30,7 @@ import { updateIssue } from '../actions';
 import IssueTagsPopup from '../popups/IssueTagsPopup';
 
 interface Props extends ComponentContextShape {
-  canSetTags: boolean;
+  canSetTags?: boolean;
   issue: Pick<Issue, 'key' | 'tags'>;
   onChange: (issue: Issue) => void;
   open?: boolean;
index 789ea2e61aca3f6651b90d55140bf0fdc476981d..360225bd25343a90e4d58ca7a96ca67e8a710efa 100644 (file)
@@ -30,7 +30,7 @@ interface Props {
   issue: Pick<Issue, 'type'>;
 }
 
-export default function IssueType({ issue }: Props) {
+export default function IssueType({ issue }: Readonly<Props>) {
   return (
     <DocumentationTooltip
       content={<DeprecatedFieldTooltip field="type" />}
@@ -41,7 +41,7 @@ export default function IssueType({ issue }: Props) {
         },
       ]}
     >
-      <TextSubdued className="sw-flex sw-items-center sw-gap-1">
+      <TextSubdued className="sw-flex sw-items-center sw-gap-1/2">
         <IssueTypeIcon fill="iconTypeDisabled" type={issue.type} aria-hidden />
         {translate('issue.type', issue.type)}
       </TextSubdued>
index e3329901c952ac9baa8f6ef415b0a6b16291cc14..7d7f6364ca4ed529890d2145f866061611ffc7ef 100644 (file)
@@ -122,7 +122,6 @@ export default class IssueView extends React.PureComponent<Props> {
                 onAssign={this.props.onAssign}
                 onChange={this.props.onChange}
                 togglePopup={this.props.togglePopup}
-                showIssueImpact
               />
               <IssueMetaBar issue={issue} />
             </div>
index 4ec36c40a484ce668b166d318265b1a9b8920669..0cf4fc63f0b6c0781ab90cd52226b42562cbd748 100644 (file)
@@ -989,7 +989,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.label=Software qualities impacted:
+issue.cct_attribute.label=Clean code attribute
+issue.software_qualities.label=Software qualities impacted
 issue.impact.severity.tooltip=This issue has a {severity} impact on the {quality} of your software.
 
 issue.clean_code_attribute_category.CONSISTENT=Consistency
@@ -1079,11 +1080,11 @@ issue.resolution.REMOVED.description=Either the rule or the resource was changed
 issue.unresolved.description=Unresolved issues have not been addressed in any way.
 
 issue.action.permalink=Get permalink
-issue.line_affected=Line affected
-issue.introduced=Introduced
-issue.code_variants=Code variant
+issue.line_affected=Line affected:
+issue.introduced=Introduced:
+issue.code_variants=Code variant:
 issue.rule_status=Rule status
-issue.effort=Effort
+issue.effort=Effort:
 issue.x_effort={0} effort
 issue.ncloc_x.short=L{0}
 issue.1_code_variant=1 variant
@@ -2361,6 +2362,7 @@ coding_rules.rule_template.title=This rule can be used as a template to create c
 coding_rules._rules=rules
 coding_rules.show_template=Show Template
 coding_rules.skip_to_filters=Skip to rules filters
+coding_rules.software_qualities.label=Software qualities impacted:
 coding_rules.to_select_rules=Select rules
 coding_rules.to_navigate=Navigate to rule
 coding_rules.type.deprecation.title=Types of detection rules are deprecated.