Sfoglia il codice sorgente

SONAR-9508 Display worker counter in background tasks page

tags/6.6-RC1
Stas Vilchik 6 anni fa
parent
commit
373c5888e0

+ 7
- 0
server/sonar-web/src/main/js/api/ce.js Vedi File

@@ -19,6 +19,7 @@
*/
// @flow
import { getJSON, post } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';

export const getActivity = (data?: Object): Promise<*> => getJSON('/api/ce/activity', data);

@@ -42,3 +43,9 @@ export const getTasksForComponent = (componentKey: string): Promise<*> =>
getJSON('/api/ce/component', { componentKey });

export const getTypes = (): Promise<*> => getJSON('/api/ce/task_types').then(r => r.taskTypes);

export const getWorkers = (): Promise<{ canSetWorkerCount: boolean, value: number }> =>
getJSON('/api/ce/worker_count').catch(throwGlobalError);

export const setWorkerCount = (count: number): Promise<void> =>
post('/api/ce/set_worker_count', { count }).catch(throwGlobalError);

+ 9
- 0
server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css Vedi File

@@ -18,3 +18,12 @@
.bt-search-form-right {
margin-left: auto !important;
}

.bt-workers-warning-icon {
position: relative;
top: -1px;
}

.bt-workers-warning-icon::before {
color: #d3d3d3;
}

+ 1
- 1
server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js Vedi File

@@ -214,7 +214,7 @@ class BackgroundTasksApp extends React.PureComponent {
return (
<div className="page page-limited">
<Helmet title={translate('background_tasks.page')} />
<Header />
<Header component={component} />

<Stats
component={component}

+ 11
- 4
server/sonar-web/src/main/js/apps/background-tasks/components/Header.js Vedi File

@@ -19,19 +19,26 @@
*/
/* @flow */
import React from 'react';
import Workers from './Workers';
import { translate } from '../../../helpers/l10n';

const Header = () => {
type Props = {
component?: Object
};

export default function Header(props: Props) {
return (
<header className="page-header">
<h1 className="page-title">
{translate('background_tasks.page')}
</h1>
{!props.component &&
<div className="page-actions">
<Workers />
</div>}
<p className="page-description">
{translate('background_tasks.page.description')}
</p>
</header>
);
};

export default Header;
}

+ 108
- 0
server/sonar-web/src/main/js/apps/background-tasks/components/Workers.js Vedi File

@@ -0,0 +1,108 @@
/*
* SonarQube
* Copyright (C) 2009-2017 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.
*/
// @flow
import React from 'react';
import WorkersForm from './WorkersForm';
import Tooltip from '../../../components/controls/Tooltip';
import { getWorkers } from '../../../api/ce';
import { translate } from '../../../helpers/l10n';

type State = {
canSetWorkerCount: boolean,
formOpen: boolean,
loading: boolean,
workerCount: number
};

export default class Workers extends React.PureComponent {
mounted: boolean;
state: State = {
canSetWorkerCount: false,
formOpen: false,
loading: true,
workerCount: 1
};

componentDidMount() {
this.mounted = true;
this.loadWorkers();
}

componentWillUnmount() {
this.mounted = false;
}

loadWorkers = () => {
this.setState({ loading: true });
getWorkers().then(({ canSetWorkerCount, value }) => {
if (this.mounted) {
this.setState({
canSetWorkerCount,
loading: false,
workerCount: value
});
}
});
};

closeForm = (newWorkerCount?: number) =>
(newWorkerCount
? this.setState({ formOpen: false, workerCount: newWorkerCount })
: this.setState({ formOpen: false }));

handleChangeClick = (event: Event) => {
event.preventDefault();
this.setState({ formOpen: true });
};

render() {
const { canSetWorkerCount, formOpen, loading, workerCount } = this.state;

return (
<div>
{!loading &&
workerCount > 1 &&
<Tooltip overlay={translate('background_tasks.number_of_workers.warning')}>
<i className="icon-alert-warn little-spacer-right bt-workers-warning-icon" />
</Tooltip>}

{translate('background_tasks.number_of_workers')}

{loading
? <i className="spinner little-spacer-left" />
: <strong className="little-spacer-left">{workerCount}</strong>}

{!loading &&
(canSetWorkerCount
? <Tooltip overlay={translate('background_tasks.change_number_of_workers')}>
<a className="icon-edit spacer-left" href="#" onClick={this.handleChangeClick} />
</Tooltip>
: <a
className="button button-promote spacer-left"
href="https://redirect.sonarsource.com/plugins/governance.html"
target="_blank">
{translate('background_tasks.add_more_with_governance')}
</a>)}

{formOpen && <WorkersForm onClose={this.closeForm} workerCount={this.state.workerCount} />}
</div>
);
}
}

+ 126
- 0
server/sonar-web/src/main/js/apps/background-tasks/components/WorkersForm.js Vedi File

@@ -0,0 +1,126 @@
/*
* SonarQube
* Copyright (C) 2009-2017 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.
*/
// @flow
import React from 'react';
import Modal from 'react-modal';
import Select from 'react-select';
import { times } from 'lodash';
import { setWorkerCount } from '../../../api/ce';
import { translate } from '../../../helpers/l10n';

const MAX_WORKERS = 10;

type Props = {
onClose: (newWorkerCount?: number) => void,
workerCount: number
};

type State = {
newWorkerCount: number,
submitting: boolean
};

export default class WorkersForm extends React.PureComponent {
mounted: boolean;
props: Props;
state: State;

constructor(props: Props) {
super(props);
this.state = {
newWorkerCount: props.workerCount,
submitting: false
};
}

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleClose = () => this.props.onClose();

handleWorkerCountChange = (option: { value: number }) =>
this.setState({ newWorkerCount: option.value });

handleSubmit = (event: Event) => {
event.preventDefault();
this.setState({ submitting: true });
const { newWorkerCount } = this.state;
setWorkerCount(newWorkerCount).then(
() => {
if (this.mounted) {
this.props.onClose(newWorkerCount);
}
},
() => {
if (this.mounted) {
this.setState({ submitting: false });
}
}
);
};

render() {
const options = times(MAX_WORKERS).map((_, i) => ({ label: i + 1, value: i + 1 }));

return (
<Modal
isOpen={true}
contentLabel={translate('background_tasks.change_number_of_workers')}
className="modal"
overlayClassName="modal-overlay"
onRequestClose={this.handleClose}>
<header className="modal-head">
<h2>{translate('background_tasks.change_number_of_workers')}</h2>
</header>
<form onSubmit={this.handleSubmit}>
<div className="modal-body">
<Select
className="input-tiny spacer-top"
clearable={false}
onChange={this.handleWorkerCountChange}
options={options}
searchable={false}
value={this.state.newWorkerCount}
/>
<div className="big-spacer-top alert alert-success markdown">
{translate('background_tasks.change_number_of_workers.hint')}
</div>
</div>
<footer className="modal-foot">
<div>
{this.state.submitting && <i className="spinner spacer-right" />}
<button disabled={this.state.submitting} type="submit">
{translate('save')}
</button>
<button type="reset" className="button-link" onClick={this.handleClose}>
{translate('cancel')}
</button>
</div>
</footer>
</form>
</Modal>
);
}
}

+ 72
- 0
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/Workers-test.js Vedi File

@@ -0,0 +1,72 @@
/*
* SonarQube
* Copyright (C) 2009-2017 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.
*/
// @flow
import React from 'react';
import { shallow } from 'enzyme';
import Workers from '../Workers';
import { click } from '../../../../helpers/testUtils';

it('renders', () => {
const wrapper = shallow(<Workers />);
expect(wrapper).toMatchSnapshot();

wrapper.setState({
canSetWorkerCount: true,
loading: false,
workerCount: 1
});
expect(wrapper).toMatchSnapshot();

wrapper.setState({ canSetWorkerCount: false });
expect(wrapper).toMatchSnapshot();

wrapper.setState({ workerCount: 2 });
expect(wrapper).toMatchSnapshot();
});

it('opens form', () => {
const wrapper = shallow(<Workers />);

wrapper.setState({
canSetWorkerCount: true,
loading: false,
workerCount: 1
});
expect(wrapper).toMatchSnapshot();

click(wrapper.find('.icon-edit'));
expect(wrapper).toMatchSnapshot();
});

it('updates worker count', () => {
const wrapper = shallow(<Workers />);

wrapper.setState({
canSetWorkerCount: true,
formOpen: true,
loading: false,
workerCount: 1
});
expect(wrapper).toMatchSnapshot();

wrapper.find('WorkersForm').prop('onClose')(7);
wrapper.update();
expect(wrapper).toMatchSnapshot();
});

+ 52
- 0
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/WorkersForm-test.js Vedi File

@@ -0,0 +1,52 @@
/*
* SonarQube
* Copyright (C) 2009-2017 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.
*/
// @flow
import React from 'react';
import { shallow } from 'enzyme';
import WorkersForm from '../WorkersForm';
import { submit, doAsync } from '../../../../helpers/testUtils';

jest.mock('../../../../api/ce', () => ({
setWorkerCount: () => Promise.resolve()
}));

it('changes select', () => {
const wrapper = shallow(<WorkersForm onClose={jest.fn()} workerCount={1} />);
expect(wrapper).toMatchSnapshot();

wrapper.find('Select').prop('onChange')({ value: 7 });
wrapper.update();
expect(wrapper).toMatchSnapshot();
});

it('returns new worker count', () => {
const onClose = jest.fn();
const wrapper = shallow(<WorkersForm onClose={onClose} workerCount={1} />);
// $FlowFixMe
wrapper.instance().mounted = true;
wrapper.find('Select').prop('onChange')({ value: 7 });

wrapper.update();
submit(wrapper.find('form'));

return doAsync(() => {
expect(onClose).toBeCalled();
});
});

+ 175
- 0
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Workers-test.js.snap Vedi File

@@ -0,0 +1,175 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`opens form 1`] = `
<div>
background_tasks.number_of_workers
<strong
className="little-spacer-left"
>
1
</strong>
<Tooltip
overlay="background_tasks.change_number_of_workers"
placement="bottom"
>
<a
className="icon-edit spacer-left"
href="#"
onClick={[Function]}
/>
</Tooltip>
</div>
`;

exports[`opens form 2`] = `
<div>
background_tasks.number_of_workers
<strong
className="little-spacer-left"
>
1
</strong>
<Tooltip
overlay="background_tasks.change_number_of_workers"
placement="bottom"
>
<a
className="icon-edit spacer-left"
href="#"
onClick={[Function]}
/>
</Tooltip>
<WorkersForm
onClose={[Function]}
workerCount={1}
/>
</div>
`;

exports[`renders 1`] = `
<div>
background_tasks.number_of_workers
<i
className="spinner little-spacer-left"
/>
</div>
`;

exports[`renders 2`] = `
<div>
background_tasks.number_of_workers
<strong
className="little-spacer-left"
>
1
</strong>
<Tooltip
overlay="background_tasks.change_number_of_workers"
placement="bottom"
>
<a
className="icon-edit spacer-left"
href="#"
onClick={[Function]}
/>
</Tooltip>
</div>
`;

exports[`renders 3`] = `
<div>
background_tasks.number_of_workers
<strong
className="little-spacer-left"
>
1
</strong>
<a
className="button button-promote spacer-left"
href="https://redirect.sonarsource.com/plugins/governance.html"
target="_blank"
>
background_tasks.add_more_with_governance
</a>
</div>
`;

exports[`renders 4`] = `
<div>
<Tooltip
overlay="background_tasks.number_of_workers.warning"
placement="bottom"
>
<i
className="icon-alert-warn little-spacer-right bt-workers-warning-icon"
/>
</Tooltip>
background_tasks.number_of_workers
<strong
className="little-spacer-left"
>
2
</strong>
<a
className="button button-promote spacer-left"
href="https://redirect.sonarsource.com/plugins/governance.html"
target="_blank"
>
background_tasks.add_more_with_governance
</a>
</div>
`;

exports[`updates worker count 1`] = `
<div>
background_tasks.number_of_workers
<strong
className="little-spacer-left"
>
1
</strong>
<Tooltip
overlay="background_tasks.change_number_of_workers"
placement="bottom"
>
<a
className="icon-edit spacer-left"
href="#"
onClick={[Function]}
/>
</Tooltip>
<WorkersForm
onClose={[Function]}
workerCount={1}
/>
</div>
`;

exports[`updates worker count 2`] = `
<div>
<Tooltip
overlay="background_tasks.number_of_workers.warning"
placement="bottom"
>
<i
className="icon-alert-warn little-spacer-right bt-workers-warning-icon"
/>
</Tooltip>
background_tasks.number_of_workers
<strong
className="little-spacer-left"
>
7
</strong>
<Tooltip
overlay="background_tasks.change_number_of_workers"
placement="bottom"
>
<a
className="icon-edit spacer-left"
href="#"
onClick={[Function]}
/>
</Tooltip>
</div>
`;

+ 283
- 0
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/WorkersForm-test.js.snap Vedi File

@@ -0,0 +1,283 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`changes select 1`] = `
<Modal
ariaHideApp={true}
className="modal"
closeTimeoutMS={0}
contentLabel="background_tasks.change_number_of_workers"
isOpen={true}
onRequestClose={[Function]}
overlayClassName="modal-overlay"
parentSelector={[Function]}
portalClassName="ReactModalPortal"
shouldCloseOnOverlayClick={true}
>
<header
className="modal-head"
>
<h2>
background_tasks.change_number_of_workers
</h2>
</header>
<form
onSubmit={[Function]}
>
<div
className="modal-body"
>
<Select
addLabelText="Add \\"{label}\\"?"
arrowRenderer={[Function]}
autosize={true}
backspaceRemoves={true}
backspaceToRemoveMessage="Press backspace to remove {label}"
className="input-tiny spacer-top"
clearAllText="Clear all"
clearValueText="Clear value"
clearable={false}
delimiter=","
disabled={false}
escapeClearsValue={true}
filterOptions={[Function]}
ignoreAccents={true}
ignoreCase={true}
inputProps={Object {}}
isLoading={false}
joinValues={false}
labelKey="label"
matchPos="any"
matchProp="any"
menuBuffer={0}
menuRenderer={[Function]}
multi={false}
noResultsText="No results found"
onBlurResetsInput={true}
onChange={[Function]}
onCloseResetsInput={true}
openAfterFocus={false}
optionComponent={[Function]}
options={
Array [
Object {
"label": 1,
"value": 1,
},
Object {
"label": 2,
"value": 2,
},
Object {
"label": 3,
"value": 3,
},
Object {
"label": 4,
"value": 4,
},
Object {
"label": 5,
"value": 5,
},
Object {
"label": 6,
"value": 6,
},
Object {
"label": 7,
"value": 7,
},
Object {
"label": 8,
"value": 8,
},
Object {
"label": 9,
"value": 9,
},
Object {
"label": 10,
"value": 10,
},
]
}
pageSize={5}
placeholder="Select..."
required={false}
scrollMenuIntoView={true}
searchable={false}
simpleValue={false}
tabSelectsValue={true}
value={1}
valueComponent={[Function]}
valueKey="value"
/>
<div
className="big-spacer-top alert alert-success markdown"
>
background_tasks.change_number_of_workers.hint
</div>
</div>
<footer
className="modal-foot"
>
<div>
<button
disabled={false}
type="submit"
>
save
</button>
<button
className="button-link"
onClick={[Function]}
type="reset"
>
cancel
</button>
</div>
</footer>
</form>
</Modal>
`;

exports[`changes select 2`] = `
<Modal
ariaHideApp={true}
className="modal"
closeTimeoutMS={0}
contentLabel="background_tasks.change_number_of_workers"
isOpen={true}
onRequestClose={[Function]}
overlayClassName="modal-overlay"
parentSelector={[Function]}
portalClassName="ReactModalPortal"
shouldCloseOnOverlayClick={true}
>
<header
className="modal-head"
>
<h2>
background_tasks.change_number_of_workers
</h2>
</header>
<form
onSubmit={[Function]}
>
<div
className="modal-body"
>
<Select
addLabelText="Add \\"{label}\\"?"
arrowRenderer={[Function]}
autosize={true}
backspaceRemoves={true}
backspaceToRemoveMessage="Press backspace to remove {label}"
className="input-tiny spacer-top"
clearAllText="Clear all"
clearValueText="Clear value"
clearable={false}
delimiter=","
disabled={false}
escapeClearsValue={true}
filterOptions={[Function]}
ignoreAccents={true}
ignoreCase={true}
inputProps={Object {}}
isLoading={false}
joinValues={false}
labelKey="label"
matchPos="any"
matchProp="any"
menuBuffer={0}
menuRenderer={[Function]}
multi={false}
noResultsText="No results found"
onBlurResetsInput={true}
onChange={[Function]}
onCloseResetsInput={true}
openAfterFocus={false}
optionComponent={[Function]}
options={
Array [
Object {
"label": 1,
"value": 1,
},
Object {
"label": 2,
"value": 2,
},
Object {
"label": 3,
"value": 3,
},
Object {
"label": 4,
"value": 4,
},
Object {
"label": 5,
"value": 5,
},
Object {
"label": 6,
"value": 6,
},
Object {
"label": 7,
"value": 7,
},
Object {
"label": 8,
"value": 8,
},
Object {
"label": 9,
"value": 9,
},
Object {
"label": 10,
"value": 10,
},
]
}
pageSize={5}
placeholder="Select..."
required={false}
scrollMenuIntoView={true}
searchable={false}
simpleValue={false}
tabSelectsValue={true}
value={7}
valueComponent={[Function]}
valueKey="value"
/>
<div
className="big-spacer-top alert alert-success markdown"
>
background_tasks.change_number_of_workers.hint
</div>
</div>
<footer
className="modal-foot"
>
<div>
<button
disabled={false}
type="submit"
>
save
</button>
<button
className="button-link"
onClick={[Function]}
type="reset"
>
cancel
</button>
</div>
</footer>
</form>
</Modal>
`;

+ 12
- 0
server/sonar-web/src/main/less/init/forms.less Vedi File

@@ -254,6 +254,18 @@ input[type="submit"].button-grey {
padding: 0 6px;
}

.button-promote,
input[type="submit"].button-promote {
border-color: #5041d2;
background-color: #5041d2;
color: #fff;
transition: background-color 0.3s ease;

&:hover, &:focus, &.active {
background-color: darken(#5041d2, 10%);
}
}

.button-group {
display: inline-block;
vertical-align: middle;

+ 6
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Vedi File

@@ -2759,6 +2759,12 @@ background_tasks.pending=pending
background_tasks.failures=still failing
background_tasks.in_progress_duration=Duration of the current task in progress.

background_tasks.number_of_workers=Number of Workers:
background_tasks.number_of_workers.warning=Configuring additional workers without first vertically scaling your server could have negative performance impacts.
background_tasks.change_number_of_workers=Edit CE Workers
background_tasks.change_number_of_workers.hint=If your queue backs up behind the analysis reports from large projects, increasing the number of Compute Engine workers will allow you to take full advantage of having configured increased Compute Engine memory on a multi-core server (vertical scaling).
background_tasks.add_more_with_governance=Add more with Governance



#------------------------------------------------------------------------------

Loading…
Annulla
Salva