]> source.dussan.org Git - sonarqube.git/commitdiff
Fix multiple Code Smells
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 7 Sep 2021 13:44:58 +0000 (15:44 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 13 Sep 2021 20:03:33 +0000 (20:03 +0000)
32 files changed:
server/sonar-web/src/main/js/components/charts/BubbleChart.tsx
server/sonar-web/src/main/js/components/charts/Histogram.tsx
server/sonar-web/src/main/js/components/charts/ZoomTimeLine.tsx
server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx
server/sonar-web/src/main/js/components/charts/__tests__/Histogram-test.tsx
server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap
server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/Histogram-test.tsx.snap
server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx
server/sonar-web/src/main/js/components/controls/Dropdown.tsx
server/sonar-web/src/main/js/components/controls/GlobalMessages.tsx
server/sonar-web/src/main/js/components/controls/InputValidationField.tsx
server/sonar-web/src/main/js/components/controls/RadioToggle.tsx
server/sonar-web/src/main/js/components/controls/ScreenPositionFixer.tsx
server/sonar-web/src/main/js/components/controls/SearchBox.tsx
server/sonar-web/src/main/js/components/controls/Select.tsx
server/sonar-web/src/main/js/components/controls/Tooltip.css
server/sonar-web/src/main/js/components/controls/Tooltip.tsx
server/sonar-web/src/main/js/components/controls/__tests__/Select-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Select-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/buttons.tsx
server/sonar-web/src/main/js/components/icons/OnboardingAddMembersIcon.tsx [deleted file]
server/sonar-web/src/main/js/components/icons/OnboardingProjectIcon.tsx [deleted file]
server/sonar-web/src/main/js/components/icons/OnboardingTeamIcon.tsx [deleted file]
server/sonar-web/src/main/js/components/icons/SeverityIcon.tsx
server/sonar-web/src/main/js/components/icons/StatusIcon.tsx
server/sonar-web/src/main/js/components/icons/TestStatusIcon.tsx
server/sonar-web/src/main/js/components/ui/Alert.tsx
server/sonar-web/src/main/js/components/ui/ContextNavBar.tsx
server/sonar-web/src/main/js/components/ui/NavBar.css
server/sonar-web/src/main/js/components/ui/NavBarTabs.tsx
server/sonar-web/src/main/js/helpers/mocks/dom.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/pages.ts

index 75d3172ea809c6baac3b6592e0af4ff0f1a7800e..f71f476a50aa30fb88c769fe6d8f65ab0713f7b9 100644 (file)
@@ -102,7 +102,6 @@ export default class BubbleChart<T> extends React.PureComponent<Props<T>, State>
     this.zoom = zoom()
       .scaleExtent([1, 10])
       .on('zoom', this.zoomed);
-    // TODO: Check why as any cast is necessary now.
     select(this.node).call(this.zoom as any);
   };
 
@@ -118,11 +117,10 @@ export default class BubbleChart<T> extends React.PureComponent<Props<T>, State>
     });
   };
 
-  resetZoom = (event: React.MouseEvent<Link>) => {
-    event.stopPropagation();
-    event.preventDefault();
+  resetZoom = (e: React.MouseEvent<Link>) => {
+    e.stopPropagation();
+    e.preventDefault();
     if (this.zoom && this.node) {
-      // TODO: Check why as any cast is necessary now.
       select(this.node).call(this.zoom.transform as any, zoomIdentity);
     }
   };
@@ -144,11 +142,11 @@ export default class BubbleChart<T> extends React.PureComponent<Props<T>, State>
   }
 
   getTicks(scale: Scale, format: (d: number) => string) {
-    const zoom = Math.ceil(this.state.transform.k);
-    const ticks = scale.ticks(TICKS_COUNT * zoom).map(tick => format(tick));
+    const zoomAmount = Math.ceil(this.state.transform.k);
+    const ticks = scale.ticks(TICKS_COUNT * zoomAmount).map(tick => format(tick));
     const uniqueTicksCount = uniq(ticks).length;
     const ticksCount =
-      uniqueTicksCount < TICKS_COUNT * zoom ? uniqueTicksCount - 1 : TICKS_COUNT * zoom;
+      uniqueTicksCount < TICKS_COUNT * zoomAmount ? uniqueTicksCount - 1 : TICKS_COUNT * zoomAmount;
     return scale.ticks(ticksCount);
   }
 
@@ -360,10 +358,10 @@ interface BubbleProps<T> {
 }
 
 function Bubble<T>(props: BubbleProps<T>) {
-  const handleClick = (event: React.MouseEvent<SVGCircleElement>) => {
+  const handleClick = (e: React.MouseEvent<SVGCircleElement>) => {
     if (props.onClick) {
-      event.stopPropagation();
-      event.preventDefault();
+      e.stopPropagation();
+      e.preventDefault();
       props.onClick(props.data);
     }
   };
index a4ae509a2efd4060543a7e59461645184a39aaeb..08b0f30e94851b6f13b9f173ea4e489ff2bb08ef 100644 (file)
@@ -47,7 +47,7 @@ export default class Histogram extends React.PureComponent<Props> {
 
     const width = Math.round(xScale(d)) + /* minimum bar width */ 1;
     const x = xScale.range()[0] + (alignTicks ? padding[3] : 0);
-    const y = Math.round(yScale(index)! + yScale.bandwidth() / 2);
+    const y = Math.round((yScale(index) || 0) + yScale.bandwidth() / 2);
 
     return <rect className="bar-chart-bar" height={BAR_HEIGHT} width={width} x={x} y={y} />;
   }
@@ -62,7 +62,7 @@ export default class Histogram extends React.PureComponent<Props> {
     }
 
     const x = xScale(d) + (alignTicks ? padding[3] : 0);
-    const y = Math.round(yScale(index)! + yScale.bandwidth() / 2 + BAR_HEIGHT / 2);
+    const y = Math.round((yScale(index) || 0) + yScale.bandwidth() / 2 + BAR_HEIGHT / 2);
 
     return (
       <Tooltip overlay={this.props.yTooltips && this.props.yTooltips[index]}>
@@ -83,7 +83,7 @@ export default class Histogram extends React.PureComponent<Props> {
     }
 
     const x = xScale.range()[0];
-    const y = Math.round(yScale(index)! + yScale.bandwidth() / 2 + BAR_HEIGHT / 2);
+    const y = Math.round((yScale(index) || 0) + yScale.bandwidth() / 2 + BAR_HEIGHT / 2);
     const historyTickClass = alignTicks ? 'histogram-tick-start' : 'histogram-tick';
 
     return (
index f4f2742eb7573bb08dc737d80bff427ddb9f81c1..7caee5b3aec5f401e149fe43084c09ca55210bd9 100644 (file)
@@ -49,7 +49,10 @@ interface State {
 }
 
 type XScale = ScaleTime<number, number>;
-// TODO it should be `ScaleLinear<number, number> | ScalePoint<number> | ScalePoint<string>`, but it's super hard to make it work :'(
+// It should be `ScaleLinear<number, number> | ScalePoint<number> | ScalePoint<string>`, but in order
+// to make it work, we need to write a lot of type guards :-(. This introduces a lot of unnecessary code,
+// not to mention overhead at runtime. The simplest is just to cast to any, and rely on D3's internals
+// to make it work.
 type YScale = any;
 
 export default class ZoomTimeLine extends React.PureComponent<Props, State> {
@@ -114,7 +117,7 @@ export default class ZoomTimeLine extends React.PureComponent<Props, State> {
     this.handleZoomUpdate(xScale, xDim);
   };
 
-  handleSelectionDrag = (xScale: XScale, width: number, xDim: number[], checkDelta?: boolean) => (
+  handleSelectionDrag = (xScale: XScale, width: number, xDim: number[], checkDelta = false) => (
     _: MouseEvent,
     data: DraggableData
   ) => {
@@ -129,7 +132,7 @@ export default class ZoomTimeLine extends React.PureComponent<Props, State> {
     fixedX: number,
     xDim: number[],
     handleDirection: string,
-    checkDelta?: boolean
+    checkDelta = false
   ) => (_: MouseEvent, data: DraggableData) => {
     if (!checkDelta || data.deltaX) {
       const x = Math.max(xDim[0], Math.min(data.x, xDim[1]));
index 3be36c3677d003b844a7de139c67bbb667a8b16c..967b075314f7a4585775a3be345ecfa45bdf4d40 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { mount } from 'enzyme';
+import { select } from 'd3-selection';
+import { zoom } from 'd3-zoom';
+import { shallow } from 'enzyme';
 import * as React from 'react';
-import { AutoSizerProps } from 'react-virtualized';
+import { Link } from 'react-router';
+import { AutoSizer, AutoSizerProps } from 'react-virtualized/dist/commonjs/AutoSizer';
+import { mockComponentMeasureEnhanced } from '../../../helpers/mocks/component';
+import { mockHtmlElement } from '../../../helpers/mocks/dom';
+import { mockEvent } from '../../../helpers/testMocks';
+import { click } from '../../../helpers/testUtils';
 import BubbleChart from '../BubbleChart';
 
 jest.mock('react-virtualized/dist/commonjs/AutoSizer', () => ({
   AutoSizer: ({ children }: AutoSizerProps) => children({ width: 100, height: NaN })
 }));
 
+jest.mock('d3-selection', () => ({
+  event: { transform: { x: 10, y: 10, k: 20 } },
+  select: jest.fn().mockReturnValue({ call: jest.fn() })
+}));
+
+jest.mock('d3-zoom', () => ({
+  ...jest.requireActual('d3-zoom'),
+  zoom: jest.fn()
+}));
+
+beforeEach(jest.clearAllMocks);
+
 it('should display bubbles', () => {
-  const items = [
-    { x: 1, y: 10, size: 7 },
-    { x: 2, y: 30, size: 5 }
-  ];
-  const chart = mount(<BubbleChart height={100} items={items} padding={[0, 0, 0, 0]} />);
-  chart.find('Bubble').forEach(bubble => expect(bubble).toMatchSnapshot());
-
-  chart.setProps({ height: 120 });
+  const wrapper = shallowRender();
+  wrapper
+    .find(AutoSizer)
+    .dive()
+    .find('Bubble')
+    .forEach(bubble => {
+      expect(bubble.dive()).toMatchSnapshot();
+    });
 });
 
 it('should render bubble links', () => {
-  const items = [
-    { x: 1, y: 10, size: 7, link: 'foo' },
-    { x: 2, y: 30, size: 5, link: 'bar' }
-  ];
-  const chart = mount(<BubbleChart height={100} items={items} padding={[0, 0, 0, 0]} />);
-  chart.find('Bubble').forEach(bubble => expect(bubble).toMatchSnapshot());
+  const wrapper = shallowRender({
+    items: [
+      { x: 1, y: 10, size: 7, link: 'foo' },
+      { x: 2, y: 30, size: 5, link: 'bar' }
+    ]
+  });
+  wrapper
+    .find(AutoSizer)
+    .dive()
+    .find('Bubble')
+    .forEach(bubble => {
+      expect(bubble.dive()).toMatchSnapshot();
+    });
 });
 
 it('should render bubbles with click handlers', () => {
-  const onClick = jest.fn();
-  const items = [
-    { x: 1, y: 10, size: 7, data: 'foo' },
-    { x: 2, y: 30, size: 5, data: 'bar' }
-  ];
-  const chart = mount(
-    <BubbleChart height={100} items={items} onBubbleClick={onClick} padding={[0, 0, 0, 0]} />
-  );
-  chart.find('Bubble').forEach(bubble => expect(bubble).toMatchSnapshot());
+  const onBubbleClick = jest.fn();
+  const wrapper = shallowRender({ onBubbleClick });
+  wrapper
+    .find(AutoSizer)
+    .dive()
+    .find('Bubble')
+    .forEach(bubble => {
+      click(bubble.dive().find('circle'));
+      expect(bubble.dive()).toMatchSnapshot();
+    });
+  expect(onBubbleClick).toBeCalledTimes(2);
+  expect(onBubbleClick).toHaveBeenLastCalledWith(mockComponentMeasureEnhanced());
+});
+
+it('should correctly handle zooming', () => {
+  class ZoomBehaviorMock {
+    on = () => this;
+    scaleExtent = () => this;
+    translateExtent = () => this;
+  }
+
+  const call = jest.fn();
+  const zoomBehavior = new ZoomBehaviorMock();
+  (select as jest.Mock).mockReturnValueOnce({ call });
+  (zoom as jest.Mock).mockReturnValueOnce(zoomBehavior);
+
+  return new Promise<void>((resolve, reject) => {
+    const wrapper = shallowRender({ padding: [5, 5, 5, 5] });
+    wrapper.instance().boundNode(
+      mockHtmlElement<SVGSVGElement>({
+        getBoundingClientRect: () => ({ width: 100, height: 100 } as DOMRect)
+      })
+    );
+
+    // Call zoom event handler.
+    wrapper.instance().zoomed();
+    expect(wrapper.state().transform).toEqual({
+      x: 105,
+      y: 105,
+      k: 20
+    });
+
+    // Reset Zoom levels.
+    const resetZoomClick = wrapper
+      .find('div.bubble-chart-zoom')
+      .find(Link)
+      .props().onClick;
+    if (!resetZoomClick) {
+      reject();
+      return;
+    }
+
+    const stopPropagation = jest.fn();
+    const preventDefault = jest.fn();
+    resetZoomClick(mockEvent({ stopPropagation, preventDefault }));
+    expect(stopPropagation).toBeCalled();
+    expect(preventDefault).toBeCalled();
+    expect(call).toHaveBeenCalledWith(zoomBehavior);
+
+    resolve();
+  });
 });
+
+function shallowRender(props: Partial<BubbleChart<T.ComponentMeasureEnhanced>['props']> = {}) {
+  return shallow<BubbleChart<T.ComponentMeasureEnhanced>>(
+    <BubbleChart
+      height={100}
+      items={[
+        { x: 1, y: 10, size: 7, data: mockComponentMeasureEnhanced() },
+        { x: 2, y: 30, size: 5, data: mockComponentMeasureEnhanced() }
+      ]}
+      padding={[0, 0, 0, 0]}
+      {...props}
+    />
+  );
+}
index 15f4754aae06466363dafeb0df09b6146a6167ad..2f67d166cfdabcf34a33973b036cc10df11f8ef0 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { scaleBand } from 'd3-scale';
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import Histogram from '../Histogram';
 
-it('renders', () => {
-  expect(shallow(<Histogram bars={[100, 75, 150]} height={75} width={100} />)).toMatchSnapshot();
+jest.mock('d3-scale', () => {
+  const d3 = jest.requireActual('d3-scale');
+  return {
+    ...d3,
+    scaleBand: jest.fn(d3.scaleBand)
+  };
 });
 
-it('renders with yValues', () => {
-  expect(
-    shallow(
-      <Histogram
-        bars={[100, 75, 150]}
-        height={75}
-        width={100}
-        yValues={['100.0', '75.0', '150.0']}
-      />
-    )
-  ).toMatchSnapshot();
-});
+beforeEach(jest.clearAllMocks);
 
-it('renders with yValues and yTicks', () => {
+it('renders correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(shallowRender({ alignTicks: true })).toMatchSnapshot('align ticks');
+  expect(shallowRender({ yValues: ['100.0', '75.0', '150.0'] })).toMatchSnapshot('with yValues');
   expect(
-    shallow(
-      <Histogram
-        bars={[100, 75, 150]}
-        height={75}
-        width={100}
-        yTicks={['a', 'b', 'c']}
-        yValues={['100.0', '75.0', '150.0']}
-      />
-    )
-  ).toMatchSnapshot();
+    shallowRender({ yTicks: ['a', 'b', 'c'], yValues: ['100.0', '75.0', '150.0'] })
+  ).toMatchSnapshot('with yValues and yTicks');
+  expect(
+    shallowRender({
+      yTicks: ['a', 'b', 'c'],
+      yTooltips: ['a - 100', 'b - 75', 'c - 150'],
+      yValues: ['100.0', '75.0', '150.0']
+    })
+  ).toMatchSnapshot('with yValues, yTicks and yTooltips');
 });
 
-it('renders with yValues, yTicks and yTooltips', () => {
+it('correctly handles yScale() returning undefined', () => {
+  const yScale = () => undefined;
+  yScale.bandwidth = () => 1;
+
+  (scaleBand as jest.Mock).mockReturnValueOnce({
+    domain: () => ({ rangeRound: () => yScale })
+  });
+
   expect(
-    shallow(
-      <Histogram
-        bars={[100, 75, 150]}
-        height={75}
-        width={100}
-        yTicks={['a', 'b', 'c']}
-        yTooltips={['a - 100', 'b - 75', 'c - 150']}
-        yValues={['100.0', '75.0', '150.0']}
-      />
-    )
+    shallowRender({ yValues: ['100.0', '75.0', '150.0'], yTicks: ['a', 'b', 'c'] })
   ).toMatchSnapshot();
 });
+
+function shallowRender(props: Partial<Histogram['props']> = {}) {
+  return shallow<Histogram>(<Histogram bars={[100, 75, 150]} height={75} width={100} {...props} />);
+}
index b0f69f95417677dde00d528beb1b5bce9da4aba2..8adfc888cd0d3be48bddd0f7e87aacdd0432b1bf 100644 (file)
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`should display bubbles 1`] = `
-<Bubble
-  key="0"
-  r={45}
-  scale={1}
-  x={33.21428571428571}
-  y={70.07936507936509}
->
-  <Tooltip>
-    <g>
-      <circle
-        className="bubble-chart-bubble"
-        r={45}
-        style={
-          Object {
-            "fill": undefined,
-            "stroke": undefined,
-          }
+<Tooltip>
+  <g>
+    <circle
+      className="bubble-chart-bubble"
+      r={45}
+      style={
+        Object {
+          "fill": undefined,
+          "stroke": undefined,
         }
-        transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
-      />
-    </g>
-  </Tooltip>
-</Bubble>
+      }
+      transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
+    />
+  </g>
+</Tooltip>
 `;
 
 exports[`should display bubbles 2`] = `
-<Bubble
-  key="1"
-  r={33.57142857142858}
-  scale={1}
-  x={66.42857142857142}
-  y={33.57142857142858}
->
-  <Tooltip>
-    <g>
-      <circle
-        className="bubble-chart-bubble"
-        r={33.57142857142858}
-        style={
-          Object {
-            "fill": undefined,
-            "stroke": undefined,
-          }
+<Tooltip>
+  <g>
+    <circle
+      className="bubble-chart-bubble"
+      r={33.57142857142858}
+      style={
+        Object {
+          "fill": undefined,
+          "stroke": undefined,
         }
-        transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
-      />
-    </g>
-  </Tooltip>
-</Bubble>
+      }
+      transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
+    />
+  </g>
+</Tooltip>
 `;
 
 exports[`should render bubble links 1`] = `
-<Bubble
-  key="0"
-  link="foo"
-  r={45}
-  scale={1}
-  x={33.21428571428571}
-  y={70.07936507936509}
->
-  <Tooltip>
-    <g>
-      <Link
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="foo"
-      >
-        <a
-          onClick={[Function]}
-          style={Object {}}
-        >
-          <circle
-            className="bubble-chart-bubble"
-            r={45}
-            style={
-              Object {
-                "fill": undefined,
-                "stroke": undefined,
-              }
-            }
-            transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
-          />
-        </a>
-      </Link>
-    </g>
-  </Tooltip>
-</Bubble>
-`;
-
-exports[`should render bubble links 2`] = `
-<Bubble
-  key="1"
-  link="bar"
-  r={33.57142857142858}
-  scale={1}
-  x={66.42857142857142}
-  y={33.57142857142858}
->
-  <Tooltip>
-    <g>
-      <Link
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="bar"
-      >
-        <a
-          onClick={[Function]}
-          style={Object {}}
-        >
-          <circle
-            className="bubble-chart-bubble"
-            r={33.57142857142858}
-            style={
-              Object {
-                "fill": undefined,
-                "stroke": undefined,
-              }
-            }
-            transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
-          />
-        </a>
-      </Link>
-    </g>
-  </Tooltip>
-</Bubble>
-`;
-
-exports[`should render bubbles with click handlers 1`] = `
-<Bubble
-  data="foo"
-  key="0"
-  onClick={[MockFunction]}
-  r={45}
-  scale={1}
-  x={33.21428571428571}
-  y={70.07936507936509}
->
-  <Tooltip>
-    <g>
+<Tooltip>
+  <g>
+    <Link
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to="foo"
+    >
       <circle
         className="bubble-chart-bubble"
-        onClick={[Function]}
         r={45}
         style={
           Object {
@@ -152,26 +55,21 @@ exports[`should render bubbles with click handlers 1`] = `
         }
         transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
       />
-    </g>
-  </Tooltip>
-</Bubble>
+    </Link>
+  </g>
+</Tooltip>
 `;
 
-exports[`should render bubbles with click handlers 2`] = `
-<Bubble
-  data="bar"
-  key="1"
-  onClick={[MockFunction]}
-  r={33.57142857142858}
-  scale={1}
-  x={66.42857142857142}
-  y={33.57142857142858}
->
-  <Tooltip>
-    <g>
+exports[`should render bubble links 2`] = `
+<Tooltip>
+  <g>
+    <Link
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to="bar"
+    >
       <circle
         className="bubble-chart-bubble"
-        onClick={[Function]}
         r={33.57142857142858}
         style={
           Object {
@@ -181,7 +79,45 @@ exports[`should render bubbles with click handlers 2`] = `
         }
         transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
       />
-    </g>
-  </Tooltip>
-</Bubble>
+    </Link>
+  </g>
+</Tooltip>
+`;
+
+exports[`should render bubbles with click handlers 1`] = `
+<Tooltip>
+  <g>
+    <circle
+      className="bubble-chart-bubble"
+      onClick={[Function]}
+      r={45}
+      style={
+        Object {
+          "fill": undefined,
+          "stroke": undefined,
+        }
+      }
+      transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
+    />
+  </g>
+</Tooltip>
+`;
+
+exports[`should render bubbles with click handlers 2`] = `
+<Tooltip>
+  <g>
+    <circle
+      className="bubble-chart-bubble"
+      onClick={[Function]}
+      r={33.57142857142858}
+      style={
+        Object {
+          "fill": undefined,
+          "stroke": undefined,
+        }
+      }
+      transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
+    />
+  </g>
+</Tooltip>
 `;
index f52a4ea6cf340f618a0c40d104148e48ac686c4c..9f2ac323b09abbf2108f24570e22232138193c51 100644 (file)
@@ -1,6 +1,162 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`renders 1`] = `
+exports[`correctly handles yScale() returning undefined 1`] = `
+<svg
+  className="bar-chart"
+  height={75}
+  width={100}
+>
+  <g
+    transform="translate(10, 10)"
+  >
+    <g>
+      <g
+        key="0"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={54}
+          x={0}
+          y={1}
+        />
+        <Tooltip>
+          <text
+            className="bar-chart-tick histogram-value"
+            dx="1em"
+            dy="0.3em"
+            x={53.33333333333333}
+            y={6}
+          >
+            100.0
+          </text>
+        </Tooltip>
+        <text
+          className="bar-chart-tick histogram-tick"
+          dx="-1em"
+          dy="0.3em"
+          x={0}
+          y={6}
+        >
+          a
+        </text>
+      </g>
+      <g
+        key="1"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={41}
+          x={0}
+          y={1}
+        />
+        <Tooltip>
+          <text
+            className="bar-chart-tick histogram-value"
+            dx="1em"
+            dy="0.3em"
+            x={40}
+            y={6}
+          >
+            75.0
+          </text>
+        </Tooltip>
+        <text
+          className="bar-chart-tick histogram-tick"
+          dx="-1em"
+          dy="0.3em"
+          x={0}
+          y={6}
+        >
+          b
+        </text>
+      </g>
+      <g
+        key="2"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={81}
+          x={0}
+          y={1}
+        />
+        <Tooltip>
+          <text
+            className="bar-chart-tick histogram-value"
+            dx="1em"
+            dy="0.3em"
+            x={80}
+            y={6}
+          >
+            150.0
+          </text>
+        </Tooltip>
+        <text
+          className="bar-chart-tick histogram-tick"
+          dx="-1em"
+          dy="0.3em"
+          x={0}
+          y={6}
+        >
+          c
+        </text>
+      </g>
+    </g>
+  </g>
+</svg>
+`;
+
+exports[`renders correctly: align ticks 1`] = `
+<svg
+  className="bar-chart"
+  height={75}
+  width={100}
+>
+  <g
+    transform="translate(4, 10)"
+  >
+    <g>
+      <g
+        key="0"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={54}
+          x={10}
+          y={10}
+        />
+      </g>
+      <g
+        key="1"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={41}
+          x={10}
+          y={28}
+        />
+      </g>
+      <g
+        key="2"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={81}
+          x={10}
+          y={46}
+        />
+      </g>
+    </g>
+  </g>
+</svg>
+`;
+
+exports[`renders correctly: default 1`] = `
 <svg
   className="bar-chart"
   height={75}
@@ -48,7 +204,7 @@ exports[`renders 1`] = `
 </svg>
 `;
 
-exports[`renders with yValues 1`] = `
+exports[`renders correctly: with yValues 1`] = `
 <svg
   className="bar-chart"
   height={75}
@@ -129,7 +285,7 @@ exports[`renders with yValues 1`] = `
 </svg>
 `;
 
-exports[`renders with yValues and yTicks 1`] = `
+exports[`renders correctly: with yValues and yTicks 1`] = `
 <svg
   className="bar-chart"
   height={75}
@@ -237,7 +393,7 @@ exports[`renders with yValues and yTicks 1`] = `
 </svg>
 `;
 
-exports[`renders with yValues, yTicks and yTooltips 1`] = `
+exports[`renders correctly: with yValues, yTicks and yTooltips 1`] = `
 <svg
   className="bar-chart"
   height={75}
index 61bf510c57d070ba217aad24383fa8c657f593c0..0ab285a3391d767d3d83a9b503fbbb350aad281a 100644 (file)
@@ -54,7 +54,9 @@ export default class ConfirmModal<T = string> extends React.PureComponent<Props<
   handleSubmit = () => {
     const result = this.props.onConfirm(this.props.confirmData);
     if (result) {
-      return result.then(this.props.onClose, () => {});
+      return result.then(this.props.onClose, () => {
+        /* noop */
+      });
     }
     this.props.onClose();
     return undefined;
index 92b1444aeb74ba62bc4ee4b3e396a0b9300b3921..04f2b3c82088a4f084d68b39473c440db4f89790 100644 (file)
@@ -119,9 +119,6 @@ interface OverlayProps {
   placement?: PopupPlacement;
 }
 
-// TODO use the same styling for <Select />
-// TODO use the same styling for <DateInput />
-
 export class DropdownOverlay extends React.Component<OverlayProps> {
   get placement() {
     return this.props.placement || PopupPlacement.Bottom;
index b2d5b1d21dd82d430f1d65868bea9f1ae7e0cdf8..5a68bf9d649f7316890701c970a771be3695073c 100644 (file)
@@ -24,7 +24,7 @@ import { colors, sizes, zIndexes } from '../../app/theme';
 import { cutLongWords } from '../../helpers/path';
 import { ClearButton } from './buttons';
 
-interface Message {
+interface IMessage {
   id: string;
   level: 'ERROR' | 'SUCCESS';
   message: string;
@@ -32,7 +32,7 @@ interface Message {
 
 export interface GlobalMessagesProps {
   closeGlobalMessage: (id: string) => void;
-  messages: Message[];
+  messages: IMessage[];
 }
 
 export default function GlobalMessages({ closeGlobalMessage, messages }: GlobalMessagesProps) {
@@ -60,7 +60,7 @@ const MessagesContainer = styled.div`
 
 export class GlobalMessage extends React.PureComponent<{
   closeGlobalMessage: (id: string) => void;
-  message: Message;
+  message: IMessage;
 }> {
   handleClose = () => {
     this.props.closeGlobalMessage(this.props.message.id);
@@ -94,7 +94,7 @@ const appearAnim = keyframes`
   }
 `;
 
-const Message = styled.div<Pick<Message, 'level'>>`
+const Message = styled.div<Pick<IMessage, 'level'>>`
   position: relative;
   padding: 0 30px 0 10px;
   line-height: ${sizes.controlHeight};
@@ -112,7 +112,7 @@ const Message = styled.div<Pick<Message, 'level'>>`
   }
 `;
 
-const CloseButton = styled(ClearButton)<Pick<Message, 'level'>>`
+const CloseButton = styled(ClearButton)<Pick<IMessage, 'level'>>`
   position: absolute;
   top: calc(${sizes.gridSize} / 4);
   right: calc(${sizes.gridSize} / 4);
index bb9cfcab90f0d5f39b0df555c06d03831ebaa8be..0e844b113d22a4ab3175f7b569744483e2b8c758 100644 (file)
@@ -31,8 +31,8 @@ interface Props {
   id?: string;
   label?: React.ReactNode;
   name: string;
-  onBlur: (event: React.FocusEvent<any>) => void;
-  onChange: (event: React.ChangeEvent<any>) => void;
+  onBlur: (event: React.FocusEvent<HTMLInputElement>) => void;
+  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
   placeholder?: string;
   touched: boolean | undefined;
   type?: string;
index 2ccf856ce302b533fe2e2c27109166f3da165881..0af42bbca83cb19f0655603bb3b3b873971e4600 100644 (file)
@@ -46,7 +46,7 @@ export default class RadioToggle extends React.PureComponent<Props> {
 
   renderOption = (option: Option) => {
     const checked = option.value === this.props.value;
-    const htmlId = this.props.name + '__' + option.value;
+    const htmlId = `${this.props.name}__${option.value}`;
     return (
       <li key={option.value.toString()}>
         <input
index 6aa05387ac53734fea7dcd39aba9c337de2e5e95..8c24f24a73015f06587b3b2180766a5f5bbe72c8 100644 (file)
@@ -43,6 +43,8 @@ interface Fixes {
   topFix?: number;
 }
 
+const EDGE_MARGIN = rawSizes.grid / 2;
+
 export default class ScreenPositionFixer extends React.Component<Props, Fixes> {
   throttledPosition: () => void;
 
@@ -82,8 +84,6 @@ export default class ScreenPositionFixer extends React.Component<Props, Fixes> {
   };
 
   position = () => {
-    const edgeMargin = 0.5 * rawSizes.grid;
-
     // eslint-disable-next-line react/no-find-dom-node
     const node = findDOMNode(this);
     if (node && node instanceof Element) {
@@ -91,17 +91,17 @@ export default class ScreenPositionFixer extends React.Component<Props, Fixes> {
       const { clientHeight, clientWidth } = document.documentElement;
 
       let leftFix = 0;
-      if (left < edgeMargin) {
-        leftFix = edgeMargin - left;
-      } else if (left + width > clientWidth - edgeMargin) {
-        leftFix = clientWidth - edgeMargin - left - width;
+      if (left < EDGE_MARGIN) {
+        leftFix = EDGE_MARGIN - left;
+      } else if (left + width > clientWidth - EDGE_MARGIN) {
+        leftFix = clientWidth - EDGE_MARGIN - left - width;
       }
 
       let topFix = 0;
-      if (top < edgeMargin) {
-        topFix = edgeMargin - top;
-      } else if (top + height > clientHeight - edgeMargin) {
-        topFix = clientHeight - edgeMargin - top - height;
+      if (top < EDGE_MARGIN) {
+        topFix = EDGE_MARGIN - top;
+      } else if (top + height > clientHeight - EDGE_MARGIN) {
+        topFix = clientHeight - EDGE_MARGIN - top - height;
       }
 
       this.setState({ leftFix, topFix });
index d03081e26bc002002561137f5a2aca286e6eeeb0..476b8f93b7ec39d701f9d45fbdb0811ea198f1c0 100644 (file)
@@ -135,7 +135,11 @@ export default class SearchBox extends React.PureComponent<Props, State> {
       <div
         className={classNames('search-box', this.props.className)}
         id={this.props.id}
-        title={tooShort ? translateWithParameters('select2.tooShort', minLength!) : ''}>
+        title={
+          tooShort && minLength !== undefined
+            ? translateWithParameters('select2.tooShort', minLength)
+            : ''
+        }>
         <input
           aria-label={translate('search_verb')}
           autoComplete="off"
@@ -165,9 +169,9 @@ export default class SearchBox extends React.PureComponent<Props, State> {
           />
         )}
 
-        {tooShort && (
+        {tooShort && minLength !== undefined && (
           <span className="search-box-note">
-            {translateWithParameters('select2.tooShort', minLength!)}
+            {translateWithParameters('select2.tooShort', minLength)}
           </span>
         )}
       </div>
index 44657a879911b560a657e35f31ddd5be50d2beb4..cc345e12677b923c06581f1dad9fb131d9d74ad6 100644 (file)
@@ -18,8 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import {
-  defaultFilterOptions as reactSelectDefaultFilterOptions,
+import ReactSelectClass, {
   ReactAsyncSelectProps,
   ReactCreatableSelectProps,
   ReactSelectProps
@@ -28,10 +27,6 @@ import { lazyLoadComponent } from '../lazyLoadComponent';
 import { ClearButton } from './buttons';
 import './Select.css';
 
-declare module 'react-select' {
-  export function defaultFilterOptions(...args: any[]): any;
-}
-
 const ReactSelectLib = import('react-select');
 const ReactSelect = lazyLoadComponent(() => ReactSelectLib);
 const ReactCreatable = lazyLoadComponent(() =>
@@ -43,30 +38,22 @@ function renderInput() {
   return <ClearButton className="button-tiny spacer-left text-middle" iconProps={{ size: 12 }} />;
 }
 
-interface WithInnerRef {
-  innerRef?: (element: React.Component) => void;
+export interface WithInnerRef {
+  innerRef?: React.Ref<ReactSelectClass<unknown>>;
 }
 
 export default function Select({ innerRef, ...props }: WithInnerRef & ReactSelectProps) {
-  // TODO try to define good defaults, if any
-  // ReactSelect doesn't declare `clearRenderer` prop
-  const ReactSelectAny = ReactSelect as any;
   // hide the "x" icon when select is empty
   const clearable = props.clearable ? Boolean(props.value) : false;
   return (
-    <ReactSelectAny {...props} clearable={clearable} clearRenderer={renderInput} ref={innerRef} />
+    <ReactSelect {...props} clearable={clearable} clearRenderer={renderInput} ref={innerRef} />
   );
 }
 
-export const defaultFilterOptions = reactSelectDefaultFilterOptions;
-
 export function Creatable(props: ReactCreatableSelectProps) {
-  // ReactSelect doesn't declare `clearRenderer` prop
-  const ReactCreatableAny = ReactCreatable as any;
-  return <ReactCreatableAny {...props} clearRenderer={renderInput} />;
+  return <ReactCreatable {...props} clearRenderer={renderInput} />;
 }
 
-// TODO figure out why `ref` prop is incompatible
-export function AsyncSelect(props: ReactAsyncSelectProps & { ref?: any }) {
+export function AsyncSelect(props: ReactAsyncSelectProps & WithInnerRef) {
   return <ReactAsync {...props} />;
 }
index d49494f1f77ce2bd20dfd4b0e9ed26988ae25441..3b03e42379270477a8d52bb25907cd853ab99aa9 100644 (file)
@@ -56,9 +56,6 @@
   border-radius: 4px;
   overflow: hidden;
   word-break: break-word;
-}
-
-.tooltip-inner {
   padding: 12px 17px;
   color: #fff;
   background-color: #475760;
index 802541ad1bcbe64f82de891e4c7d48d9283d6a1c..79facbd79451fd096b3cabd56ae1f9cca7348adf 100644 (file)
@@ -334,7 +334,7 @@ export class TooltipInner extends React.Component<TooltipProps, State> {
     }
 
     const { classNameSpace = 'tooltip' } = this.props;
-    const placement = this.getPlacement();
+    const currentPlacement = this.getPlacement();
     const style = isMeasured(this.state)
       ? {
           left: this.state.left + leftFix,
@@ -346,7 +346,7 @@ export class TooltipInner extends React.Component<TooltipProps, State> {
 
     return (
       <div
-        className={`${classNameSpace} ${placement}`}
+        className={`${classNameSpace} ${currentPlacement}`}
         onMouseEnter={this.handleOverlayMouseEnter}
         onMouseLeave={this.handleOverlayMouseLeave}
         ref={this.tooltipNodeRef}
@@ -356,7 +356,7 @@ export class TooltipInner extends React.Component<TooltipProps, State> {
           className={`${classNameSpace}-arrow`}
           style={
             isMeasured(this.state)
-              ? this.adjustArrowPosition(placement, { leftFix, topFix })
+              ? this.adjustArrowPosition(currentPlacement, { leftFix, topFix })
               : undefined
           }
         />
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/Select-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/Select-test.tsx
new file mode 100644 (file)
index 0000000..2b3bd98
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { ReactAsyncSelectProps, ReactCreatableSelectProps, ReactSelectProps } from 'react-select';
+import Select, { AsyncSelect, Creatable, WithInnerRef } from '../Select';
+
+describe('Select', () => {
+  it('should render correctly', () => {
+    return new Promise<void>((resolve, reject) => {
+      expect(shallowRender()).toMatchSnapshot('default');
+      expect(shallowRender({ clearable: true, value: undefined })).toMatchSnapshot(
+        'disable clear button if no value'
+      );
+
+      const clearRenderFn = shallowRender().props().clearRenderer;
+      if (!clearRenderFn) {
+        reject();
+        return;
+      }
+      expect(clearRenderFn()).toMatchSnapshot('clear button');
+
+      resolve();
+    });
+  });
+
+  function shallowRender(props: Partial<WithInnerRef & ReactSelectProps> = {}) {
+    return shallow<WithInnerRef & ReactSelectProps>(<Select value="foo" {...props} />);
+  }
+});
+
+describe('Creatable', () => {
+  it('should render correctly', () => {
+    return new Promise<void>((resolve, reject) => {
+      expect(shallowRender()).toMatchSnapshot('default');
+
+      const clearRenderFn = shallowRender().props().clearRenderer;
+      if (!clearRenderFn) {
+        reject();
+        return;
+      }
+      expect(clearRenderFn()).toMatchSnapshot('clear button');
+
+      resolve();
+    });
+  });
+
+  function shallowRender(props: Partial<ReactCreatableSelectProps> = {}) {
+    return shallow<ReactCreatableSelectProps>(<Creatable {...props} />);
+  }
+});
+
+describe('AsyncSelect', () => {
+  it('should render correctly', () => {
+    expect(shallowRender()).toMatchSnapshot('default');
+  });
+
+  function shallowRender(props: Partial<WithInnerRef & ReactAsyncSelectProps> = {}) {
+    return shallow<WithInnerRef & ReactAsyncSelectProps>(
+      <AsyncSelect loadOptions={jest.fn()} {...props} />
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Select-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Select-test.tsx.snap
new file mode 100644 (file)
index 0000000..c96f416
--- /dev/null
@@ -0,0 +1,50 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AsyncSelect should render correctly: default 1`] = `
+<LazyComponentWrapper
+  loadOptions={[MockFunction]}
+/>
+`;
+
+exports[`Creatable should render correctly: clear button 1`] = `
+<ClearButton
+  className="button-tiny spacer-left text-middle"
+  iconProps={
+    Object {
+      "size": 12,
+    }
+  }
+/>
+`;
+
+exports[`Creatable should render correctly: default 1`] = `
+<LazyComponentWrapper
+  clearRenderer={[Function]}
+/>
+`;
+
+exports[`Select should render correctly: clear button 1`] = `
+<ClearButton
+  className="button-tiny spacer-left text-middle"
+  iconProps={
+    Object {
+      "size": 12,
+    }
+  }
+/>
+`;
+
+exports[`Select should render correctly: default 1`] = `
+<LazyComponentWrapper
+  clearRenderer={[Function]}
+  clearable={false}
+  value="foo"
+/>
+`;
+
+exports[`Select should render correctly: disable clear button if no value 1`] = `
+<LazyComponentWrapper
+  clearRenderer={[Function]}
+  clearable={false}
+/>
+`;
index ec5b58dbb74f27c66c51b7f7f158cf6019c9eee0..ff4bd8acdc0686aa2afb3cb74e0df081999bc5bb 100644 (file)
@@ -41,7 +41,7 @@ interface ButtonProps extends AllowedButtonAttributes {
   onClick?: () => void;
   preventDefault?: boolean;
   stopPropagation?: boolean;
-  type?: 'button' | 'submit' | 'reset' | undefined;
+  type?: 'button' | 'submit' | 'reset';
 }
 
 export class Button extends React.PureComponent<ButtonProps> {
diff --git a/server/sonar-web/src/main/js/components/icons/OnboardingAddMembersIcon.tsx b/server/sonar-web/src/main/js/components/icons/OnboardingAddMembersIcon.tsx
deleted file mode 100644 (file)
index 919affa..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 { colors } from '../../app/theme';
-import Icon, { IconProps } from './Icon';
-
-export default function OnboardingAddMembersIcon({ fill, size = 64, ...iconProps }: IconProps) {
-  return (
-    <Icon height={(size / 64) * 80} viewBox="0 0 64 80" width={size} {...iconProps}>
-      <g>
-        <path
-          d="M49 34c0 9.389-7.611 17-17 17s-17-7.611-17-17 7.611-17 17-17 17 7.611 17 17z"
-          style={{ fill: 'none', stroke: fill || colors.darkBlue, strokeWidth: 2 }}
-        />
-        <path
-          d="M36 32c0 2.2-1.8 4-4 4s-4-1.8-4-4v-1c0-2.2 1.8-4 4-4s4 1.8 4 4v1zm4 39a8 8 0 1 1-16 0 8 8 0 0 1 16 0z"
-          style={{ fill: 'none', stroke: fill || colors.darkBlue, strokeWidth: 2 }}
-        />
-        <path
-          d="M33 70h2v2h-2v2h-2v-2h-2v-2h2v-2h2v2zm-5-14l-.072-.001c-1.521-.054-2.834-1.337-2.925-2.855L25 50h2c0 1.745-.532 3.91.952 3.999L28 54h8v.002l.072-.005c.506-.042.922-.489.928-1.003V50h2c0 1.024.011 2.048-.001 3.072-.054 1.518-1.337 2.834-2.855 2.925l-.072.002L36 56v8h-2v-7.982c-1.333.007-2.667.007-4 0V64h-2v-8zm-7 0H1V10 0h62v56H43v-2h18V10H3v44h18v2zm38-4H43v-2h14V14H7v36h14v2H5V12h54v40zm-19-9l1 .017c-.03 1.79-2.454 2.506-3.918 2.717-4.074.584-8.503.911-12.176-.477-.949-.358-1.887-1.119-1.906-2.24l.191-.017H23v-3.566l5.38-3.228.913-.913 1.414 1.414-1.087 1.087L25 40.566v2.438c.067 1.304 10.98 2.117 13.844.157.076-.052.152-.172.156-.178v-2.417l-4.62-2.772-1.087-1.087 1.414-1.414.913.913L41 39.434V43h-1zm14-4h-2v-2h2v2zm-42 0h-2v-2h2v2zm42-4h-2v-2h2v2zm-42 0h-2v-2h2v2zm42-4h-2v-2h2v2zm-42 0h-2v-2h2v2zm20.198-10.999c3.529.062 6.837 1.669 9.386 4.169l-1.289 1.539c-4.178-4.152-11.167-5.254-16.359-.228l-.231.228-1.41-1.418c2.633-2.617 6.031-4.313 9.903-4.29zM3 2v6h58V2H3zm56 4H17V4h42v2zM11 6H9V4h2v2zM7 6H5V4h2v2zm8 0h-2V4h2v2z"
-          style={{ fill: fill || colors.darkBlue, fillRule: 'nonzero' }}
-        />
-      </g>
-    </Icon>
-  );
-}
diff --git a/server/sonar-web/src/main/js/components/icons/OnboardingProjectIcon.tsx b/server/sonar-web/src/main/js/components/icons/OnboardingProjectIcon.tsx
deleted file mode 100644 (file)
index 36aaaec..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 { colors } from '../../app/theme';
-import Icon, { IconProps } from './Icon';
-
-export default function OnboardingProjectIcon({ fill, size = 64, ...iconProps }: IconProps) {
-  return (
-    <Icon size={size} viewBox="0 0 64 64" {...iconProps}>
-      <g fill="none" fillRule="evenodd" stroke={fill || colors.darkBlue} strokeWidth="2">
-        <path d="M2 59h60V13H2zm0-46h60V5H2zm3-4h2m2 0h2m2 0h2m2 0h42" />
-        <path d="M59 34h-6l-2-4h-6l-2 5h-6l-2 2h-6l-2-4h-6l-2 5h-6l-2 4H5m1 14v-9m4 9v-6m4 6V43m4 13V45m4 11V42m4 14V39m4 17V41m4 15V46m4 10V40m4 16V44m4 12V37m4 19V38m4 18V43m4 13V39m-3-18h-2m-2 0h-2m-2 0h-2M9 29h14M9 33h7m17-12h8m-14 4h8m-8-4h4m-21 4h12v-4H10z" />
-        <path d="M58 31V17H6v22" />
-      </g>
-    </Icon>
-  );
-}
diff --git a/server/sonar-web/src/main/js/components/icons/OnboardingTeamIcon.tsx b/server/sonar-web/src/main/js/components/icons/OnboardingTeamIcon.tsx
deleted file mode 100644 (file)
index 4bb4b12..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 { colors } from '../../app/theme';
-import Icon, { IconProps } from './Icon';
-
-export default function OnboardingTeamIcon({ fill, size = 64, ...iconProps }: IconProps) {
-  return (
-    <Icon size={size} viewBox="0 0 64 64" {...iconProps}>
-      <g fill="none" fillRule="evenodd" stroke={fill || colors.darkBlue} strokeWidth="2">
-        <path d="M32 9v5M11.5195 43.0898l7.48-4.091m33.481-18.0994l-7.48 4.1m-33.481-4.1l7.48 4.1M45 38.999l7.48 4.101M32 50v5m15-23c0 8.284-6.715 15-15 15s-15-6.716-15-15c0-8.285 6.715-15 15-15s15 6.715 15 15z" />
-        <path d="M40 38c0 1.656-3.58 2-8 2s-8-.344-8-2m16 0v-3l-5-3-1-1m-10 7v-3l5-3 1-1m6-4c0 2.2-1.8 4-4 4s-4-1.8-4-4v-1c0-2.2 1.8-4 4-4s4 1.8 4 4v1zm-.0098-21.71c7.18 1.069 13.439 4.96 17.609 10.51m-17.609 42.91c7.18-1.07 13.439-4.96 17.609-10.51M6.6299 41.25c-1.06-2.88-1.63-6-1.63-9.25s.57-6.37 1.63-9.25m3.7705-6.9502c4.17-5.55 10.43-9.44 17.609-10.51m-17.609 42.9104c4.17 5.55 10.43 9.439 17.609 10.51M57.3701 22.75c1.06 2.88 1.63 6 1.63 9.25s-.57 6.37-1.63 9.25" />
-        <path d="M36 5c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M12 19c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2m51 0c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M12 45c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2m51 0c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M36 59c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2" />
-      </g>
-    </Icon>
-  );
-}
index 6fc6042d705d87133ea8550a1e8cbbfeae598d3b..044ed7edca04f537c6b666058f65a06c08030ec3 100644 (file)
@@ -38,8 +38,8 @@ export default function SeverityIcon({ severity, ...iconProps }: Props) {
     return null;
   }
 
-  const Icon = severityIcons[severity.toLowerCase()];
-  return Icon ? <Icon {...iconProps} /> : null;
+  const DesiredIcon = severityIcons[severity.toLowerCase()];
+  return DesiredIcon ? <DesiredIcon {...iconProps} /> : null;
 }
 
 function BlockerSeverityIcon(iconProps: IconProps) {
index 06b6e01ebe53f38865dd75b52eed408aeabd9138..713a92cdcecabf71870e01b5e52846a8aef5ac01 100644 (file)
@@ -37,8 +37,8 @@ const statusIcons: T.Dict<(props: IconProps) => React.ReactElement> = {
 };
 
 export default function StatusIcon({ status, ...iconProps }: Props) {
-  const Icon = statusIcons[status.toLowerCase()];
-  return Icon ? <Icon {...iconProps} /> : null;
+  const DesiredStatusIcon = statusIcons[status.toLowerCase()];
+  return DesiredStatusIcon ? <DesiredStatusIcon {...iconProps} /> : null;
 }
 
 function OpenStatusIcon(iconProps: IconProps) {
index f7bd5ff5843ae1423fd4a7c919c4a753df45aca1..6555744c948e4863f18589d5bc2072484e353287 100644 (file)
@@ -33,8 +33,8 @@ const statusIcons: T.Dict<(props: IconProps) => React.ReactElement> = {
 };
 
 export default function TestStatusIcon({ status, ...iconProps }: Props) {
-  const Icon = statusIcons[status.toLowerCase()];
-  return Icon ? <Icon {...iconProps} /> : null;
+  const DesiredStatusIcon = statusIcons[status.toLowerCase()];
+  return DesiredStatusIcon ? <DesiredStatusIcon {...iconProps} /> : null;
 }
 
 function OkTestStatusIcon(iconProps: IconProps) {
index 2a63bad32ad22f7d4dcb2ad3667cb44e421cc589..0a86b415a9c2e6389a6774407c5bba8be1513666 100644 (file)
@@ -44,12 +44,15 @@ interface AlertVariantInformation {
   backGroundColor: string;
 }
 
+const DOUBLE = 2;
+const QUADRUPLE = 4;
+
 const StyledAlertIcon = styled.div<{ isBanner: boolean; variantInfo: AlertVariantInformation }>`
   flex: 0 0 auto;
   display: flex;
   justify-content: center;
   align-items: center;
-  width: calc(${({ isBanner }) => (isBanner ? 2 : 4)} * ${sizes.gridSize});
+  width: calc(${({ isBanner }) => (isBanner ? DOUBLE : QUADRUPLE)} * ${sizes.gridSize});
   border-right: ${({ isBanner }) => (!isBanner ? '1px solid' : 'none')};
   border-color: ${({ variantInfo }) => variantInfo.borderColor};
 `;
index 07a75d15ae5bda458b183b047eac8677892464b4..e81fbf8bbce422eb8fa1b5c43fd99a5d731af4ec 100644 (file)
 import * as classNames from 'classnames';
 import * as React from 'react';
 import './ContextNavBar.css';
-import NavBar from './NavBar';
+import NavBar, { NavBarProps } from './NavBar';
 
-interface Props {
+interface Props extends NavBarProps {
   className?: string;
+  id?: string;
   height: number;
-  [attr: string]: any;
 }
 
 export default function ContextNavBar({ className, ...other }: Props) {
index c9e5757121db3f94972349e2dcc38555c0e1d417..40b364d2d75a526adc7725b4ea1443c512fb2186 100644 (file)
@@ -23,9 +23,6 @@
   box-sizing: border-box;
 }
 
-.navbar {
-}
-
 .navbar-inner {
   position: fixed;
   left: 0;
index 0cc09e5abc6a5dee72a50f3356dee313653261fe..d10bb7b4446f7c32b77f65716d43394d4295c4c6 100644 (file)
@@ -21,10 +21,9 @@ import * as classNames from 'classnames';
 import * as React from 'react';
 import './NavBarTabs.css';
 
-interface Props {
-  children?: any;
+interface Props extends React.HTMLAttributes<HTMLUListElement> {
+  children?: React.ReactNode;
   className?: string;
-  [attr: string]: any;
 }
 
 export default function NavBarTabs({ children, className, ...other }: Props) {
diff --git a/server/sonar-web/src/main/js/helpers/mocks/dom.ts b/server/sonar-web/src/main/js/helpers/mocks/dom.ts
new file mode 100644 (file)
index 0000000..71e666f
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+
+export function mockHtmlElement<T extends Element>(overrides: Partial<T> = {}): T {
+  return {
+    getBoundingClientRect: () => ({
+      bottom: 0,
+      height: 100,
+      width: 50,
+      left: 0,
+      right: 0,
+      top: 10,
+      x: 12,
+      y: 23,
+      toJSON: () => ''
+    }),
+    ...overrides
+  } as T;
+}
index 40c686e43fa61e1a0ce4b050d5ccc9d8f11e8ac5..f56c4faaa11226824e6fc6ac8c5aa68d5cd50613 100644 (file)
@@ -49,7 +49,7 @@ export function removeNoFooterPageClass() {
   toggleBodyClass(CLASS_NO_FOOTER_PAGE, false);
 }
 
-function toggleBodyClass(className: string, force?: boolean) {
+function toggleBodyClass(className: string, force: boolean) {
   document.body.classList.toggle(className, force);
   if (document.documentElement) {
     document.documentElement.classList.toggle(className, force);