diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2023-09-25 10:12:18 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-10-05 20:02:47 +0000 |
commit | b944a031eaaf02ea9d2250851a6c88fab6c6f30b (patch) | |
tree | 20e967d529f3cfed51015af798d61be2dd730455 /server/sonar-web/src | |
parent | 7c7a67fcb303e1170536b1604f9003a613936211 (diff) | |
download | sonarqube-b944a031eaaf02ea9d2250851a6c88fab6c6f30b.tar.gz sonarqube-b944a031eaaf02ea9d2250851a6c88fab6c6f30b.zip |
SONAR-20500 Migrate Rule Details to the new UI
Diffstat (limited to 'server/sonar-web/src')
10 files changed, 353 insertions, 314 deletions
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationButton.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationButton.tsx index 6315b33f5b1..24aa404d821 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationButton.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationButton.tsx @@ -17,9 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ButtonSecondary } from 'design-system'; import * as React from 'react'; import { Profile as BaseProfile } from '../../../api/quality-profiles'; -import { Button } from '../../../components/controls/buttons'; import { Rule, RuleActivation, RuleDetails } from '../../../types/types'; import ActivationFormModal from './ActivationFormModal'; @@ -40,14 +40,14 @@ export default function ActivationButton(props: Props) { return ( <> - <Button + <ButtonSecondary aria-label={ariaLabel} className={className} id="coding-rules-quality-profile-activate" onClick={() => setModalOpen(true)} > {buttonText} - </Button> + </ButtonSecondary> {modalOpen && ( <ActivationFormModal diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RemoveExtendedDescriptionModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RemoveExtendedDescriptionModal.tsx index 1246fd30a58..5c32d8addfa 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RemoveExtendedDescriptionModal.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RemoveExtendedDescriptionModal.tsx @@ -17,9 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { DangerButtonPrimary, Modal } from 'design-system'; import * as React from 'react'; -import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; -import SimpleModal from '../../../components/controls/SimpleModal'; import { translate } from '../../../helpers/l10n'; interface Props { @@ -28,28 +27,26 @@ interface Props { } export default function RemoveExtendedDescriptionModal({ onCancel, onSubmit }: Props) { + const [submitting, setSubmitting] = React.useState(false); const header = translate('coding_rules.remove_extended_description'); - return ( - <SimpleModal header={header} onClose={onCancel} onSubmit={onSubmit}> - {({ onCloseClick, onFormSubmit, submitting }) => ( - <form onSubmit={onFormSubmit}> - <header className="modal-head"> - <h2>{header}</h2> - </header> - <div className="modal-body"> - {translate('coding_rules.remove_extended_description.confirm')} - </div> + const handleClick = React.useCallback(() => { + setSubmitting(true); + onSubmit(); + }, [onSubmit]); - <footer className="modal-foot"> - {submitting && <i className="spinner spacer-right" />} - <SubmitButton className="button-red" disabled={submitting}> - {translate('remove')} - </SubmitButton> - <ResetButtonLink onClick={onCloseClick}>{translate('cancel')}</ResetButtonLink> - </footer> - </form> - )} - </SimpleModal> + return ( + <Modal + headerTitle={header} + body={translate('coding_rules.remove_extended_description.confirm')} + onClose={onCancel} + primaryButton={ + <DangerButtonPrimary disabled={submitting} onClick={handleClick}> + {translate('remove')} + </DangerButtonPrimary> + } + loading={submitting} + secondaryButtonLabel={translate('cancel')} + /> ); } 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 eef92663a2d..feafa53b4da 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 @@ -17,12 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { SubHeadingHighlight } from 'design-system/lib'; import * as React from 'react'; import { Profile } from '../../../api/quality-profiles'; import { deleteRule, getRuleDetails, updateRule } from '../../../api/rules'; import ConfirmButton from '../../../components/controls/ConfirmButton'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import { Button } from '../../../components/controls/buttons'; +import DateFormatter from '../../../components/intl/DateFormatter'; import Spinner from '../../../components/ui/Spinner'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { Dict, RuleActivation, RuleDetails as TypeRuleDetails } from '../../../types/types'; @@ -252,6 +254,13 @@ export default class RuleDetails extends React.PureComponent<Props, State> { {!ruleDetails.isTemplate && ruleDetails.type !== 'SECURITY_HOTSPOT' && ( <RuleDetailsIssues ruleDetails={ruleDetails} /> )} + + <div className="sw-mb-8" data-meta="available-since"> + <SubHeadingHighlight as="h3"> + {translate('coding_rules.available_since')} + </SubHeadingHighlight> + <DateFormatter date={ruleDetails.createdAt} /> + </div> </Spinner> </div> ); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx index df822427393..d104dd1514b 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx @@ -17,16 +17,25 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { + ButtonSecondary, + ContentCell, + DangerButtonSecondary, + HeadingDark, + Link, + Spinner, + Table, + TableRow, + UnorderedList, +} from 'design-system'; import { sortBy } from 'lodash'; import * as React from 'react'; import { deleteRule, searchRules } from '../../../api/rules'; -import Link from '../../../components/common/Link'; import ConfirmButton from '../../../components/controls/ConfirmButton'; -import { Button } from '../../../components/controls/buttons'; -import SeverityHelper from '../../../components/shared/SeverityHelper'; -import Spinner from '../../../components/ui/Spinner'; +import IssueSeverityIcon from '../../../components/icon-mappers/IssueSeverityIcon'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getRuleUrl } from '../../../helpers/urls'; +import { IssueSeverity } from '../../../types/issues'; import { Rule, RuleDetails } from '../../../types/types'; import CustomRuleButton from './CustomRuleButton'; @@ -40,6 +49,9 @@ interface State { rules?: Rule[]; } +const COLUMN_COUNT = 3; +const COLUMN_COUNT_WITH_EDIT_PERMISSIONS = 4; + export default class RuleDetailsCustomRules extends React.PureComponent<Props, State> { mounted = false; state: State = { loading: false }; @@ -97,32 +109,36 @@ export default class RuleDetailsCustomRules extends React.PureComponent<Props, S }; renderRule = (rule: Rule) => ( - <tr data-rule={rule.key} key={rule.key}> - <td className="coding-rules-detail-list-name"> + <TableRow data-rule={rule.key} key={rule.key}> + <ContentCell> <Link to={getRuleUrl(rule.key)}>{rule.name}</Link> - </td> - - <td className="coding-rules-detail-list-severity"> - <SeverityHelper className="display-flex-center" severity={rule.severity} /> - </td> - - <td className="coding-rules-detail-list-parameters"> - {rule.params && - rule.params - .filter((param) => param.defaultValue) + </ContentCell> + + <ContentCell> + <IssueSeverityIcon + className="sw-mr-1" + severity={rule.severity as IssueSeverity} + aria-hidden + /> + {translate('severity', rule.severity)} + </ContentCell> + + <ContentCell> + <UnorderedList className="sw-mt-0"> + {rule.params + ?.filter((param) => param.defaultValue) .map((param) => ( - <div className="coding-rules-detail-list-parameter" key={param.key}> - <span className="key">{param.key}</span> - <span className="sep">: </span> - <span className="value" title={param.defaultValue}> - {param.defaultValue} - </span> - </div> + <li key={param.key}> + <span className="sw-font-semibold">{param.key}</span> + <span>: </span> + <span title={param.defaultValue}>{param.defaultValue}</span> + </li> ))} - </td> + </UnorderedList> + </ContentCell> {this.props.canChange && ( - <td className="coding-rules-detail-list-actions"> + <ContentCell> <ConfirmButton confirmButtonText={translate('delete')} confirmData={rule.key} @@ -132,43 +148,49 @@ export default class RuleDetailsCustomRules extends React.PureComponent<Props, S onConfirm={this.handleRuleDelete} > {({ onClick }) => ( - <Button - className="button-red js-delete-custom-rule" + <DangerButtonSecondary + className="js-delete-custom-rule" aria-label={translateWithParameters('coding_rules.delete_rule_x', rule.name)} onClick={onClick} > {translate('delete')} - </Button> + </DangerButtonSecondary> )} </ConfirmButton> - </td> + </ContentCell> )} - </tr> + </TableRow> ); render() { const { loading, rules = [] } = this.state; return ( - <div className="js-rule-custom-rules coding-rule-section"> - <div className="coding-rules-detail-custom-rules-section"> - <h2 className="coding-rules-detail-title">{translate('coding_rules.custom_rules')}</h2> + <div className="js-rule-custom-rules"> + <div> + <HeadingDark as="h2">{translate('coding_rules.custom_rules')}</HeadingDark> {this.props.canChange && ( <CustomRuleButton onDone={this.handleRuleCreate} templateRule={this.props.ruleDetails}> {({ onClick }) => ( - <Button className="js-create-custom-rule spacer-left" onClick={onClick}> + <ButtonSecondary className="js-create-custom-rule sw-mt-6" onClick={onClick}> {translate('coding_rules.create')} - </Button> + </ButtonSecondary> )} </CustomRuleButton> )} - <Spinner className="spacer-left" loading={loading}> + <Spinner className="sw-my-6" loading={loading}> {rules.length > 0 && ( - <table className="coding-rules-detail-list" id="coding-rules-detail-custom-rules"> - <tbody>{sortBy(rules, (rule) => rule.name).map(this.renderRule)}</tbody> - </table> + <Table + className="sw-my-6" + id="coding-rules-detail-custom-rules" + columnCount={ + this.props.canChange ? COLUMN_COUNT_WITH_EDIT_PERMISSIONS : COLUMN_COUNT + } + > + {sortBy(rules, (rule) => rule.name).map(this.renderRule)} + </Table> )} </Spinner> </div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx index 62e860d6fab..57a2fa03602 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx @@ -18,11 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { CodeSyntaxHighlighter } from 'design-system'; +import { + ButtonPrimary, + ButtonSecondary, + CodeSyntaxHighlighter, + DangerButtonSecondary, + InputTextArea, + Spinner, +} from 'design-system'; import * as React from 'react'; import { updateRule } from '../../../api/rules'; import FormattingTips from '../../../components/common/FormattingTips'; -import { Button, ResetButtonLink } from '../../../components/controls/buttons'; import RuleTabViewer from '../../../components/rules/RuleTabViewer'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { sanitizeString, sanitizeUserInput } from '../../../helpers/sanitize'; @@ -67,7 +73,8 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S this.setState({ descriptionForm: false }); }; - handleSaveClick = () => { + handleSaveClick = (event: React.SyntheticEvent<HTMLFormElement>) => { + event.preventDefault(); this.updateDescription(this.state.description); }; @@ -116,88 +123,82 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S <div id="coding-rules-detail-description-extra"> {this.props.ruleDetails.htmlNote !== undefined && ( <CodeSyntaxHighlighter - className="rule-desc markdown sw-mb-2" + className="markdown sw-my-6" htmlAsString={sanitizeUserInput(this.props.ruleDetails.htmlNote)} language={this.props.ruleDetails.lang} /> )} - {this.props.canWrite && ( - <Button - id="coding-rules-detail-extend-description" - onClick={this.handleExtendDescriptionClick} - > - {translate('coding_rules.extend_description')} - </Button> - )} + <div className="sw-my-6"> + {this.props.canWrite && ( + <ButtonSecondary onClick={this.handleExtendDescriptionClick}> + {translate('coding_rules.extend_description')} + </ButtonSecondary> + )} + </div> </div> ); renderForm = () => ( - <div className="coding-rules-detail-extend-description-form"> - <table className="width-100"> - <tbody> - <tr> - <td colSpan={2}> - <textarea - autoFocus - aria-label={translate('coding_rules.extend_description')} - className="width-100 little-spacer-bottom" - id="coding-rules-detail-extend-description-text" - onChange={this.handleDescriptionChange} - rows={4} - value={this.state.description} - /> - </td> - </tr> - - <tr> - <td> - <Button + <form + aria-label={translate('coding_rules.detail.extend_description.form')} + className="sw-my-6" + onSubmit={this.handleSaveClick} + > + <InputTextArea + aria-label={translate('coding_rules.extend_description')} + className="sw-mb-2 sw-resize-y" + id="coding-rules-detail-extend-description-text" + size="full" + onChange={this.handleDescriptionChange} + rows={4} + value={this.state.description} + /> + + <div className="sw-flex sw-items-center sw-justify-between"> + <div className="sw-flex sw-items-center"> + <ButtonPrimary + id="coding-rules-detail-extend-description-submit" + disabled={this.state.submitting} + type="submit" + > + {translate('save')} + </ButtonPrimary> + + {this.props.ruleDetails.mdNote !== undefined && ( + <> + <DangerButtonSecondary + className="sw-ml-2" disabled={this.state.submitting} - id="coding-rules-detail-extend-description-submit" - onClick={this.handleSaveClick} + id="coding-rules-detail-extend-description-remove" + onClick={this.handleRemoveDescriptionClick} > - {translate('save')} - </Button> - - {this.props.ruleDetails.mdNote !== undefined && ( - <> - <Button - className="button-red spacer-left" - disabled={this.state.submitting} - id="coding-rules-detail-extend-description-remove" - onClick={this.handleRemoveDescriptionClick} - > - {translate('remove')} - </Button> - {this.state.removeDescriptionModal && ( - <RemoveExtendedDescriptionModal - onCancel={this.handleCancelRemoving} - onSubmit={this.handleConfirmRemoving} - /> - )} - </> + {translate('remove')} + </DangerButtonSecondary> + {this.state.removeDescriptionModal && ( + <RemoveExtendedDescriptionModal + onCancel={this.handleCancelRemoving} + onSubmit={this.handleConfirmRemoving} + /> )} - - <ResetButtonLink - className="spacer-left" - disabled={this.state.submitting} - id="coding-rules-detail-extend-description-cancel" - onClick={this.handleCancelClick} - > - {translate('cancel')} - </ResetButtonLink> - {this.state.submitting && <i className="spinner spacer-left" />} - </td> - - <td className="text-right"> - <FormattingTips /> - </td> - </tr> - </tbody> - </table> - </div> + </> + )} + + <ButtonSecondary + className="sw-ml-2" + disabled={this.state.submitting} + id="coding-rules-detail-extend-description-cancel" + onClick={this.handleCancelClick} + > + {translate('cancel')} + </ButtonSecondary> + + <Spinner className="sw-ml-2" loading={this.state.submitting} /> + </div> + + <FormattingTips /> + </div> + </form> ); render() { @@ -252,7 +253,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S )} {!ruleDetails.templateKey && ( - <div className="coding-rules-detail-description coding-rules-detail-description-extra"> + <div className="sw-mt-6"> {!this.state.descriptionForm && this.renderExtendedDescription()} {this.state.descriptionForm && this.props.canWrite && this.renderForm()} </div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx index 874ed9da3c1..057c9242af3 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx @@ -17,14 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ContentCell, Link, Spinner, SubHeadingHighlight, Table, TableRow } from 'design-system'; import * as React from 'react'; import { getFacet } from '../../../api/issues'; import withAvailableFeatures, { WithAvailableFeaturesProps, } from '../../../app/components/available-features/withAvailableFeatures'; -import Link from '../../../components/common/Link'; import Tooltip from '../../../components/controls/Tooltip'; -import Spinner from '../../../components/ui/Spinner'; import { translate } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; import { getIssuesUrl } from '../../../helpers/urls'; @@ -137,12 +136,12 @@ export class RuleDetailsIssues extends React.PureComponent<Props, State> { const path = getIssuesUrl({ resolved: 'false', rules: key, projects: project.key }); return ( - <tr key={project.key}> - <td className="coding-rules-detail-list-name">{project.name}</td> - <td className="coding-rules-detail-list-parameters"> + <TableRow key={project.key}> + <ContentCell>{project.name}</ContentCell> + <ContentCell> <Link to={path}>{formatMeasure(project.count, MetricType.Integer)}</Link> - </td> - </tr> + </ContentCell> + </TableRow> ); }; @@ -150,26 +149,29 @@ export class RuleDetailsIssues extends React.PureComponent<Props, State> { const { loading, projects = [] } = this.state; return ( - <div className="js-rule-issues coding-rule-section"> + <div className="sw-mb-8"> <Spinner loading={loading}> - <h2 className="coding-rules-detail-title"> + <SubHeadingHighlight as="h2"> {translate('coding_rules.issues')} {this.renderTotal()} - </h2> + </SubHeadingHighlight> {projects.length > 0 ? ( - <table className="coding-rules-detail-list coding-rules-most-violated-projects"> - <tbody> - <tr> - <td className="coding-rules-detail-list-name" colSpan={2}> + <Table + className="sw-mt-6" + columnCount={2} + header={ + <TableRow> + <ContentCell colSpan={2}> {translate('coding_rules.most_violating_projects')} - </td> - </tr> - {projects.map(this.renderProject)} - </tbody> - </table> + </ContentCell> + </TableRow> + } + > + {projects.map(this.renderProject)} + </Table> ) : ( - <div className="big-padded-bottom"> + <div className="sw-mb-6"> {translate('coding_rules.no_issue_detected_for_projects')} </div> )} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx index ba62978ab67..88dff1ff20f 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.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 { CellComponent, Note, SubHeadingHighlight, Table, TableRow } from 'design-system'; import * as React from 'react'; import { translate } from '../../../helpers/l10n'; import { sanitizeString } from '../../../helpers/sanitize'; @@ -29,33 +30,33 @@ interface Props { export default function RuleDetailsParameters({ params }: Props) { return ( <div className="js-rule-parameters"> - <h3 className="coding-rules-detail-title">{translate('coding_rules.parameters')}</h3> - <table className="coding-rules-detail-parameters"> - <tbody> - {params.map((param) => ( - <tr className="coding-rules-detail-parameter" key={param.key}> - <td className="coding-rules-detail-parameter-name">{param.key}</td> - <td className="coding-rules-detail-parameter-description"> + <SubHeadingHighlight as="h3">{translate('coding_rules.parameters')}</SubHeadingHighlight> + <Table className="sw-my-4" columnCount={2} columnWidths={[0, 'auto']}> + {params.map((param) => ( + <TableRow key={param.key}> + <CellComponent className="sw-align-top sw-font-semibold">{param.key}</CellComponent> + <CellComponent> + <div className="sw-flex sw-flex-col sw-gap-2"> {param.htmlDesc !== undefined && ( - <p + <div // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }} /> )} {param.defaultValue !== undefined && ( - <div className="note spacer-top"> + <Note as="div"> {translate('coding_rules.parameters.default_value')} <br /> <span className="coding-rules-detail-parameter-value"> {param.defaultValue} </span> - </div> + </Note> )} - </td> - </tr> - ))} - </tbody> - </table> + </div> + </CellComponent> + </TableRow> + ))} + </Table> </div> ); } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx index c5f2e31b5ac..d99f838eec7 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx @@ -17,19 +17,30 @@ * 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 { + ActionCell, + CellComponent, + ContentCell, + DangerButtonSecondary, + DiscreetLink, + InheritanceIcon, + Link, + Note, + SubHeadingHighlight, + Table, + TableRowInteractive, +} from 'design-system'; import { filter } from 'lodash'; import * as React from 'react'; -import { activateRule, deactivateRule, Profile } from '../../../api/quality-profiles'; -import InstanceMessage from '../../../components/common/InstanceMessage'; -import Link from '../../../components/common/Link'; -import { Button } from '../../../components/controls/buttons'; +import { FormattedMessage } from 'react-intl'; +import { Profile, activateRule, deactivateRule } from '../../../api/quality-profiles'; import ConfirmButton from '../../../components/controls/ConfirmButton'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getQualityProfileUrl } from '../../../helpers/urls'; import { Dict, RuleActivation, RuleDetails } from '../../../types/types'; import BuiltInQualityProfileBadge from '../../quality-profiles/components/BuiltInQualityProfileBadge'; import ActivationButton from './ActivationButton'; -import RuleInheritanceIcon from './RuleInheritanceIcon'; interface Props { activations: RuleActivation[] | undefined; @@ -40,6 +51,11 @@ interface Props { ruleDetails: RuleDetails; } +const COLUMN_COUNT_WITH_PARAMS = 3; +const COLUMN_COUNT_WITHOUT_PARAMS = 2; + +const PROFILES_HEADING_ID = 'rule-details-profiles-heading'; + export default class RuleDetailsProfiles extends React.PureComponent<Props> { handleActivate = () => this.props.onActivate(); @@ -68,16 +84,16 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props> { } const profilePath = getQualityProfileUrl(profile.parentName, profile.language); return ( - <div className="coding-rules-detail-quality-profile-inheritance"> - {(activation.inherit === 'OVERRIDES' || activation.inherit === 'INHERITED') && ( - <> - <RuleInheritanceIcon className="text-middle" inheritance={activation.inherit} /> - <Link className="little-spacer-left text-middle" to={profilePath}> - {profile.parentName} - </Link> - </> - )} - </div> + (activation.inherit === 'OVERRIDES' || activation.inherit === 'INHERITED') && ( + <Note as="div" className="sw-flex sw-items-center sw-mt-2"> + <InheritanceIcon + fill={activation.inherit === 'OVERRIDES' ? 'destructiveIconFocus' : 'currentColor'} + /> + <DiscreetLink className="sw-ml-1" to={profilePath}> + {profile.parentName} + </DiscreetLink> + </Note> + ) ); }; @@ -86,25 +102,28 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props> { const originalValue = originalParam?.value; return ( - <div className="coding-rules-detail-quality-profile-parameter" key={param.key}> + <StyledParameter className="sw-my-4" key={param.key}> <span className="key">{param.key}</span> - <span className="sep">: </span> + <span className="sep sw-mr-1">: </span> <span className="value" title={param.value}> {param.value} </span> {parentActivation && param.value !== originalValue && ( - <div className="coding-rules-detail-quality-profile-inheritance"> - {translate('coding_rules.original')} <span className="value">{originalValue}</span> + <div className="sw-flex sw-ml-4"> + {translate('coding_rules.original')} + <span className="value sw-ml-1" title={originalValue}> + {originalValue} + </span> </div> )} - </div> + </StyledParameter> ); }; renderParameters = (activation: RuleActivation, parentActivation?: RuleActivation) => ( - <td className="coding-rules-detail-quality-profile-parameters"> + <CellComponent> {activation.params.map((param) => this.renderParameter(param, parentActivation))} - </td> + </CellComponent> ); renderActions = (activation: RuleActivation, profile: Profile) => { @@ -112,7 +131,7 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props> { const { ruleDetails } = this.props; const hasParent = activation.inherit !== 'NONE' && profile.parentKey; return ( - <td className="coding-rules-detail-quality-profile-actions"> + <ActionCell> {canEdit && ( <> {!ruleDetails.isTemplate && !!ruleDetails.params?.length && ( @@ -120,7 +139,6 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props> { activation={activation} ariaLabel={translateWithParameters('coding_rules.change_details_x', profile.name)} buttonText={translate('change_verb')} - className="coding-rules-detail-quality-profile-change" modalHeader={translate('coding_rules.change_details')} onDone={this.handleActivate} profiles={[profile]} @@ -140,12 +158,9 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props> { onConfirm={this.handleRevert} > {({ onClick }) => ( - <Button - className="coding-rules-detail-quality-profile-revert button-red spacer-left" - onClick={onClick} - > + <DangerButtonSecondary className="sw-ml-2" onClick={onClick}> {translate('coding_rules.revert_to_parent_definition')} - </Button> + </DangerButtonSecondary> )} </ConfirmButton> )} @@ -159,8 +174,8 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props> { onConfirm={this.handleDeactivate} > {({ onClick }) => ( - <Button - className="coding-rules-detail-quality-profile-deactivate button-red spacer-left" + <DangerButtonSecondary + className="sw-ml-2" aria-label={translateWithParameters( 'coding_rules.deactivate_in_quality_profile_x', profile.name, @@ -168,13 +183,13 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props> { onClick={onClick} > {translate('coding_rules.deactivate')} - </Button> + </DangerButtonSecondary> )} </ConfirmButton> )} </> )} - </td> + </ActionCell> ); }; @@ -188,16 +203,20 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props> { const parentActivation = activations.find((x) => x.qProfile === profile.parentKey); return ( - <tr key={profile.key}> - <td className="coding-rules-detail-quality-profile-name"> - <Link to={getQualityProfileUrl(profile.name, profile.language)}>{profile.name}</Link> - {profile.isBuiltIn && <BuiltInQualityProfileBadge className="spacer-left" />} - {this.renderInheritedProfile(activation, profile)} - </td> + <TableRowInteractive key={profile.key}> + <ContentCell className="coding-rules-detail-quality-profile-name"> + <div className="sw-flex sw-flex-col"> + <div> + <Link to={getQualityProfileUrl(profile.name, profile.language)}>{profile.name}</Link> + {profile.isBuiltIn && <BuiltInQualityProfileBadge className="sw-ml-2" />} + </div> + {this.renderInheritedProfile(activation, profile)} + </div> + </ContentCell> {!ruleDetails.templateKey && this.renderParameters(activation, parentActivation)} {this.renderActions(activation, profile)} - </tr> + </TableRowInteractive> ); }; @@ -208,36 +227,52 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props> { ); return ( - <div className="js-rule-profiles coding-rule-section"> - <div className="coding-rules-detail-quality-profiles-section"> - <h2 className="coding-rules-detail-title"> - <InstanceMessage message={translate('coding_rules.quality_profiles')} /> - </h2> - - {canActivate && ( - <ActivationButton - buttonText={translate('coding_rules.activate')} - className="coding-rules-quality-profile-activate" - modalHeader={translate('coding_rules.activate_in_quality_profile')} - onDone={this.handleActivate} - profiles={filter( - this.props.referencedProfiles, - (profile) => !activations.find((activation) => activation.qProfile === profile.key), - )} - rule={ruleDetails} - /> - )} - - {activations.length > 0 && ( - <table - className="coding-rules-detail-quality-profiles width-100" - id="coding-rules-detail-quality-profiles" - > - <tbody>{activations.map(this.renderActivation)}</tbody> - </table> - )} - </div> + <div className="js-rule-profiles sw-mb-8"> + <SubHeadingHighlight as="h2" id={PROFILES_HEADING_ID}> + <FormattedMessage id="coding_rules.quality_profiles" /> + </SubHeadingHighlight> + + {canActivate && ( + <ActivationButton + buttonText={translate('coding_rules.activate')} + className="sw-mt-6" + modalHeader={translate('coding_rules.activate_in_quality_profile')} + onDone={this.handleActivate} + profiles={filter( + this.props.referencedProfiles, + (profile) => !activations.find((activation) => activation.qProfile === profile.key), + )} + rule={ruleDetails} + /> + )} + + {activations.length > 0 && ( + <Table + aria-labelledby={PROFILES_HEADING_ID} + className="sw-my-6" + columnCount={ + ruleDetails.templateKey ? COLUMN_COUNT_WITHOUT_PARAMS : COLUMN_COUNT_WITH_PARAMS + } + id="coding-rules-detail-quality-profiles" + > + {activations.map(this.renderActivation)} + </Table> + )} </div> ); } } + +const StyledParameter = styled.div` + display: flex; + align-items: center; + flex-wrap: wrap; + + .value { + display: inline-block; + max-width: 300px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +`; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx b/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx index 7af8515d6b0..2a7b9afd7e9 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx @@ -124,7 +124,7 @@ const selectors = { name: 'coding_rules.description_section.title.assess_the_problem', }), moreInfoTab: byRole('tab', { - name: 'coding_rules.description_section.title.more_info', + name: /coding_rules.description_section.title.more_info/, }), howToFixTab: byRole('tab', { name: 'coding_rules.description_section.title.how_to_fix', diff --git a/server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx b/server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx index a6f04251be1..f8db72459dd 100644 --- a/server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx +++ b/server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx @@ -29,21 +29,19 @@ import { RuleDescriptionSections } from '../../apps/coding-rules/rule'; import { translate } from '../../helpers/l10n'; import { Issue, RuleDetails } from '../../types/types'; import { NoticeType } from '../../types/users'; -import ScreenPositionHelper from '../common/ScreenPositionHelper'; -import BoxedTabs, { getTabId, getTabPanelId } from '../controls/BoxedTabs'; +import { getTabId, getTabPanelId } from '../controls/BoxedTabs'; import withLocation from '../hoc/withLocation'; import MoreInfoRuleDescription from './MoreInfoRuleDescription'; import RuleDescription from './RuleDescription'; +import { ToggleButton } from 'design-system/lib'; import './style.css'; interface RuleTabViewerProps extends CurrentUserContextInterface { ruleDetails: RuleDetails; extendedDescription?: string; ruleDescriptionContextKey?: string; - codeTabContent?: React.ReactNode; activityTabContent?: React.ReactNode; - scrollInTab?: boolean; location: Location; selectedFlowIndex?: number; selectedLocationIndex?: number; @@ -58,9 +56,10 @@ interface State { } export interface Tab { - key: TabKeys; - label: React.ReactNode; + value: TabKeys; + label: string; content: React.ReactNode; + counter?: number; } export enum TabKeys { @@ -102,7 +101,7 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State if (query.has('why')) { this.setState({ - selectedTab: tabs.find((tab) => tab.key === TabKeys.WhyIsThisAnIssue) ?? tabs[0], + selectedTab: tabs.find((tab) => tab.value === TabKeys.WhyIsThisAnIssue) ?? tabs[0], }); } } @@ -138,12 +137,12 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State ); } - if (selectedTab?.key === TabKeys.MoreInfo) { + if (selectedTab?.value === TabKeys.MoreInfo) { this.checkIfEducationPrinciplesAreVisible(); } if ( - prevState.selectedTab?.key === TabKeys.MoreInfo && + prevState.selectedTab?.value === TabKeys.MoreInfo && prevState.displayEducationalPrinciplesNotification && prevState.educationalPrinciplesNotificationHasBeenDismissed ) { @@ -178,9 +177,7 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State computeTabs = (displayEducationalPrinciplesNotification: boolean) => { const { - codeTabContent, ruleDetails: { descriptionSections, educationPrinciples, lang: ruleLanguage, type: ruleType }, - ruleDescriptionContextKey, extendedDescription, activityTabContent, } = this.props; @@ -211,8 +208,6 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State content: (descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] || descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]) && ( <RuleDescription - className="padded" - defaultContextKey={ruleDescriptionContextKey} language={ruleLanguage} sections={ descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] || @@ -220,7 +215,7 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State } /> ), - key: TabKeys.WhyIsThisAnIssue, + value: TabKeys.WhyIsThisAnIssue, label: ruleType === 'SECURITY_HOTSPOT' ? translate('coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT') @@ -229,29 +224,26 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State { content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && ( <RuleDescription - className="padded" language={ruleLanguage} sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]} /> ), - key: TabKeys.AssessTheIssue, + value: TabKeys.AssessTheIssue, label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue), }, { content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && ( <RuleDescription - className="padded" - defaultContextKey={ruleDescriptionContextKey} language={ruleLanguage} sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]} /> ), - key: TabKeys.HowToFixIt, + value: TabKeys.HowToFixIt, label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt), }, { content: activityTabContent, - key: TabKeys.Activity, + value: TabKeys.Activity, label: translate('coding_rules.description_section.title', TabKeys.Activity), }, { @@ -265,24 +257,12 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]} /> ), - key: TabKeys.MoreInfo, - label: ( - <> - {translate('coding_rules.description_section.title', TabKeys.MoreInfo)} - {displayEducationalPrinciplesNotification && <div className="notice-dot" />} - </> - ), + value: TabKeys.MoreInfo, + label: translate('coding_rules.description_section.title', TabKeys.MoreInfo), + counter: displayEducationalPrinciplesNotification ? 1 : undefined, }, ]; - if (codeTabContent !== undefined) { - tabs.unshift({ - content: codeTabContent, - key: TabKeys.Code, - label: translate('issue.tabs', TabKeys.Code), - }); - } - return tabs.filter((tab) => tab.content); }; @@ -327,12 +307,11 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State handleSelectTabs = (currentTabKey: TabKeys) => { this.setState(({ tabs }) => ({ - selectedTab: tabs.find((tab) => tab.key === currentTabKey) || tabs[0], + selectedTab: tabs.find((tab) => tab.value === currentTabKey) ?? tabs[0], })); }; render() { - const { scrollInTab } = this.props; const { tabs, selectedTab } = this.state; if (!tabs || tabs.length === 0 || !selectedTab) { @@ -341,42 +320,35 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State return ( <> - <div> - <BoxedTabs - className="big-spacer-top" - onSelect={this.handleSelectTabs} - selected={selectedTab.key} - tabs={tabs} + <div className="sw-mt-4"> + <ToggleButton + role="tablist" + onChange={this.handleSelectTabs} + options={tabs} + value={selectedTab.value} /> </div> - <ScreenPositionHelper> - {({ top }) => ( - <div - aria-labelledby={getTabId(selectedTab.key)} - className="bordered display-flex-column" - id={getTabPanelId(selectedTab.key)} - role="tabpanel" - style={{ - // We substract the footer height with padding (80) and the main layout padding (20) - maxHeight: scrollInTab ? `calc(100vh - ${top + 100}px)` : 'initial', - }} - > - { - // Preserve tabs state by always rendering all of them. Only hide them when not selected - tabs.map((tab) => ( - <div - className={classNames('overflow-y-auto spacer', { - hidden: tab.key !== selectedTab.key, - })} - key={tab.key} - > - {tab.content} - </div> - )) - } - </div> - )} - </ScreenPositionHelper> + + <div + aria-labelledby={getTabId(selectedTab.value)} + className="sw-flex sw-flex-col sw-py-6" + id={getTabPanelId(selectedTab.value)} + role="tabpanel" + > + { + // Preserve tabs state by always rendering all of them. Only hide them when not selected + tabs.map((tab) => ( + <div + className={classNames({ + 'sw-hidden': tab.value !== selectedTab.value, + })} + key={tab.value} + > + {tab.content} + </div> + )) + } + </div> </> ); } |