]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10347 Create a BoxedGroupAccordion component and use it in system page
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Thu, 8 Feb 2018 14:38:53 +0000 (15:38 +0100)
committerGuillaume Jambet <guillaume.jambet@gmail.com>
Thu, 1 Mar 2018 14:21:05 +0000 (15:21 +0100)
13 files changed:
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/SystemInfoPage.java
server/sonar-web/src/main/js/app/styles/components/boxed-group.css
server/sonar-web/src/main/js/apps/system/components/ClusterSysInfos.tsx
server/sonar-web/src/main/js/apps/system/components/StandaloneSysInfos.tsx
server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ClusterSysInfos-test.tsx.snap
server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/StandaloneSysInfos-test.tsx.snap
server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx
server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthCard-test.tsx
server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthCard-test.tsx.snap
server/sonar-web/src/main/js/apps/system/styles.css
server/sonar-web/src/main/js/components/controls/BoxedGroupAccordion.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/BoxedGroupAccordion-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/BoxedGroupAccordion-test.tsx.snap [new file with mode: 0644]

index 9c9802e7e7fe84ba611032033f78f2a524c9bc80..857153eb51773c8b0f319413a4f2fe1dbd3ec1d3 100644 (file)
@@ -21,26 +21,28 @@ package org.sonarqube.qa.util.pageobjects;
 
 import com.codeborne.selenide.CollectionCondition;
 import com.codeborne.selenide.Condition;
-import com.codeborne.selenide.Selenide;
+import com.codeborne.selenide.ElementsCollection;
 import com.codeborne.selenide.SelenideElement;
 
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+
 public class SystemInfoPage {
   public SystemInfoPage() {
-    Selenide.$(".page-title").should(Condition.exist).shouldHave(Condition.text("System Info"));
-  }
-
-  public SystemInfoPage shouldHaveCard(String title) {
-    Selenide.$$(".system-info-health-card-title").find(Condition.text(title)).should(Condition.exist);
-    return this;
+    $(".page-title").should(Condition.exist).shouldHave(Condition.text("System Info"));
   }
 
   public SystemInfoPage shouldHaveCards(String... titles) {
-    Selenide.$$(".system-info-health-card-title").shouldHave(CollectionCondition.texts(titles));
+    getHealthCards().shouldHave(CollectionCondition.texts(titles));
     return this;
   }
 
   public SystemInfoPageItem getCardItem(String card) {
-    SelenideElement cardTitle = Selenide.$$(".system-info-health-card-title").find(Condition.text(card)).should(Condition.exist);
+    SelenideElement cardTitle = getHealthCards().find(Condition.text(card)).should(Condition.exist);
     return new SystemInfoPageItem(cardTitle.parent().parent());
   }
+
+  private static ElementsCollection getHealthCards() {
+    return $$(".boxed-group-accordion-title");
+  }
 }
index 68e97474d38d83064e9c1f49dfc411158af57ee5..49aa03123b615c19c282f16eaba68303e8a7c237 100644 (file)
 
 .boxed-group > h2 {
   line-height: var(--controlHeight);
-  padding: 15px 20px 0;
+  padding: calc(2 * var(--gridSize)) 20px 0;
 }
 
 .boxed-group hr {
   height: 0;
   border-top: 1px solid var(--gray94);
-  margin: 15px -20px;
+  margin: calc(2 * var(--gridSize)) -20px;
 }
 
 .boxed-group-header {
   position: relative;
   z-index: 10;
-  padding: 15px 20px 0;
+  padding: calc(2 * var(--gridSize)) 20px 0;
 }
 
 .boxed-group-header > h2 {
   position: relative;
   z-index: 12;
   float: right;
-  margin-top: 15px;
+  margin-top: calc(2 * var(--gridSize));
   margin-right: 20px;
 }
 
 .boxed-group-inner {
-  padding: 15px 20px;
+  padding: calc(2 * var(--gridSize)) 20px;
 }
 
 .boxed-group-inner:empty {
 }
 
 .boxed-group-list {
-  margin-top: -8px;
-  margin-bottom: -8px;
+  margin-top: - var(--gridSize);
+  margin-bottom: - var(--gridSize);
 }
 
 .boxed-group-list > li {
   margin-left: -20px;
   margin-right: -20px;
-  padding: 8px 20px;
+  padding: var(--gridSize) 20px;
+}
+
+.boxed-group-accordion {
+  margin-bottom: var(--gridSize);
+  transition: border-color 0.3s ease;
+}
+
+.boxed-group-accordion:not(.no-hover):hover {
+  border-color: var(--blue);
+}
+
+.boxed-group-accordion:not(.no-hover):hover .boxed-group-accordion-title {
+  color: var(--blue);
+}
+
+.boxed-group-accordion .boxed-group-header {
+  cursor: pointer;
+  padding-bottom: calc(2 * var(--gridSize));
+}
+
+.boxed-group-accordion .boxed-group-header > .alert {
+  display: inline-block;
+  margin-bottom: -6px;
+  margin-top: -6px;
+}
+
+.boxed-group-accordion .boxed-group-inner {
+  padding-top: 0;
+}
+
+.boxed-group-accordion-title {
+  font-weight: bold;
+  transition: color 0.3s ease;
 }
index dec9d88310fc9fc7a6efd26ce1d3121411c3e82e..1d7ae6d3f8c0e2c3f2d1941a6dba61a05c360eda 100644 (file)
@@ -41,7 +41,7 @@ interface Props {
 export default function ClusterSysInfos({ expandedCards, sysInfoData, toggleCard }: Props) {
   const mainCardName = 'System';
   return (
-    <ul>
+    <>
       <HealthCard
         biggerHealth={true}
         health={getHealth(sysInfoData)}
@@ -77,6 +77,6 @@ export default function ClusterSysInfos({ expandedCards, sysInfoData, toggleCard
           sysInfoData={ignoreInfoFields(node)}
         />
       ))}
-    </ul>
+    </>
   );
 }
index ba09a11934adab6c167c10921223a871b6351672..ecdb45fe85c72715287a1606b56b2e17036233c3 100644 (file)
@@ -38,7 +38,7 @@ interface Props {
 export default function StandAloneSysInfos({ expandedCards, sysInfoData, toggleCard }: Props) {
   const mainCardName = 'System';
   return (
-    <ul>
+    <>
       <HealthCard
         biggerHealth={true}
         health={getHealth(sysInfoData)}
@@ -57,6 +57,6 @@ export default function StandAloneSysInfos({ expandedCards, sysInfoData, toggleC
           sysInfoData={ignoreInfoFields(section)}
         />
       ))}
-    </ul>
+    </>
   );
 }
index ca216f72e048f093642b821c6003ddb7f4bfae18..0e31e22e4be495f0fd9b90831b4d434215e82963 100644 (file)
@@ -1,7 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`should support more than two nodes 1`] = `
-<ul>
+<React.Fragment>
   <HealthCard
     biggerHealth={true}
     health="RED"
@@ -67,5 +67,5 @@ exports[`should support more than two nodes 1`] = `
       }
     }
   />
-</ul>
+</React.Fragment>
 `;
index e5832de513c4d3ca8e959529c6c40ae99a2e77b3..0d7912636bf496aa889e5b2cce67f8c35046a5cb 100644 (file)
@@ -1,7 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`should render correctly 1`] = `
-<ul>
+<React.Fragment>
   <HealthCard
     biggerHealth={true}
     health="RED"
@@ -60,5 +60,5 @@ exports[`should render correctly 1`] = `
       }
     }
   />
-</ul>
+</React.Fragment>
 `;
index 105c8002bcddc9e6ed077cfa1ec546f17cd38348..f042eed7de36e7288896888abf568622590df9ed 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import * as classNames from 'classnames';
 import { map } from 'lodash';
 import HealthItem from './HealthItem';
 import Section from './Section';
-import OpenCloseIcon from '../../../../components/icons-components/OpenCloseIcon';
+import BoxedGroupAccordion from '../../../../components/controls/BoxedGroupAccordion';
 import { HealthType, SysValueObject } from '../../../../api/system';
 import { LOGS_LEVELS, groupSections, getLogsLevel } from '../../utils';
 import { translate } from '../../../../helpers/l10n';
@@ -37,34 +36,27 @@ interface Props {
   sysInfoData: SysValueObject;
 }
 
-interface State {
-  hoveringDetail: boolean;
-}
-
-export default class HealthCard extends React.PureComponent<Props, State> {
-  state: State = { hoveringDetail: false };
-
-  handleClick = () => this.props.onClick(this.props.name);
-  onDetailEnter = () => this.setState({ hoveringDetail: true });
-  onDetailLeave = () => this.setState({ hoveringDetail: false });
-
-  render() {
-    const { health, open, sysInfoData } = this.props;
-    const { mainSection, sections } = groupSections(sysInfoData);
-    const showFields = open && mainSection && Object.keys(mainSection).length > 0;
-    const showSections = open && sections;
-    const logLevel = getLogsLevel(sysInfoData);
-    const showLogLevelWarning = logLevel && logLevel !== LOGS_LEVELS[0];
-    return (
-      <li
-        className={classNames('boxed-group system-info-health-card', {
-          'no-hover': this.state.hoveringDetail
-        })}>
-        <div className="boxed-group-header" onClick={this.handleClick}>
-          <span className="system-info-health-card-title">
-            <OpenCloseIcon className="little-spacer-right" open={open} />
-            {this.props.name}
-          </span>
+export default function HealthCard({
+  biggerHealth,
+  health,
+  healthCauses,
+  onClick,
+  open,
+  name,
+  sysInfoData
+}: Props) {
+  const { mainSection, sections } = groupSections(sysInfoData);
+  const showFields = open && mainSection && Object.keys(mainSection).length > 0;
+  const showSections = open && sections;
+  const logLevel = getLogsLevel(sysInfoData);
+  const showLogLevelWarning = logLevel && logLevel !== LOGS_LEVELS[0];
+  return (
+    <BoxedGroupAccordion
+      data={name}
+      onClick={onClick}
+      open={open}
+      renderHeader={() => (
+        <>
           {showLogLevelWarning && (
             <span className="alert alert-danger spacer-left">
               {translate('system.log_level.warning.short')}
@@ -72,25 +64,19 @@ export default class HealthCard extends React.PureComponent<Props, State> {
           )}
           {health && (
             <HealthItem
-              biggerHealth={this.props.biggerHealth}
+              biggerHealth={biggerHealth}
               className="pull-right"
               health={health}
-              healthCauses={this.props.healthCauses}
-              name={this.props.name}
+              healthCauses={healthCauses}
+              name={name}
             />
           )}
-        </div>
-        {open && (
-          <div
-            className="boxed-group-inner"
-            onMouseEnter={this.onDetailEnter}
-            onMouseLeave={this.onDetailLeave}>
-            {showFields && <Section items={mainSection} />}
-            {showSections &&
-              map(sections, (section, name) => <Section key={name} items={section} name={name} />)}
-          </div>
-        )}
-      </li>
-    );
-  }
+        </>
+      )}
+      title={name}>
+      {showFields && <Section items={mainSection} />}
+      {showSections &&
+        map(sections, (section, name) => <Section key={name} items={section} name={name} />)}
+    </BoxedGroupAccordion>
+  );
 }
index abe3f8b002b477ecd48f789204761735bd531e9b..08ea6b5f66dd919e03dcb4ef353c0be1e67b9cd7 100644 (file)
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import HealthCard from '../HealthCard';
-import { click } from '../../../../../helpers/testUtils';
 import { HealthType } from '../../../../../api/system';
 
 it('should render correctly', () => {
-  expect(getShallowWrapper()).toMatchSnapshot();
-});
-
-it('should display the sysinfo detail', () => {
-  expect(getShallowWrapper({ biggerHealth: true, open: true })).toMatchSnapshot();
-});
-
-it('should show the sysinfo detail when the card is clicked', () => {
-  const onClick = jest.fn();
-  click(getShallowWrapper({ onClick }).find('.boxed-group-header'));
-  expect(onClick).toBeCalled();
-  expect(onClick).toBeCalledWith('Foobar');
+  expect(getWrapper()).toMatchSnapshot();
 });
 
 it('should show a main section and multiple sub sections', () => {
@@ -45,16 +33,18 @@ it('should show a main section and multiple sub sections', () => {
     Database: { db: 'test' },
     Elasticseach: { Elastic: 'search' }
   };
-  expect(getShallowWrapper({ open: true, sysInfoData })).toMatchSnapshot();
+  expect(getWrapper({ open: true, sysInfoData })).toMatchSnapshot();
 });
 
 it('should display the log level alert', () => {
   expect(
-    getShallowWrapper({ sysInfoData: { 'Logs Level': 'DEBUG' } }).find('.alert')
+    getWrapper({ sysInfoData: { 'Logs Level': 'DEBUG' } })
+      .dive()
+      .find('.alert')
   ).toMatchSnapshot();
 });
 
-function getShallowWrapper(props = {}) {
+function getWrapper(props = {}) {
   return shallow(
     <HealthCard
       biggerHealth={false}
index fe7c59a4286d28f3b03b87cf5356afa3b487cfc4..8ce1a5d4f14434a113c2e880ff5edd78b96400fa 100644 (file)
@@ -8,135 +8,49 @@ exports[`should display the log level alert 1`] = `
 </span>
 `;
 
-exports[`should display the sysinfo detail 1`] = `
-<li
-  className="boxed-group system-info-health-card"
->
-  <div
-    className="boxed-group-header"
-    onClick={[Function]}
-  >
-    <span
-      className="system-info-health-card-title"
-    >
-      <OpenCloseIcon
-        className="little-spacer-right"
-        open={true}
-      />
-      Foobar
-    </span>
-    <HealthItem
-      biggerHealth={true}
-      className="pull-right"
-      health="RED"
-      healthCauses={
-        Array [
-          "foo",
-        ]
-      }
-      name="Foobar"
-    />
-  </div>
-  <div
-    className="boxed-group-inner"
-    onMouseEnter={[Function]}
-    onMouseLeave={[Function]}
-  />
-</li>
-`;
-
 exports[`should render correctly 1`] = `
-<li
-  className="boxed-group system-info-health-card"
->
-  <div
-    className="boxed-group-header"
-    onClick={[Function]}
-  >
-    <span
-      className="system-info-health-card-title"
-    >
-      <OpenCloseIcon
-        className="little-spacer-right"
-        open={false}
-      />
-      Foobar
-    </span>
-    <HealthItem
-      biggerHealth={false}
-      className="pull-right"
-      health="RED"
-      healthCauses={
-        Array [
-          "foo",
-        ]
-      }
-      name="Foobar"
-    />
-  </div>
-</li>
+<BoxedGroupAccordion
+  data="Foobar"
+  onClick={[Function]}
+  open={false}
+  renderHeader={[Function]}
+  title="Foobar"
+/>
 `;
 
 exports[`should show a main section and multiple sub sections 1`] = `
-<li
-  className="boxed-group system-info-health-card"
+<BoxedGroupAccordion
+  data="Foobar"
+  onClick={[Function]}
+  open={true}
+  renderHeader={[Function]}
+  title="Foobar"
 >
-  <div
-    className="boxed-group-header"
-    onClick={[Function]}
-  >
-    <span
-      className="system-info-health-card-title"
-    >
-      <OpenCloseIcon
-        className="little-spacer-right"
-        open={true}
-      />
-      Foobar
-    </span>
-    <HealthItem
-      biggerHealth={false}
-      className="pull-right"
-      health="RED"
-      healthCauses={
-        Array [
-          "foo",
-        ]
-      }
-      name="Foobar"
-    />
-  </div>
-  <div
-    className="boxed-group-inner"
-    onMouseEnter={[Function]}
-    onMouseLeave={[Function]}
-  >
-    <Section
-      items={
-        Object {
-          "Name": "foo",
-          "bar": "Bar",
-        }
+  <Section
+    items={
+      Object {
+        "Name": "foo",
+        "bar": "Bar",
       }
-    />
-    <Section
-      items={
-        Object {
-          "db": "test",
-        }
+    }
+  />
+  <Section
+    items={
+      Object {
+        "db": "test",
       }
-      key="Database"
-      name="Database"
-    />
-    <Section
-      items={
-        Object {
-          "Elastic": "search",
-        }
+    }
+    key="Database"
+    name="Database"
+  />
+  <Section
+    items={
+      Object {
+        "Elastic": "search",
       }
-      key="Elasticseach"
-      name="Elasticseach"
-    />
-  </div>
-</li>
+    }
+    key="Elasticseach"
+    name="Elasticseach"
+  />
+</BoxedGroupAccordion>
 `;
index c6c4ad4afb59521bb3ab726259c9be8e36792422..3c332eaa7c1df392bb89584e6fb450ace783eaa3 100644 (file)
   margin-bottom: 16px;
 }
 
-.system-info-health-card {
-  margin-bottom: 8px;
-  transition: border-color 0.3s ease;
-}
-
-.system-info-health-card:not(.no-hover):hover {
-  border-color: var(--blue);
-}
-
-.system-info-health-card:not(.no-hover):hover .system-info-health-card-title {
-  color: var(--blue);
-}
-
-.system-info-health-card .boxed-group-header {
-  cursor: pointer;
-  padding-bottom: 15px;
-}
-
-.system-info-health-card .boxed-group-header > .alert {
-  display: inline-block;
-  margin-bottom: -6px;
-  margin-top: -6px;
-}
-
-.system-info-health-card .boxed-group-inner {
-  padding-top: 0;
-}
-
-.system-info-health-card-title {
-  font-weight: bold;
-}
-
 .system-info-health-info {
   margin-top: -12px;
 }
diff --git a/server/sonar-web/src/main/js/components/controls/BoxedGroupAccordion.tsx b/server/sonar-web/src/main/js/components/controls/BoxedGroupAccordion.tsx
new file mode 100644 (file)
index 0000000..63129cc
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import OpenCloseIcon from '../icons-components/OpenCloseIcon';
+
+interface Props {
+  children: React.ReactNode;
+  className?: string;
+  data?: string;
+  onClick: (data?: string) => void;
+  open: boolean;
+  renderHeader?: () => React.ReactNode;
+  title: React.ReactNode;
+}
+
+interface State {
+  hoveringInner: boolean;
+}
+
+export default class BoxedGroupAccordion extends React.PureComponent<Props, State> {
+  state: State = { hoveringInner: false };
+
+  handleClick = () => {
+    this.props.onClick(this.props.data);
+  };
+
+  onDetailEnter = () => {
+    this.setState({ hoveringInner: true });
+  };
+
+  onDetailLeave = () => {
+    this.setState({ hoveringInner: false });
+  };
+
+  render() {
+    const { className, open, renderHeader, title } = this.props;
+    return (
+      <div
+        className={classNames('boxed-group boxed-group-accordion', className, {
+          'no-hover': this.state.hoveringInner
+        })}>
+        <div className="boxed-group-header" onClick={this.handleClick} role="listitem">
+          <span className="boxed-group-accordion-title">
+            <OpenCloseIcon className="little-spacer-right" open={open} />
+            {title}
+          </span>
+          {renderHeader && renderHeader()}
+        </div>
+        {open && (
+          <div
+            className="boxed-group-inner"
+            onMouseEnter={this.onDetailEnter}
+            onMouseLeave={this.onDetailLeave}>
+            {this.props.children}
+          </div>
+        )}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/BoxedGroupAccordion-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/BoxedGroupAccordion-test.tsx
new file mode 100644 (file)
index 0000000..8101724
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import { click } from '../../../helpers/testUtils';
+import BoxedGroupAccordion from '../BoxedGroupAccordion';
+
+it('should render correctly', () => {
+  expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should show the inner content after a click', () => {
+  const onClick = jest.fn();
+  const wrapper = getWrapper({ onClick });
+  click(wrapper.find('.boxed-group-header'));
+
+  expect(onClick).lastCalledWith('foo');
+  wrapper.setProps({ open: true });
+
+  expect(wrapper.find('.boxed-group-inner').exists()).toBeTruthy();
+});
+
+function getWrapper(props = {}) {
+  return shallow(
+    <BoxedGroupAccordion
+      data="foo"
+      onClick={() => {}}
+      open={false}
+      renderHeader={() => <div>header content</div>}
+      title="Foo"
+      {...props}>
+      <div>inner content</div>
+    </BoxedGroupAccordion>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/BoxedGroupAccordion-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/BoxedGroupAccordion-test.tsx.snap
new file mode 100644 (file)
index 0000000..0c7b74b
--- /dev/null
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="boxed-group boxed-group-accordion"
+>
+  <div
+    className="boxed-group-header"
+    onClick={[Function]}
+    role="listitem"
+  >
+    <span
+      className="boxed-group-accordion-title"
+    >
+      <OpenCloseIcon
+        className="little-spacer-right"
+        open={false}
+      />
+      Foo
+    </span>
+    <div>
+      header content
+    </div>
+  </div>
+</div>
+`;