]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17169 Improve scrolling architecture in issues page
authorPhilippe Perrin <philippe.perrin@sonarsource.com>
Wed, 10 Aug 2022 17:06:16 +0000 (19:06 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 17 Aug 2022 20:03:09 +0000 (20:03 +0000)
13 files changed:
server/sonar-web/src/main/js/apps/issues/components/IssueSourceViewerScrollContext.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx
server/sonar-web/src/main/js/apps/issues/utils.ts
server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap
server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx
server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx

diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueSourceViewerScrollContext.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueSourceViewerScrollContext.tsx
new file mode 100644 (file)
index 0000000..9a4a1a2
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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 React from 'react';
+
+export interface IssueSourceViewerScrollContextInterface {
+  registerPrimaryLocationRef: React.Ref<HTMLElement>;
+  registerSelectedSecondaryLocationRef: React.Ref<HTMLElement>;
+}
+
+export const IssueSourceViewerScrollContext = React.createContext<
+  IssueSourceViewerScrollContextInterface | undefined
+>(undefined);
index 46fea9e8fb14690e0a48aa20d88e97139010b93d..e1b7e1caaa5959b12e2d165d99d3864d0d769e15 100644 (file)
@@ -89,7 +89,6 @@ import {
   parseQuery,
   Query,
   saveMyIssues,
-  scrollToIssue,
   serializeQuery,
   shouldOpenSonarSourceSecurityFacet,
   shouldOpenStandardsChildFacet,
@@ -227,13 +226,6 @@ export class App extends React.PureComponent<Props, State> {
     ) {
       this.fetchFirstIssues();
       this.setState({ checkAll: false });
-    } else if (
-      !this.state.openIssue &&
-      (prevState.selected !== this.state.selected || prevState.openIssue)
-    ) {
-      // if user simply selected another issue
-      // or if user went from the source code back to the list of issues
-      this.scrollToSelectedIssue();
     } else if (openIssue && openIssue.key !== this.state.selected) {
       this.setState({
         locationsNavigator: false,
@@ -391,14 +383,11 @@ export class App extends React.PureComponent<Props, State> {
     };
     if (this.state.openIssue) {
       if (path.query.open && path.query.open === this.state.openIssue.key) {
-        this.setState(
-          {
-            locationsNavigator: false,
-            selectedFlowIndex: undefined,
-            selectedLocationIndex: undefined
-          },
-          this.scrollToSelectedIssue
-        );
+        this.setState({
+          locationsNavigator: false,
+          selectedFlowIndex: undefined,
+          selectedLocationIndex: undefined
+        });
       } else {
         this.props.router.replace(path);
       }
@@ -429,13 +418,6 @@ export class App extends React.PureComponent<Props, State> {
     }
   };
 
-  scrollToSelectedIssue = (smooth = true) => {
-    const { selected } = this.state;
-    if (selected) {
-      scrollToIssue(selected, smooth);
-    }
-  };
-
   createdAfterIncludesTime = () => Boolean(this.props.location.query.createdAfter?.includes('T'));
 
   fetchIssuesHelper = (query: RawQuery) => {
index ee79ab270591cceffea76750f54632f5d2568d67..8e0999fd40325c546f570469bb19c8acb2c07656 100644 (file)
@@ -22,6 +22,7 @@ import { BranchLike } from '../../../types/branch-like';
 import { Issue } from '../../../types/types';
 import CrossComponentSourceViewer from '../crossComponentSourceViewer/CrossComponentSourceViewer';
 import { getLocations, getSelectedLocation } from '../utils';
+import { IssueSourceViewerScrollContext } from './IssueSourceViewerScrollContext';
 
 export interface IssuesSourceViewerProps {
   branchLike: BranchLike | undefined;
@@ -34,39 +35,96 @@ export interface IssuesSourceViewerProps {
   selectedLocationIndex: number | undefined;
 }
 
-export default function IssuesSourceViewer(props: IssuesSourceViewerProps) {
-  const {
-    openIssue,
-    selectedFlowIndex,
-    selectedLocationIndex,
-    locationsNavigator,
-    branchLike,
-    issues
-  } = props;
+export default class IssuesSourceViewer extends React.PureComponent<IssuesSourceViewerProps> {
+  primaryLocationRef?: HTMLElement;
+  selectedSecondaryLocationRef?: HTMLElement;
 
-  const locations = getLocations(openIssue, selectedFlowIndex).map((loc, index) => {
-    loc.index = index;
-    return loc;
-  });
-  const selectedLocation = getSelectedLocation(openIssue, selectedFlowIndex, selectedLocationIndex);
+  componentDidUpdate() {
+    if (this.props.selectedLocationIndex === -1) {
+      this.refreshScroll();
+    }
+  }
 
-  const highlightedLocationMessage =
-    locationsNavigator && selectedLocationIndex !== undefined
-      ? selectedLocation && { index: selectedLocationIndex, text: selectedLocation.msg }
-      : undefined;
-  return (
-    <div>
-      <CrossComponentSourceViewer
-        branchLike={branchLike}
-        highlightedLocationMessage={highlightedLocationMessage}
-        issue={openIssue}
-        issues={issues}
-        locations={locations}
-        onIssueSelect={props.onIssueSelect}
-        onLocationSelect={props.onLocationSelect}
-        selectedFlowIndex={selectedFlowIndex}
-        selectedLocationIndex={selectedLocationIndex}
-      />
-    </div>
-  );
+  registerPrimaryLocationRef = (ref: HTMLElement) => {
+    this.primaryLocationRef = ref;
+
+    if (ref) {
+      this.refreshScroll();
+    }
+  };
+
+  registerSelectedSecondaryLocationRef = (ref: HTMLElement) => {
+    this.selectedSecondaryLocationRef = ref;
+
+    if (ref) {
+      this.refreshScroll();
+    }
+  };
+
+  refreshScroll() {
+    const { selectedLocationIndex } = this.props;
+
+    if (
+      selectedLocationIndex !== undefined &&
+      selectedLocationIndex !== -1 &&
+      this.selectedSecondaryLocationRef
+    ) {
+      this.selectedSecondaryLocationRef.scrollIntoView({
+        behavior: 'smooth',
+        block: 'center',
+        inline: 'nearest'
+      });
+    } else if (this.primaryLocationRef) {
+      this.primaryLocationRef.scrollIntoView({
+        block: 'center',
+        inline: 'nearest'
+      });
+    }
+  }
+
+  render() {
+    const {
+      openIssue,
+      selectedFlowIndex,
+      selectedLocationIndex,
+      locationsNavigator,
+      branchLike,
+      issues
+    } = this.props;
+
+    const locations = getLocations(openIssue, selectedFlowIndex).map((loc, index) => {
+      loc.index = index;
+      return loc;
+    });
+
+    const selectedLocation = getSelectedLocation(
+      openIssue,
+      selectedFlowIndex,
+      selectedLocationIndex
+    );
+
+    const highlightedLocationMessage =
+      locationsNavigator && selectedLocationIndex !== undefined
+        ? selectedLocation && { index: selectedLocationIndex, text: selectedLocation.msg }
+        : undefined;
+
+    return (
+      <IssueSourceViewerScrollContext.Provider
+        value={{
+          registerPrimaryLocationRef: this.registerPrimaryLocationRef,
+          registerSelectedSecondaryLocationRef: this.registerSelectedSecondaryLocationRef
+        }}>
+        <CrossComponentSourceViewer
+          branchLike={branchLike}
+          highlightedLocationMessage={highlightedLocationMessage}
+          issue={openIssue}
+          issues={issues}
+          locations={locations}
+          onIssueSelect={this.props.onIssueSelect}
+          onLocationSelect={this.props.onLocationSelect}
+          selectedFlowIndex={selectedFlowIndex}
+        />
+      </IssueSourceViewerScrollContext.Provider>
+    );
+  }
 }
index 17f3c6d788756b80600eb04780976b0637c5a1f7..f835fb8a7b3402996db06271322fc736e699cc10 100644 (file)
@@ -1,7 +1,14 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`should render CrossComponentSourceViewer correctly 1`] = `
-<div>
+<ContextProvider
+  value={
+    Object {
+      "registerPrimaryLocationRef": [Function],
+      "registerSelectedSecondaryLocationRef": [Function],
+    }
+  }
+>
   <CrossComponentSourceViewer
     branchLike={
       Object {
@@ -211,11 +218,18 @@ exports[`should render CrossComponentSourceViewer correctly 1`] = `
     onIssueSelect={[MockFunction]}
     onLocationSelect={[MockFunction]}
   />
-</div>
+</ContextProvider>
 `;
 
 exports[`should render SourceViewer correctly: all secondary locations on same line 1`] = `
-<div>
+<ContextProvider
+  value={
+    Object {
+      "registerPrimaryLocationRef": [Function],
+      "registerSelectedSecondaryLocationRef": [Function],
+    }
+  }
+>
   <CrossComponentSourceViewer
     branchLike={
       Object {
@@ -445,11 +459,18 @@ exports[`should render SourceViewer correctly: all secondary locations on same l
     onIssueSelect={[MockFunction]}
     onLocationSelect={[MockFunction]}
   />
-</div>
+</ContextProvider>
 `;
 
 exports[`should render SourceViewer correctly: default 1`] = `
-<div>
+<ContextProvider
+  value={
+    Object {
+      "registerPrimaryLocationRef": [Function],
+      "registerSelectedSecondaryLocationRef": [Function],
+    }
+  }
+>
   <CrossComponentSourceViewer
     branchLike={
       Object {
@@ -525,11 +546,18 @@ exports[`should render SourceViewer correctly: default 1`] = `
     onIssueSelect={[MockFunction]}
     onLocationSelect={[MockFunction]}
   />
-</div>
+</ContextProvider>
 `;
 
 exports[`should render SourceViewer correctly: single secondary location 1`] = `
-<div>
+<ContextProvider
+  value={
+    Object {
+      "registerPrimaryLocationRef": [Function],
+      "registerSelectedSecondaryLocationRef": [Function],
+    }
+  }
+>
   <CrossComponentSourceViewer
     branchLike={
       Object {
@@ -719,5 +747,5 @@ exports[`should render SourceViewer correctly: single secondary location 1`] = `
     onIssueSelect={[MockFunction]}
     onLocationSelect={[MockFunction]}
   />
-</div>
+</ContextProvider>
 `;
index 892849613c1934cb66819aa96116c0b067df9d6d..a8553151e0f401e4194c8eee3a4c4f339314a583 100644 (file)
@@ -37,6 +37,7 @@ import {
   SourceLine,
   SourceViewerFile
 } from '../../../types/types';
+import { IssueSourceViewerScrollContext } from '../components/IssueSourceViewerScrollContext';
 import IssueSourceViewerHeader from './IssueSourceViewerHeader';
 import SnippetViewer from './SnippetViewer';
 import {
@@ -67,7 +68,6 @@ interface Props {
     line: number
   ) => React.ReactNode;
   snippetGroup: SnippetGroup;
-  selectedLocationIndex: number | undefined;
 }
 
 interface State {
@@ -206,13 +206,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
   };
 
   renderIssuesList = (line: SourceLine) => {
-    const {
-      isLastOccurenceOfPrimaryComponent,
-      issue,
-      issuesByLine,
-      snippetGroup,
-      selectedLocationIndex
-    } = this.props;
+    const { isLastOccurenceOfPrimaryComponent, issue, issuesByLine, snippetGroup } = this.props;
     const locations =
       issue.component === snippetGroup.component.key && issue.textRange !== undefined
         ? locationsByLine([issue])
@@ -226,15 +220,21 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
     return (
       issuesForLine.length > 0 && (
         <div>
-          {issuesForLine.map(issueToDisplay => (
-            <IssueMessageBox
-              selected={!!(issueToDisplay.key === issue.key && issueLocations.length > 0)}
-              key={issueToDisplay.key}
-              issue={issueToDisplay}
-              onClick={this.props.onIssueSelect}
-              selectedLocationIndex={selectedLocationIndex}
-            />
-          ))}
+          {issuesForLine.map(issueToDisplay => {
+            const isSelectedIssue = issueToDisplay.key === issue.key;
+            return (
+              <IssueSourceViewerScrollContext.Consumer key={issueToDisplay.key}>
+                {ctx => (
+                  <IssueMessageBox
+                    selected={!!(isSelectedIssue && issueLocations.length > 0)}
+                    issue={issueToDisplay}
+                    onClick={this.props.onIssueSelect}
+                    ref={isSelectedIssue ? ctx?.registerPrimaryLocationRef : undefined}
+                  />
+                )}
+              </IssueSourceViewerScrollContext.Consumer>
+            );
+          })}
         </div>
       )
     );
@@ -246,8 +246,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
       isLastOccurenceOfPrimaryComponent,
       issue,
       lastSnippetGroup,
-      snippetGroup,
-      selectedLocationIndex
+      snippetGroup
     } = this.props;
     const { additionalLines, loading, snippets } = this.state;
     const locations =
@@ -280,12 +279,16 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
         />
 
         {issue.component === snippetGroup.component.key && issue.textRange === undefined && (
-          <IssueMessageBox
-            selected={true}
-            issue={issue}
-            onClick={this.props.onIssueSelect}
-            selectedLocationIndex={selectedLocationIndex}
-          />
+          <IssueSourceViewerScrollContext.Consumer>
+            {ctx => (
+              <IssueMessageBox
+                selected={true}
+                issue={issue}
+                onClick={this.props.onIssueSelect}
+                ref={ctx?.registerPrimaryLocationRef}
+              />
+            )}
+          </IssueSourceViewerScrollContext.Consumer>
         )}
         {snippetLines.map((snippet, index) => (
           <SnippetViewer
index 77588f5fe94196d4cebf94fd809cca23df018ca0..6fcd02d3a24e10f38c12f5db69f4d1607e0d155a 100644 (file)
@@ -62,7 +62,6 @@ interface Props {
   onIssueSelect: (issueKey: string) => void;
   onLocationSelect: (index: number) => void;
   selectedFlowIndex: number | undefined;
-  selectedLocationIndex: number | undefined;
 }
 
 interface State {
@@ -184,7 +183,6 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop
 
   render() {
     const { loading, notAccessible } = this.state;
-    const { selectedLocationIndex } = this.props;
 
     if (loading) {
       return (
@@ -238,7 +236,6 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop
                 onLocationSelect={this.props.onLocationSelect}
                 renderDuplicationPopup={this.renderDuplicationPopup}
                 snippetGroup={snippetGroup}
-                selectedLocationIndex={selectedLocationIndex}
               />
             </SourceViewerContext.Provider>
           );
index 57e292a25d21599df0a6b813bff19b7c1aa8f2bd..260173f7191f8047b9b7d52244dd05639c3951e9 100644 (file)
@@ -148,7 +148,13 @@ it('should render file-level issue correctly', () => {
     }
   });
 
-  expect(wrapper.find(IssueMessageBox).exists()).toBe(true);
+  expect(
+    wrapper
+      .find('ContextConsumer')
+      .dive()
+      .find(IssueMessageBox)
+      .exists()
+  ).toBe(true);
 });
 
 it('should expand block', async () => {
@@ -294,7 +300,6 @@ function shallowRender(props: Partial<ComponentSourceSnippetGroupViewer['props']
     <ComponentSourceSnippetGroupViewer
       branchLike={mockMainBranch()}
       highlightedLocationMessage={{ index: 0, text: '' }}
-      selectedLocationIndex={0}
       isLastOccurenceOfPrimaryComponent={true}
       issue={mockIssue()}
       issuesByLine={{}}
index 22ba8be364b8d049b3592122928aebf1667c9f36..645a2b9974e1a97376be69f9dd9e148912dc57ef 100644 (file)
@@ -116,7 +116,6 @@ function shallowRender(props: Partial<CrossComponentSourceViewer['props']> = {})
     <CrossComponentSourceViewer
       branchLike={undefined}
       highlightedLocationMessage={undefined}
-      selectedLocationIndex={undefined}
       issue={mockIssue(true, {
         key: '1',
         component: 'project:main.js',
index 2ace6b60664b37f3e050b82f6c2c6d4291251dc4..d7266b3d8fe0de42f0f3cbc93fa2d517d364be45 100644 (file)
@@ -236,6 +236,7 @@ export function allLocationsEmpty(
   return getLocations(issue, selectedFlowIndex).every(location => !location.msg);
 }
 
+// TODO: drop as it's useless now
 export function scrollToIssue(issue: string, smooth = true) {
   const element = document.querySelector(`[data-issue="${issue}"]`);
   if (element) {
index 498b2df1e9a48bbafcd31698ade267ceb9da28da..713c0ea288bd6abb04308b7a452815271dbdc2ae 100644 (file)
@@ -19,6 +19,7 @@
  */
 import classNames from 'classnames';
 import * as React from 'react';
+import { IssueSourceViewerScrollContext } from '../../../apps/issues/components/IssueSourceViewerScrollContext';
 import { LinearIssueLocation, SourceLine } from '../../../types/types';
 import LocationIndex from '../../common/LocationIndex';
 import Tooltip from '../../controls/Tooltip';
@@ -38,35 +39,8 @@ interface Props {
 }
 
 export default class LineCode extends React.PureComponent<React.PropsWithChildren<Props>> {
-  activeMarkerNode?: HTMLElement | null;
   symbols?: NodeListOf<HTMLElement>;
 
-  componentDidMount() {
-    if (this.activeMarkerNode) {
-      this.activeMarkerNode.scrollIntoView({
-        behavior: 'smooth',
-        block: 'center',
-        inline: 'center'
-      });
-    }
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (
-      this.props.highlightedLocationMessage &&
-      (!prevProps.highlightedLocationMessage ||
-        prevProps.highlightedLocationMessage.index !==
-          this.props.highlightedLocationMessage.index) &&
-      this.activeMarkerNode
-    ) {
-      this.activeMarkerNode.scrollIntoView({
-        behavior: 'smooth',
-        block: 'center',
-        inline: 'center'
-      });
-    }
-  }
-
   nodeNodeRef = (el: HTMLElement | null) => {
     if (el) {
       this.attachEvents(el);
@@ -105,7 +79,6 @@ export default class LineCode extends React.PureComponent<React.PropsWithChildre
   renderMarker(index: number, message: string | undefined, selected: boolean, leading: boolean) {
     const { onLocationSelect } = this.props;
     const onClick = onLocationSelect ? () => onLocationSelect(index) : undefined;
-    const ref = selected ? (node: HTMLElement | null) => (this.activeMarkerNode = node) : undefined;
 
     return (
       <Tooltip key={`marker-${index}`} overlay={message} placement="top">
@@ -114,7 +87,13 @@ export default class LineCode extends React.PureComponent<React.PropsWithChildre
           onClick={onClick}
           selected={selected}
           aria-label={message ? `${index + 1}-${message}` : index + 1}>
-          <span ref={ref}>{index + 1}</span>
+          <IssueSourceViewerScrollContext.Consumer>
+            {ctx => (
+              <span ref={selected ? ctx?.registerSelectedSecondaryLocationRef : undefined}>
+                {index + 1}
+              </span>
+            )}
+          </IssueSourceViewerScrollContext.Consumer>
         </LocationIndex>
       </Tooltip>
     );
index fa518fddd0e0f212c411167762547323f471d839..2dcef7709c8a5d7ae95e2178818d0d659bdca60f 100644 (file)
@@ -117,9 +117,9 @@ exports[`render code: with secondary location 1`] = `
           onClick={[Function]}
           selected={false}
         >
-          <span>
-            2
-          </span>
+          <ContextConsumer>
+            <Component />
+          </ContextConsumer>
         </LocationIndex>
       </Tooltip>
       <span
index cceb592fb354a2704114af12a13240264fbc7bfb..e2b86d4edb57314d5931d444af35c8ad645afcb6 100644 (file)
@@ -28,56 +28,30 @@ export interface IssueMessageBoxProps {
   selected: boolean;
   issue: Issue;
   onClick: (issueKey: string) => void;
-  selectedLocationIndex?: number;
 }
 
-export default class IssueMessageBox extends React.Component<IssueMessageBoxProps> {
-  messageBoxRef: React.RefObject<HTMLDivElement> = React.createRef();
+export function IssueMessageBox(props: IssueMessageBoxProps, ref: React.ForwardedRef<any>) {
+  const { issue, selected } = props;
 
-  componentDidMount() {
-    if (this.props.selected && this.messageBoxRef.current) {
-      this.messageBoxRef.current.scrollIntoView({
-        block: 'center'
-      });
-    }
-  }
-
-  componentDidUpdate(prevProps: IssueMessageBoxProps) {
-    if (
-      this.messageBoxRef.current &&
-      ((prevProps.selected !== this.props.selected && this.props.selected) ||
-        (prevProps.selectedLocationIndex !== this.props.selectedLocationIndex &&
-          this.props.selectedLocationIndex === -1))
-    ) {
-      this.messageBoxRef.current.scrollIntoView({
-        block: 'center'
-      });
-    }
-  }
-
-  render() {
-    const { issue, selected } = this.props;
-    return (
-      <div
-        className={classNames(
-          'issue-message-box display-flex-row display-flex-center padded-right',
-          {
-            'selected big-padded-top big-padded-bottom': selected,
-            'secondary-issue padded-top padded-bottom': !selected
-          }
-        )}
-        key={issue.key}
-        onClick={() => this.props.onClick(issue.key)}
-        role="region"
-        ref={this.messageBoxRef}
-        aria-label={issue.message}>
-        <IssueTypeIcon
-          className="big-spacer-right spacer-left"
-          fill={colors.baseFontColor}
-          query={issue.type}
-        />
-        {issue.message}
-      </div>
-    );
-  }
+  return (
+    <div
+      className={classNames('issue-message-box display-flex-row display-flex-center padded-right', {
+        'selected big-padded-top big-padded-bottom': selected,
+        'secondary-issue padded-top padded-bottom': !selected
+      })}
+      key={issue.key}
+      onClick={() => props.onClick(issue.key)}
+      role="region"
+      ref={ref}
+      aria-label={issue.message}>
+      <IssueTypeIcon
+        className="big-spacer-right spacer-left"
+        fill={colors.baseFontColor}
+        query={issue.type}
+      />
+      {issue.message}
+    </div>
+  );
 }
+
+export default React.forwardRef(IssueMessageBox);
index f4b9cdf4f1dabda8622b61bba9047faf28fe4608..4adc421d9e307c240b01c573cb1fea0abefe39cd 100644 (file)
@@ -119,7 +119,7 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
     this.detachScrollEvent();
   }
 
-  computeState = (prevState: State, resetSelectedTab: boolean = false) => {
+  computeState = (prevState: State, resetSelectedTab = false) => {
     const {
       ruleDetails,
       currentUser: { isLoggedIn, dismissedNotices }