]> source.dussan.org Git - sonarqube.git/commitdiff
[NO-JIRA] Clean up components name and structure
authorPhilippe Perrin <philippe.perrin@sonarsource.com>
Wed, 10 Aug 2022 10:51:13 +0000 (12:51 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 17 Aug 2022 20:03:09 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx [deleted file]
server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx [deleted file]
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/rules/TabViewer.tsx [deleted file]

index 8da3e5384e140a49170a31e9da9452da66095af0..060277b5577c961cbb4c321abc499639e786081f 100644 (file)
@@ -21,12 +21,12 @@ 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 } from '../../../helpers/sanitize';
 import { RuleDetails } from '../../../types/types';
 import { RuleDescriptionSections } from '../rule';
 import RemoveExtendedDescriptionModal from './RemoveExtendedDescriptionModal';
-import RuleTabViewer from './RuleTabViewer';
 
 interface Props {
   canWrite: boolean | undefined;
@@ -203,6 +203,10 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
         ? ruleDetails.descriptionSections[0]
         : undefined;
 
+    const introductionSection = ruleDetails.descriptionSections?.find(
+      section => section.key === RuleDescriptionSections.INTRODUCTION
+    )?.content;
+
     return (
       <div className="js-rule-description">
         {defaultSection && (
@@ -214,7 +218,18 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
           />
         )}
 
-        {hasDescriptionSection && !defaultSection && <RuleTabViewer ruleDetails={ruleDetails} />}
+        {hasDescriptionSection && !defaultSection && (
+          <>
+            {introductionSection && (
+              <div
+                className="rule-desc"
+                // eslint-disable-next-line react/no-danger
+                dangerouslySetInnerHTML={{ __html: sanitizeString(introductionSection) }}
+              />
+            )}
+            <RuleTabViewer ruleDetails={ruleDetails} />
+          </>
+        )}
 
         {ruleDetails.isExternal && (
           <div className="coding-rules-detail-description rule-desc markdown">
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx
deleted file mode 100644 (file)
index 3a5c4b2..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import * as React from 'react';
-import TabViewer from '../../../components/rules/TabViewer';
-import { sanitizeString } from '../../../helpers/sanitize';
-import { RuleDetails } from '../../../types/types';
-import { RuleDescriptionSections } from '../rule';
-
-export interface RuleTabViewerProps {
-  ruleDetails: RuleDetails;
-}
-
-export default function RuleTabViewer(props: RuleTabViewerProps) {
-  const { ruleDetails } = props;
-  const introduction = ruleDetails.descriptionSections?.find(
-    section => section.key === RuleDescriptionSections.INTRODUCTION
-  )?.content;
-
-  return (
-    <>
-      {introduction && (
-        <div
-          className="rule-desc"
-          // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{ __html: sanitizeString(introduction) }}
-        />
-      )}
-      <TabViewer ruleDetails={ruleDetails} />
-    </>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx
deleted file mode 100644 (file)
index 303e233..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import * as React from 'react';
-import TabViewer from '../../../components/rules/TabViewer';
-import { BranchLike } from '../../../types/branch-like';
-import { Issue, RuleDetails } from '../../../types/types';
-import IssueHeader from './IssueHeader';
-
-interface IssueViewerTabsProps {
-  branchLike?: BranchLike;
-  issue: Issue;
-  codeTabContent: React.ReactNode;
-  ruleDetails: RuleDetails;
-  onIssueChange: (issue: Issue) => void;
-}
-
-export default function IssueViewerTabs(props: IssueViewerTabsProps) {
-  const { ruleDetails, issue, codeTabContent, branchLike } = props;
-  return (
-    <>
-      <IssueHeader
-        issue={issue}
-        ruleDetails={ruleDetails}
-        branchLike={branchLike}
-        onIssueChange={props.onIssueChange}
-      />
-      <TabViewer
-        ruleDetails={ruleDetails}
-        extendedDescription={ruleDetails.htmlNote}
-        ruleDescriptionContextKey={issue.ruleDescriptionContextKey}
-        codeTabContent={codeTabContent}
-        scrollInTab={true}
-      />
-    </>
-  );
-}
index 7f902a2dd3b36e0120ce534cf9fd9a2a4314b05d..46fea9e8fb14690e0a48aa20d88e97139010b93d 100644 (file)
@@ -41,6 +41,7 @@ import ListFooter from '../../../components/controls/ListFooter';
 import Suggestions from '../../../components/embed-docs-modal/Suggestions';
 import withIndexationGuard from '../../../components/hoc/withIndexationGuard';
 import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
+import RuleTabViewer from '../../../components/rules/RuleTabViewer';
 import '../../../components/search-navigator.css';
 import { Alert } from '../../../components/ui/Alert';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
@@ -96,9 +97,9 @@ import {
   STANDARDS
 } from '../utils';
 import BulkChangeModal, { MAX_PAGE_SIZE } from './BulkChangeModal';
+import IssueHeader from './IssueHeader';
 import IssuesList from './IssuesList';
 import IssuesSourceViewer from './IssuesSourceViewer';
-import IssueTabViewer from './IssueTabViewer';
 import NoIssues from './NoIssues';
 import NoMyIssues from './NoMyIssues';
 import PageActions from './PageActions';
@@ -1105,24 +1106,32 @@ export class App extends React.PureComponent<Props, State> {
       <div className="layout-page-main-inner">
         <DeferredSpinner loading={loadingRule}>
           {openIssue && openRuleDetails ? (
-            <IssueTabViewer
-              codeTabContent={
-                <IssuesSourceViewer
-                  branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
-                  issues={issues}
-                  locationsNavigator={this.state.locationsNavigator}
-                  onIssueSelect={this.openIssue}
-                  onLocationSelect={this.selectLocation}
-                  openIssue={openIssue}
-                  selectedFlowIndex={this.state.selectedFlowIndex}
-                  selectedLocationIndex={this.state.selectedLocationIndex}
-                />
-              }
-              issue={openIssue}
-              ruleDetails={openRuleDetails}
-              branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
-              onIssueChange={this.handleIssueChange}
-            />
+            <>
+              <IssueHeader
+                issue={openIssue}
+                ruleDetails={openRuleDetails}
+                branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
+                onIssueChange={this.handleIssueChange}
+              />
+              <RuleTabViewer
+                ruleDetails={openRuleDetails}
+                extendedDescription={openRuleDetails.htmlNote}
+                ruleDescriptionContextKey={openIssue.ruleDescriptionContextKey}
+                codeTabContent={
+                  <IssuesSourceViewer
+                    branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
+                    issues={issues}
+                    locationsNavigator={this.state.locationsNavigator}
+                    onIssueSelect={this.openIssue}
+                    onLocationSelect={this.selectLocation}
+                    openIssue={openIssue}
+                    selectedFlowIndex={this.state.selectedFlowIndex}
+                    selectedLocationIndex={this.state.selectedLocationIndex}
+                  />
+                }
+                scrollInTab={true}
+              />
+            </>
           ) : (
             <DeferredSpinner loading={loading} ariaLabel={translate('issues.loading_issues')}>
               {checkAll && paging && paging.total > MAX_PAGE_SIZE && (
diff --git a/server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx b/server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx
new file mode 100644 (file)
index 0000000..f4b9cdf
--- /dev/null
@@ -0,0 +1,328 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { cloneDeep, debounce, groupBy } from 'lodash';
+import * as React from 'react';
+import { dismissNotice } from '../../api/users';
+import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext';
+import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
+import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
+import { translate } from '../../helpers/l10n';
+import { RuleDetails } from '../../types/types';
+import { NoticeType } from '../../types/users';
+import ScreenPositionHelper from '../common/ScreenPositionHelper';
+import BoxedTabs from '../controls/BoxedTabs';
+import MoreInfoRuleDescription from './MoreInfoRuleDescription';
+import RuleDescription from './RuleDescription';
+import './style.css';
+
+interface RuleTabViewerProps extends CurrentUserContextInterface {
+  ruleDetails: RuleDetails;
+  extendedDescription?: string;
+  ruleDescriptionContextKey?: string;
+  codeTabContent?: React.ReactNode;
+  scrollInTab?: boolean;
+}
+
+interface State {
+  tabs: Tab[];
+  selectedTab?: Tab;
+  displayEducationalPrinciplesNotification?: boolean;
+  educationalPrinciplesNotificationHasBeenDismissed?: boolean;
+}
+
+export interface Tab {
+  key: TabKeys;
+  label: React.ReactNode;
+  content: React.ReactNode;
+}
+
+export enum TabKeys {
+  Code = 'code',
+  WhyIsThisAnIssue = 'why',
+  HowToFixIt = 'how_to_fix',
+  AssessTheIssue = 'assess_the_problem',
+  MoreInfo = 'more_info'
+}
+
+const DEBOUNCE_FOR_SCROLL = 250;
+
+export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State> {
+  state: State = {
+    tabs: []
+  };
+
+  educationPrinciplesRef: React.RefObject<HTMLDivElement>;
+
+  constructor(props: RuleTabViewerProps) {
+    super(props);
+    this.educationPrinciplesRef = React.createRef();
+    this.checkIfEducationPrinciplesAreVisible = debounce(
+      this.checkIfEducationPrinciplesAreVisible,
+      DEBOUNCE_FOR_SCROLL
+    );
+  }
+
+  componentDidMount() {
+    this.setState(prevState => this.computeState(prevState));
+    this.attachScrollEvent();
+  }
+
+  componentDidUpdate(prevProps: RuleTabViewerProps, prevState: State) {
+    const { ruleDetails, codeTabContent, ruleDescriptionContextKey, currentUser } = this.props;
+    const { selectedTab } = this.state;
+
+    if (
+      prevProps.ruleDetails.key !== ruleDetails.key ||
+      prevProps.ruleDescriptionContextKey !== ruleDescriptionContextKey ||
+      prevProps.codeTabContent !== codeTabContent ||
+      prevProps.currentUser !== currentUser
+    ) {
+      this.setState(pState =>
+        this.computeState(
+          pState,
+          prevProps.ruleDetails !== ruleDetails || prevProps.codeTabContent !== codeTabContent
+        )
+      );
+    }
+
+    if (selectedTab?.key === TabKeys.MoreInfo) {
+      this.checkIfEducationPrinciplesAreVisible();
+    }
+
+    if (
+      prevState.selectedTab?.key === TabKeys.MoreInfo &&
+      prevState.displayEducationalPrinciplesNotification &&
+      prevState.educationalPrinciplesNotificationHasBeenDismissed
+    ) {
+      this.props.updateDismissedNotices(NoticeType.EDUCATION_PRINCIPLES, true);
+    }
+  }
+
+  componentWillUnmount() {
+    this.detachScrollEvent();
+  }
+
+  computeState = (prevState: State, resetSelectedTab: boolean = false) => {
+    const {
+      ruleDetails,
+      currentUser: { isLoggedIn, dismissedNotices }
+    } = this.props;
+
+    const displayEducationalPrinciplesNotification =
+      !!ruleDetails.educationPrinciples &&
+      ruleDetails.educationPrinciples.length > 0 &&
+      isLoggedIn &&
+      !dismissedNotices[NoticeType.EDUCATION_PRINCIPLES];
+    const tabs = this.computeTabs(displayEducationalPrinciplesNotification);
+
+    return {
+      tabs,
+      selectedTab: resetSelectedTab || !prevState.selectedTab ? tabs[0] : prevState.selectedTab,
+      displayEducationalPrinciplesNotification
+    };
+  };
+
+  computeTabs = (displayEducationalPrinciplesNotification: boolean) => {
+    const {
+      codeTabContent,
+      ruleDetails: { descriptionSections, educationPrinciples, type: ruleType },
+      ruleDescriptionContextKey,
+      extendedDescription
+    } = this.props;
+
+    // As we might tamper with the description later on, we clone to avoid any side effect
+    const descriptionSectionsByKey = cloneDeep(
+      groupBy(descriptionSections, section => section.key)
+    );
+
+    if (extendedDescription) {
+      if (descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]?.length > 0) {
+        // We add the extended description (htmlNote) in the first context, in case there are contexts
+        // Extended description will get reworked in future
+        descriptionSectionsByKey[RuleDescriptionSections.RESOURCES][0].content +=
+          '<br/>' + extendedDescription;
+      } else {
+        descriptionSectionsByKey[RuleDescriptionSections.RESOURCES] = [
+          {
+            key: RuleDescriptionSections.RESOURCES,
+            content: extendedDescription
+          }
+        ];
+      }
+    }
+
+    const tabs: Tab[] = [
+      {
+        key: TabKeys.WhyIsThisAnIssue,
+        label:
+          ruleType === 'SECURITY_HOTSPOT'
+            ? translate('coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT')
+            : translate('coding_rules.description_section.title.root_cause'),
+        content: (descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
+          descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]) && (
+          <RuleDescription
+            className="padded"
+            sections={
+              descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
+              descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]
+            }
+            isDefault={descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] !== undefined}
+            defaultContextKey={ruleDescriptionContextKey}
+          />
+        )
+      },
+      {
+        key: TabKeys.AssessTheIssue,
+        label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue),
+        content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
+          <RuleDescription
+            className="padded"
+            sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
+          />
+        )
+      },
+      {
+        key: TabKeys.HowToFixIt,
+        label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
+        content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
+          <RuleDescription
+            className="padded"
+            sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
+            defaultContextKey={ruleDescriptionContextKey}
+          />
+        )
+      },
+      {
+        key: TabKeys.MoreInfo,
+        label: (
+          <>
+            {translate('coding_rules.description_section.title', TabKeys.MoreInfo)}
+            {displayEducationalPrinciplesNotification && <div className="notice-dot" />}
+          </>
+        ),
+        content: ((educationPrinciples && educationPrinciples.length > 0) ||
+          descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
+          <MoreInfoRuleDescription
+            educationPrinciples={educationPrinciples}
+            sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
+            displayEducationalPrinciplesNotification={displayEducationalPrinciplesNotification}
+            educationPrinciplesRef={this.educationPrinciplesRef}
+          />
+        )
+      }
+    ];
+
+    if (codeTabContent !== undefined) {
+      tabs.unshift({
+        key: TabKeys.Code,
+        label: translate('issue.tabs', TabKeys.Code),
+        content: codeTabContent
+      });
+    }
+
+    return tabs.filter(tab => tab.content);
+  };
+
+  attachScrollEvent = () => {
+    document.addEventListener('scroll', this.checkIfEducationPrinciplesAreVisible, {
+      capture: true
+    });
+  };
+
+  detachScrollEvent = () => {
+    document.removeEventListener('scroll', this.checkIfEducationPrinciplesAreVisible, {
+      capture: true
+    });
+  };
+
+  checkIfEducationPrinciplesAreVisible = () => {
+    const {
+      displayEducationalPrinciplesNotification,
+      educationalPrinciplesNotificationHasBeenDismissed
+    } = this.state;
+
+    if (this.educationPrinciplesRef.current) {
+      const rect = this.educationPrinciplesRef.current.getBoundingClientRect();
+      const isVisible = rect.top <= (window.innerHeight || document.documentElement.clientHeight);
+
+      if (
+        isVisible &&
+        displayEducationalPrinciplesNotification &&
+        !educationalPrinciplesNotificationHasBeenDismissed
+      ) {
+        dismissNotice(NoticeType.EDUCATION_PRINCIPLES)
+          .then(() => {
+            this.detachScrollEvent();
+            this.setState({ educationalPrinciplesNotificationHasBeenDismissed: true });
+          })
+          .catch(() => {
+            /* noop */
+          });
+      }
+    }
+  };
+
+  handleSelectTabs = (currentTabKey: TabKeys) => {
+    this.setState(({ tabs }) => ({
+      selectedTab: tabs.find(tab => tab.key === currentTabKey) || tabs[0]
+    }));
+  };
+
+  render() {
+    const { scrollInTab } = this.props;
+    const { tabs, selectedTab } = this.state;
+
+    if (!tabs || tabs.length === 0 || !selectedTab) {
+      return null;
+    }
+
+    const tabContent = tabs.find(t => t.key === selectedTab.key)?.content;
+
+    return (
+      <>
+        <div>
+          <BoxedTabs
+            className="big-spacer-top"
+            onSelect={this.handleSelectTabs}
+            selected={selectedTab.key}
+            tabs={tabs}
+          />
+        </div>
+        <ScreenPositionHelper>
+          {({ top }) => (
+            <div
+              style={{
+                // We substract the footer height with padding (80) and the main layout padding (20)
+                maxHeight: scrollInTab ? `calc(100vh - ${top + 100}px)` : 'initial'
+              }}
+              className="bordered display-flex-column">
+              {/* Adding a key to force re-rendering of the tab container, so that it resets the scroll position */}
+              <div className="overflow-y-auto spacer" key={selectedTab.key}>
+                {tabContent}
+              </div>
+            </div>
+          )}
+        </ScreenPositionHelper>
+      </>
+    );
+  }
+}
+
+export default withCurrentUserContext(RuleTabViewer);
diff --git a/server/sonar-web/src/main/js/components/rules/TabViewer.tsx b/server/sonar-web/src/main/js/components/rules/TabViewer.tsx
deleted file mode 100644 (file)
index f03307c..0000000
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { cloneDeep, debounce, groupBy } from 'lodash';
-import * as React from 'react';
-import { dismissNotice } from '../../api/users';
-import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext';
-import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
-import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
-import { translate } from '../../helpers/l10n';
-import { RuleDetails } from '../../types/types';
-import { NoticeType } from '../../types/users';
-import ScreenPositionHelper from '../common/ScreenPositionHelper';
-import BoxedTabs from '../controls/BoxedTabs';
-import MoreInfoRuleDescription from './MoreInfoRuleDescription';
-import RuleDescription from './RuleDescription';
-import './style.css';
-
-interface TabViewerProps extends CurrentUserContextInterface {
-  ruleDetails: RuleDetails;
-  extendedDescription?: string;
-  ruleDescriptionContextKey?: string;
-  codeTabContent?: React.ReactNode;
-  scrollInTab?: boolean;
-}
-
-interface State {
-  tabs: Tab[];
-  selectedTab?: Tab;
-  displayEducationalPrinciplesNotification?: boolean;
-  educationalPrinciplesNotificationHasBeenDismissed?: boolean;
-}
-
-export interface Tab {
-  key: TabKeys;
-  label: React.ReactNode;
-  content: React.ReactNode;
-}
-
-export enum TabKeys {
-  Code = 'code',
-  WhyIsThisAnIssue = 'why',
-  HowToFixIt = 'how_to_fix',
-  AssessTheIssue = 'assess_the_problem',
-  MoreInfo = 'more_info'
-}
-
-const DEBOUNCE_FOR_SCROLL = 250;
-
-export class TabViewer extends React.PureComponent<TabViewerProps, State> {
-  state: State = {
-    tabs: []
-  };
-
-  educationPrinciplesRef: React.RefObject<HTMLDivElement>;
-
-  constructor(props: TabViewerProps) {
-    super(props);
-    this.educationPrinciplesRef = React.createRef();
-    this.checkIfEducationPrinciplesAreVisible = debounce(
-      this.checkIfEducationPrinciplesAreVisible,
-      DEBOUNCE_FOR_SCROLL
-    );
-  }
-
-  componentDidMount() {
-    this.setState(prevState => this.computeState(prevState));
-    this.attachScrollEvent();
-  }
-
-  componentDidUpdate(prevProps: TabViewerProps, prevState: State) {
-    const { ruleDetails, codeTabContent, ruleDescriptionContextKey, currentUser } = this.props;
-    const { selectedTab } = this.state;
-
-    if (
-      prevProps.ruleDetails.key !== ruleDetails.key ||
-      prevProps.ruleDescriptionContextKey !== ruleDescriptionContextKey ||
-      prevProps.codeTabContent !== codeTabContent ||
-      prevProps.currentUser !== currentUser
-    ) {
-      this.setState(pState =>
-        this.computeState(
-          pState,
-          prevProps.ruleDetails !== ruleDetails || prevProps.codeTabContent !== codeTabContent
-        )
-      );
-    }
-
-    if (selectedTab?.key === TabKeys.MoreInfo) {
-      this.checkIfEducationPrinciplesAreVisible();
-    }
-
-    if (
-      prevState.selectedTab?.key === TabKeys.MoreInfo &&
-      prevState.displayEducationalPrinciplesNotification &&
-      prevState.educationalPrinciplesNotificationHasBeenDismissed
-    ) {
-      this.props.updateDismissedNotices(NoticeType.EDUCATION_PRINCIPLES, true);
-    }
-  }
-
-  componentWillUnmount() {
-    this.detachScrollEvent();
-  }
-
-  computeState = (prevState: State, resetSelectedTab: boolean = false) => {
-    const {
-      ruleDetails,
-      currentUser: { isLoggedIn, dismissedNotices }
-    } = this.props;
-
-    const displayEducationalPrinciplesNotification =
-      !!ruleDetails.educationPrinciples &&
-      ruleDetails.educationPrinciples.length > 0 &&
-      isLoggedIn &&
-      !dismissedNotices[NoticeType.EDUCATION_PRINCIPLES];
-    const tabs = this.computeTabs(displayEducationalPrinciplesNotification);
-
-    return {
-      tabs,
-      selectedTab: resetSelectedTab || !prevState.selectedTab ? tabs[0] : prevState.selectedTab,
-      displayEducationalPrinciplesNotification
-    };
-  };
-
-  computeTabs = (displayEducationalPrinciplesNotification: boolean) => {
-    const {
-      codeTabContent,
-      ruleDetails: { descriptionSections, educationPrinciples, type: ruleType },
-      ruleDescriptionContextKey,
-      extendedDescription
-    } = this.props;
-
-    // As we might tamper with the description later on, we clone to avoid any side effect
-    const descriptionSectionsByKey = cloneDeep(
-      groupBy(descriptionSections, section => section.key)
-    );
-
-    if (extendedDescription) {
-      if (descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]?.length > 0) {
-        // We add the extended description (htmlNote) in the first context, in case there are contexts
-        // Extended description will get reworked in future
-        descriptionSectionsByKey[RuleDescriptionSections.RESOURCES][0].content +=
-          '<br/>' + extendedDescription;
-      } else {
-        descriptionSectionsByKey[RuleDescriptionSections.RESOURCES] = [
-          {
-            key: RuleDescriptionSections.RESOURCES,
-            content: extendedDescription
-          }
-        ];
-      }
-    }
-
-    const tabs: Tab[] = [
-      {
-        key: TabKeys.WhyIsThisAnIssue,
-        label:
-          ruleType === 'SECURITY_HOTSPOT'
-            ? translate('coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT')
-            : translate('coding_rules.description_section.title.root_cause'),
-        content: (descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
-          descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]) && (
-          <RuleDescription
-            className="padded"
-            sections={
-              descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
-              descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]
-            }
-            isDefault={descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] !== undefined}
-            defaultContextKey={ruleDescriptionContextKey}
-          />
-        )
-      },
-      {
-        key: TabKeys.AssessTheIssue,
-        label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue),
-        content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
-          <RuleDescription
-            className="padded"
-            sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
-          />
-        )
-      },
-      {
-        key: TabKeys.HowToFixIt,
-        label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
-        content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
-          <RuleDescription
-            className="padded"
-            sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
-            defaultContextKey={ruleDescriptionContextKey}
-          />
-        )
-      },
-      {
-        key: TabKeys.MoreInfo,
-        label: (
-          <>
-            {translate('coding_rules.description_section.title', TabKeys.MoreInfo)}
-            {displayEducationalPrinciplesNotification && <div className="notice-dot" />}
-          </>
-        ),
-        content: ((educationPrinciples && educationPrinciples.length > 0) ||
-          descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
-          <MoreInfoRuleDescription
-            educationPrinciples={educationPrinciples}
-            sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
-            displayEducationalPrinciplesNotification={displayEducationalPrinciplesNotification}
-            educationPrinciplesRef={this.educationPrinciplesRef}
-          />
-        )
-      }
-    ];
-
-    if (codeTabContent !== undefined) {
-      tabs.unshift({
-        key: TabKeys.Code,
-        label: translate('issue.tabs', TabKeys.Code),
-        content: codeTabContent
-      });
-    }
-
-    return tabs.filter(tab => tab.content);
-  };
-
-  attachScrollEvent = () => {
-    document.addEventListener('scroll', this.checkIfEducationPrinciplesAreVisible, {
-      capture: true
-    });
-  };
-
-  detachScrollEvent = () => {
-    document.removeEventListener('scroll', this.checkIfEducationPrinciplesAreVisible, {
-      capture: true
-    });
-  };
-
-  checkIfEducationPrinciplesAreVisible = () => {
-    const {
-      displayEducationalPrinciplesNotification,
-      educationalPrinciplesNotificationHasBeenDismissed
-    } = this.state;
-
-    if (this.educationPrinciplesRef.current) {
-      const rect = this.educationPrinciplesRef.current.getBoundingClientRect();
-      const isVisible = rect.top <= (window.innerHeight || document.documentElement.clientHeight);
-
-      if (
-        isVisible &&
-        displayEducationalPrinciplesNotification &&
-        !educationalPrinciplesNotificationHasBeenDismissed
-      ) {
-        dismissNotice(NoticeType.EDUCATION_PRINCIPLES)
-          .then(() => {
-            this.detachScrollEvent();
-            this.setState({ educationalPrinciplesNotificationHasBeenDismissed: true });
-          })
-          .catch(() => {
-            /* noop */
-          });
-      }
-    }
-  };
-
-  handleSelectTabs = (currentTabKey: TabKeys) => {
-    this.setState(({ tabs }) => ({
-      selectedTab: tabs.find(tab => tab.key === currentTabKey) || tabs[0]
-    }));
-  };
-
-  render() {
-    const { scrollInTab } = this.props;
-    const { tabs, selectedTab } = this.state;
-
-    if (!tabs || tabs.length === 0 || !selectedTab) {
-      return null;
-    }
-
-    const tabContent = tabs.find(t => t.key === selectedTab.key)?.content;
-
-    return (
-      <>
-        <div>
-          <BoxedTabs
-            className="big-spacer-top"
-            onSelect={this.handleSelectTabs}
-            selected={selectedTab.key}
-            tabs={tabs}
-          />
-        </div>
-        <ScreenPositionHelper>
-          {({ top }) => (
-            <div
-              style={{
-                // We substract the footer height with padding (80) and the main layout padding (20)
-                maxHeight: scrollInTab ? `calc(100vh - ${top + 100}px)` : 'initial'
-              }}
-              className="bordered display-flex-column">
-              {/* Adding a key to force re-rendering of the tab container, so that it resets the scroll position */}
-              <div className="overflow-y-auto spacer" key={selectedTab.key}>
-                {tabContent}
-              </div>
-            </div>
-          )}
-        </ScreenPositionHelper>
-      </>
-    );
-  }
-}
-
-export default withCurrentUserContext(TabViewer);