Browse Source

SONAR-10347 Create a BoxedGroupAccordion component and use it in system page

tags/7.5
Grégoire Aubert 6 years ago
parent
commit
87ca6669cd

+ 11
- 9
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/SystemInfoPage.java View 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");
}
}

+ 41
- 8
server/sonar-web/src/main/js/app/styles/components/boxed-group.css View File

@@ -26,19 +26,19 @@

.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 {
@@ -56,12 +56,12 @@
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 {
@@ -69,12 +69,45 @@
}

.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;
}

+ 2
- 2
server/sonar-web/src/main/js/apps/system/components/ClusterSysInfos.tsx View 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>
</>
);
}

+ 2
- 2
server/sonar-web/src/main/js/apps/system/components/StandaloneSysInfos.tsx View 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>
</>
);
}

+ 2
- 2
server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ClusterSysInfos-test.tsx.snap View 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>
`;

+ 2
- 2
server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/StandaloneSysInfos-test.tsx.snap View 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>
`;

+ 33
- 47
server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx View File

@@ -18,11 +18,10 @@
* 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>
);
}

+ 6
- 16
server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthCard-test.tsx View File

@@ -20,22 +20,10 @@
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}

+ 37
- 123
server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthCard-test.tsx.snap View 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>
`;

+ 0
- 32
server/sonar-web/src/main/js/apps/system/styles.css View File

@@ -22,38 +22,6 @@
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;
}

+ 78
- 0
server/sonar-web/src/main/js/components/controls/BoxedGroupAccordion.tsx View File

@@ -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>
);
}
}

+ 52
- 0
server/sonar-web/src/main/js/components/controls/__tests__/BoxedGroupAccordion-test.tsx View File

@@ -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>
);
}

+ 26
- 0
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/BoxedGroupAccordion-test.tsx.snap View File

@@ -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>
`;

Loading…
Cancel
Save