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