*/
// @flow
import { getJSON, post } from '../helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
export const getActivity = (data?: Object): Promise<*> => getJSON('/api/ce/activity', data);
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);
.bt-search-form-right {
margin-left: auto !important;
}
+
+.bt-workers-warning-icon {
+ position: relative;
+ top: -1px;
+}
+
+.bt-workers-warning-icon::before {
+ color: #d3d3d3;
+}
return (
<div className="page page-limited">
<Helmet title={translate('background_tasks.page')} />
- <Header />
+ <Header component={component} />
<Stats
component={component}
*/
/* @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;
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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();
+});
--- /dev/null
+/*
+ * 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();
+ });
+});
--- /dev/null
+// 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>
+`;
--- /dev/null
+// 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>
+`;
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;
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
+
#------------------------------------------------------------------------------