From 58af6f56c90ead32581539b109e767186aea9e83 Mon Sep 17 00:00:00 2001 From: 7PH Date: Wed, 6 Dec 2023 12:20:26 +0100 Subject: [PATCH] SONAR-21172 Rework rule detail layout and styling --- .../coding-rules/__tests__/CodingRules-it.ts | 4 +- .../coding-rules/components/RuleDetails.tsx | 4 +- .../components/RuleDetailsHeader.tsx | 80 ++++ .../components/RuleDetailsHeaderActions.tsx | 117 ++++++ .../components/RuleDetailsHeaderMeta.tsx | 170 +++++++++ .../components/RuleDetailsHeaderSide.tsx | 75 ++++ .../components/RuleDetailsMeta.tsx | 348 ------------------ .../coding-rules/components/RuleListItem.tsx | 5 +- .../resources/org/sonar/l10n/core.properties | 17 +- 9 files changed, 461 insertions(+), 359 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeader.tsx create mode 100644 server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeaderActions.tsx create mode 100644 server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeaderMeta.tsx create mode 100644 server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeaderSide.tsx delete mode 100644 server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts index a6497ce29e7..d0c0d7726ae 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts @@ -21,7 +21,7 @@ import { fireEvent, screen, within } from '@testing-library/react'; import selectEvent from 'react-select-event'; import CodingRulesServiceMock, { RULE_TAGS_MOCK } from '../../../api/mocks/CodingRulesServiceMock'; import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock'; -import { QP_2 } from '../../../api/mocks/data/ids'; +import { QP_2, RULE_1 } from '../../../api/mocks/data/ids'; import { CLEAN_CODE_CATEGORIES, SOFTWARE_QUALITIES } from '../../../helpers/constants'; import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks'; import { renderAppRoutes } from '../../../helpers/testReactTestingUtils'; @@ -379,7 +379,7 @@ describe('Rule app details', () => { describe('rendering', () => { it('shows rule with default description section and params', async () => { const { ui } = getPageObjects(); - renderCodingRulesApp(undefined, 'coding_rules?open=rule1'); + renderCodingRulesApp(undefined, 'coding_rules?open=' + RULE_1); await ui.detailsloaded(); expect(ui.ruleTitle('Awsome java rule').get()).toBeInTheDocument(); expect( diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx index 0dc13d6d8f8..291d06f4516 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx @@ -43,8 +43,8 @@ import { Activation } from '../query'; import CustomRuleButton from './CustomRuleButton'; import RuleDetailsCustomRules from './RuleDetailsCustomRules'; import RuleDetailsDescription from './RuleDetailsDescription'; +import RuleDetailsHeader from './RuleDetailsHeader'; import RuleDetailsIssues from './RuleDetailsIssues'; -import RuleDetailsMeta from './RuleDetailsMeta'; import RuleDetailsParameters from './RuleDetailsParameters'; import RuleDetailsProfiles from './RuleDetailsProfiles'; @@ -108,7 +108,7 @@ export default function RuleDetails(props: Readonly) { {ruleDetails && ( <> - void; + referencedRepositories: Dict<{ key: string; language: string; name: string }>; + ruleDetails: RuleDetails; +} + +export default function RuleDetailsMeta(props: Readonly) { + const { ruleDetails, onTagsChange, referencedRepositories, canWrite } = props; + const ruleUrl = getRuleUrl(ruleDetails.key); + + const hasTypeData = !ruleDetails.isExternal || ruleDetails.type !== 'UNKNOWN'; + return ( +
+
+ + + + + +
+ {hasTypeData && ( + + )} + + +
+
+ + +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeaderActions.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeaderActions.tsx new file mode 100644 index 00000000000..8115c5fa871 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeaderActions.tsx @@ -0,0 +1,117 @@ +/* + * 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 { Note, SeparatorCircleIcon, TextSubdued } from 'design-system'; +import * as React from 'react'; +import DocumentationTooltip from '../../../components/common/DocumentationTooltip'; +import IssueSeverityIcon from '../../../components/icon-mappers/IssueSeverityIcon'; +import IssueTypeIcon from '../../../components/icon-mappers/IssueTypeIcon'; +import TagsList from '../../../components/tags/TagsList'; +import { translate } from '../../../helpers/l10n'; +import { IssueSeverity } from '../../../types/issues'; +import { Dict, RuleDetails } from '../../../types/types'; +import RuleDetailsTagsPopup from './RuleDetailsTagsPopup'; + +interface Props { + canWrite: boolean | undefined; + onTagsChange: (tags: string[]) => void; + referencedRepositories: Dict<{ key: string; language: string; name: string }>; + ruleDetails: RuleDetails; +} + +export default function RuleDetailsHeaderActions(props: Readonly) { + const { canWrite, ruleDetails, onTagsChange } = props; + const { sysTags = [], tags = [] } = ruleDetails; + const allTags = [...sysTags, ...tags]; + const TAGS_TO_DISPLAY = 1; + + return ( + + {/* Type */} + +

{translate('coding_rules.type.deprecation.title')}

+

{translate('coding_rules.type.deprecation.filter_by')}

+ + } + links={[ + { + href: '/user-guide/rules/overview', + label: translate('learn_more'), + }, + ]} + > + + + {translate('issue.type', ruleDetails.type)} + +
+ + + {/* Severity */} + +

{translate('coding_rules.severity.deprecation.title')}

+

{translate('coding_rules.severity.deprecation.filter_by')}

+ + } + links={[ + { + href: '/user-guide/rules/overview', + label: translate('learn_more'), + }, + ]} + > + + + {translate('severity', ruleDetails.severity)} + +
+ + + {/* Tags */} +
+ 0 ? allTags : [translate('coding_rules.no_tags')]} + overlay={ + canWrite ? ( + + ) : undefined + } + /> +
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeaderMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeaderMeta.tsx new file mode 100644 index 00000000000..b292f6925c8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeaderMeta.tsx @@ -0,0 +1,170 @@ +/* + * 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 { Badge, HelperHintIcon, Link, Note, SeparatorCircleIcon } from 'design-system'; +import React from 'react'; +import HelpTooltip from '../../../components/controls/HelpTooltip'; +import Tooltip from '../../../components/controls/Tooltip'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { getRuleUrl } from '../../../helpers/urls'; +import { Dict, RuleDetails } from '../../../types/types'; + +const EXTERNAL_RULE_REPO_PREFIX = 'external_'; + +interface Props { + ruleDetails: RuleDetails; + referencedRepositories: Dict<{ key: string; language: string; name: string }>; +} + +export default function RuleDetailsHeaderMeta(props: Readonly) { + const { referencedRepositories, ruleDetails } = props; + const repository = referencedRepositories[ruleDetails.repo]; + const externalEngine = ruleDetails.repo.replace(new RegExp(`^${EXTERNAL_RULE_REPO_PREFIX}`), ''); + + return ( + + {/* Template */} + {!ruleDetails.isExternal && ruleDetails.isTemplate && ( + <> +
  • + + + {translate('coding_rules.rule_template')} + + +
  • + + + )} + + {/* Parent template */} + {!ruleDetails.isExternal && ruleDetails.templateKey && ( + <> +
  • + + {translate('coding_rules.custom_rule')} + {' ('} + + {translate('coding_rules.show_template')} + + {') '} + + + + +
  • + + + )} + + {/* Key */} +
  • + {translate('coding_rules.rule_id')} + {ruleDetails.key} +
  • + + {/* Scope */} + {ruleDetails.scope && ( + <> + +
  • + {translate('coding_rules.analysis_scope')} + + {translate('coding_rules.scope', ruleDetails.scope)} + +
  • + + )} + + {/* Repository */} + {repository && ( + <> + +
  • + {translate('coding_rules.repository')} + + {repository.name} ({ruleDetails.langName}) + +
  • + + )} + + {/* Engine */} + {ruleDetails.isExternal && ruleDetails.repo && externalEngine && ( + <> + +
  • + +
    + {translate('coding_rules.external_rule.engine')} + + {externalEngine} + +
    +
    +
  • + + )} + + {/* Status */} + {!ruleDetails.isExternal && ruleDetails.status !== 'READY' && ( + <> + +
  • + + + {translate('rules.status', ruleDetails.status)} + + +
  • + + )} + + {/* Effort */} + {ruleDetails.remFnType && ruleDetails.remFnBaseEffort && ( + <> + +
  • + {translate('coding_rules.remediation_effort')} + + {ruleDetails.remFnBaseEffort !== undefined && ` ${ruleDetails.remFnBaseEffort}`} + +
  • + + )} +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeaderSide.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeaderSide.tsx new file mode 100644 index 00000000000..1c282ded7cc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeaderSide.tsx @@ -0,0 +1,75 @@ +/* + * 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 { RuleDetails } from '../../../types/types'; + +interface Props { + ruleDetails: RuleDetails; +} + +export default function RuleDetailsHeaderSide({ ruleDetails }: Readonly) { + return ( + + {ruleDetails.cleanCodeAttributeCategory && ruleDetails.cleanCodeAttribute && ( + + + + )} + + + + + + ); +} + +interface RuleHeaderMetaItemProps { + children: React.ReactNode; + title: string; + className?: string; +} + +function RuleHeaderInfo({ children, title, ...props }: Readonly) { + return ( +
    + + {title} + + {children} +
    + ); +} + +const StyledSection = styled.div` + border-left: ${themeBorder('default', 'pageBlockBorder')}; +`; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx deleted file mode 100644 index d2a1062966e..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx +++ /dev/null @@ -1,348 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import styled from '@emotion/styled'; -import { - Badge, - BasicSeparator, - ClipboardIconButton, - HelperHintIcon, - IssueMessageHighlighting, - LightLabel, - Link, - LinkIcon, - Note, - PageContentFontWrapper, - TextSubdued, - themeBorder, -} from 'design-system'; -import * as React from 'react'; -import DocumentationTooltip from '../../../components/common/DocumentationTooltip'; -import HelpTooltip from '../../../components/controls/HelpTooltip'; -import Tooltip from '../../../components/controls/Tooltip'; -import IssueSeverityIcon from '../../../components/icon-mappers/IssueSeverityIcon'; -import IssueTypeIcon from '../../../components/icon-mappers/IssueTypeIcon'; -import { CleanCodeAttributePill } from '../../../components/shared/CleanCodeAttributePill'; -import SoftwareImpactPillList from '../../../components/shared/SoftwareImpactPillList'; -import TagsList from '../../../components/tags/TagsList'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { getPathUrlAsString, getRuleUrl } from '../../../helpers/urls'; -import { IssueSeverity as IssueSeverityType } from '../../../types/issues'; -import { Dict, RuleDetails } from '../../../types/types'; -import RuleDetailsTagsPopup from './RuleDetailsTagsPopup'; - -interface Props { - canWrite: boolean | undefined; - onTagsChange: (tags: string[]) => void; - referencedRepositories: Dict<{ key: string; language: string; name: string }>; - ruleDetails: RuleDetails; -} - -const EXTERNAL_RULE_REPO_PREFIX = 'external_'; - -export default class RuleDetailsMeta extends React.PureComponent { - renderType = () => { - const { ruleDetails } = this.props; - return ( - - -

    {translate('coding_rules.type.deprecation.title')}

    -

    {translate('coding_rules.type.deprecation.filter_by')}

    - - } - links={[ - { - href: '/user-guide/rules/overview', - label: translate('learn_more'), - }, - ]} - > - - - {translate('issue.type', ruleDetails.type)} - -
    -
    - ); - }; - - renderSeverity = () => ( - - -

    {translate('coding_rules.severity.deprecation.title')}

    -

    {translate('coding_rules.severity.deprecation.filter_by')}

    - - } - links={[ - { - href: '/user-guide/rules/overview', - label: translate('learn_more'), - }, - ]} - > - - - {translate('severity', this.props.ruleDetails.severity)} - -
    -
    - ); - - renderStatus = () => { - const { ruleDetails } = this.props; - if (ruleDetails.status === 'READY') { - return null; - } - return ( - - - {translate('rules.status', ruleDetails.status)} - - - ); - }; - - renderTags = () => { - const { canWrite, ruleDetails } = this.props; - const { sysTags = [], tags = [] } = ruleDetails; - const allTags = [...sysTags, ...tags]; - const TAGS_TO_DISPLAY = 1; - - return ( -
    - 0 ? allTags : [translate('coding_rules.no_tags')]} - overlay={ - canWrite ? ( - - ) : undefined - } - /> -
    - ); - }; - - renderRepository = () => { - const { referencedRepositories, ruleDetails } = this.props; - const repository = referencedRepositories[ruleDetails.repo]; - if (!repository) { - return null; - } - return ( - - - {repository.name} ({ruleDetails.langName}) - - - ); - }; - - renderTemplate = () => { - if (!this.props.ruleDetails.isTemplate) { - return null; - } - return ( - - - {translate('coding_rules.rule_template')} - - - ); - }; - - renderParentTemplate = () => { - const { ruleDetails } = this.props; - if (!ruleDetails.templateKey) { - return null; - } - return ( - - {translate('coding_rules.custom_rule')} - {' ('} - - {translate('coding_rules.show_template')} - - {') '} - - - - - ); - }; - - renderRemediation = () => { - const { ruleDetails } = this.props; - if (!ruleDetails.remFnType) { - return null; - } - return ( - <> - - - - - {ruleDetails.remFnBaseEffort !== undefined && ` ${ruleDetails.remFnBaseEffort}`} - {ruleDetails.remFnGapMultiplier !== undefined && - ` +${ruleDetails.remFnGapMultiplier}`} - {ruleDetails.gapDescription !== undefined && ` ${ruleDetails.gapDescription}`} - - - - - ); - }; - - renderExternalBadge = () => { - const { ruleDetails } = this.props; - if (!ruleDetails.repo) { - return null; - } - const engine = ruleDetails.repo.replace(new RegExp(`^${EXTERNAL_RULE_REPO_PREFIX}`), ''); - if (!engine) { - return null; - } - return ( - - - {engine} - - - ); - }; - - renderKey() { - const EXTERNAL_PREFIX = 'external_'; - const { ruleDetails } = this.props; - const displayedKey = ruleDetails.key.startsWith(EXTERNAL_PREFIX) - ? ruleDetails.key.substring(EXTERNAL_PREFIX.length) - : ruleDetails.key; - return ( - - {displayedKey} - - ); - } - - render() { - const { ruleDetails } = this.props; - const ruleUrl = getRuleUrl(ruleDetails.key); - - const hasTypeData = !ruleDetails.isExternal || ruleDetails.type !== 'UNKNOWN'; - return ( -
    -
    -
    - {ruleDetails.cleanCodeAttributeCategory !== undefined && ( - - )} -
    - -
    - - - - -
    - -
    - {!!ruleDetails.impacts.length && ( -
    - {translate('coding_rules.software_qualities.label')} - -
    - )} -
    - - {hasTypeData && ( -
    - {!ruleDetails.isExternal && ( - <> - {this.renderTemplate()} - {this.renderParentTemplate()} - - )} - - {this.renderRepository()} - {this.renderType()} - {this.renderSeverity()} - {ruleDetails.isExternal && this.renderExternalBadge()} - {!ruleDetails.isExternal && this.renderStatus()} -
    - )} -
    - - {this.renderKey()} - - - {this.renderTags()} - - {this.renderRemediation()} - -
    - ); - } -} - -function RightMetaHeaderInfo({ - title, - children, -}: Readonly<{ title: string; children: React.ReactNode }>) { - return ( -
    - - {title} - - {children} -
    - ); -} - -const StyledSection = styled.div` - border-left: ${themeBorder('default', 'pageBlockBorder')}; -`; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx index b382cc6e72c..6452b4081e2 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx @@ -237,10 +237,7 @@ export default class RuleListItem extends React.PureComponent {
    {rule.impacts.length > 0 && ( - + )}
    diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index d47606dfb64..c68ef607cc0 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2354,7 +2354,7 @@ coding_rules.return_to_list=Return to list coding_rules.see_all=See all rules coding_rules.remove_extended_description=Remove Extended Description coding_rules.remove_extended_description.confirm=Are you sure you want to remove the extended description? -coding_rules.repository_language=Rule repository (language) +coding_rules.repository=Rule repo: coding_rules.revert_to_parent_definition=Revert to Parent Definition coding_rules.revert_to_parent_definition.confirm=This rule will be reverted to the parameters defined in profile {0}. Are you sure? coding_rules.rule_template=Rule Template @@ -2363,7 +2363,8 @@ 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.software_qualities.label=Software qualities impacted +coding_rules.cct_attribute.label=Clean Code attribute 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. @@ -2433,6 +2434,15 @@ coding_rules.facets.tags=Tags coding_rules.facets.repositories=Repositories coding_rules.facets.top=Top {0} +coding_rules.rule_id=Rule ID: + +coding_rules.analysis_scope=Analysis scope: + +coding_rules.scope.MAIN=main sources +coding_rules.scope.TEST=test sources +coding_rules.scope.ALL=all sources + +coding_rules.remediation_effort=Effort: coding_rules.remediation_function=Remediation function coding_rules.remediation_function.LINEAR=Linear coding_rules.remediation_function.LINEAR_OFFSET=Linear with offset @@ -2441,7 +2451,8 @@ coding_rules.remediation_function.coeff=Coeff coding_rules.remediation_function.offset=Offset coding_rules.remediation_function.constant=Constant -coding_rules.external_rule.engine=Rule provided by an external rule engine: {0} +coding_rules.external_rule.engine_tooltip=Rule provided by an external rule engine: {0} +coding_rules.external_rule.engine=Engine: coding_rules.description_section.title.introduction=Introduction coding_rules.description_section.title.root_cause=Why is this an issue? -- 2.39.5