aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2023-09-25 10:12:18 +0200
committersonartech <sonartech@sonarsource.com>2023-10-05 20:02:47 +0000
commitb944a031eaaf02ea9d2250851a6c88fab6c6f30b (patch)
tree20e967d529f3cfed51015af798d61be2dd730455 /server/sonar-web/src
parent7c7a67fcb303e1170536b1604f9003a613936211 (diff)
downloadsonarqube-b944a031eaaf02ea9d2250851a6c88fab6c6f30b.tar.gz
sonarqube-b944a031eaaf02ea9d2250851a6c88fab6c6f30b.zip
SONAR-20500 Migrate Rule Details to the new UI
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/ActivationButton.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RemoveExtendedDescriptionModal.tsx41
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx100
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx149
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx42
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx31
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx173
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx114
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">:&nbsp;</span>
- <span className="value" title={param.defaultValue}>
- {param.defaultValue}
- </span>
- </div>
+ <li key={param.key}>
+ <span className="sw-font-semibold">{param.key}</span>
+ <span>:&nbsp;</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>
</>
);
}