diff options
author | stanislavh <stanislav.honcharov@sonarsource.com> | 2023-08-03 11:36:55 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-08-18 20:02:47 +0000 |
commit | c27adc272a98942fa46e11a9a7732744b7940f22 (patch) | |
tree | bafebe188031db00f715f35bf84f8ee808901085 /server | |
parent | f3028f632704711d23bd27ce35bb04bd56f0b984 (diff) | |
download | sonarqube-c27adc272a98942fa46e11a9a7732744b7940f22.tar.gz sonarqube-c27adc272a98942fa46e11a9a7732744b7940f22.zip |
SONAR-20023 Modify issue details to reflect CCT
Diffstat (limited to 'server')
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> ); } |