diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-07-06 14:37:46 +0200 |
---|---|---|
committer | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2017-07-17 10:52:47 +0200 |
commit | 373c5888e080843bf252bb4f776e8c17c4b667e2 (patch) | |
tree | e50a97eb8355ac4cf65f1a957fe07185953d63a3 /server/sonar-web/src | |
parent | 0839c4ba9cf741764c885ce6ba544bc6b1a82410 (diff) | |
download | sonarqube-373c5888e080843bf252bb4f776e8c17c4b667e2.tar.gz sonarqube-373c5888e080843bf252bb4f776e8c17c4b667e2.zip |
SONAR-9508 Display worker counter in background tasks page
Diffstat (limited to 'server/sonar-web/src')
11 files changed, 856 insertions, 5 deletions
diff --git a/server/sonar-web/src/main/js/api/ce.js b/server/sonar-web/src/main/js/api/ce.js index bc4eb2283e2..4fa75478b1d 100644 --- a/server/sonar-web/src/main/js/api/ce.js +++ b/server/sonar-web/src/main/js/api/ce.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css b/server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css index 2bad2aba5a0..a957b5f0250 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css +++ b/server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css @@ -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; +} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js index 4062eaea41a..a26112033ff 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Header.js b/server/sonar-web/src/main/js/apps/background-tasks/components/Header.js index c775e76f17a..e91b1b1188b 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Header.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Header.js @@ -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; +} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.js b/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.js new file mode 100644 index 00000000000..b37622a54f7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/WorkersForm.js b/server/sonar-web/src/main/js/apps/background-tasks/components/WorkersForm.js new file mode 100644 index 00000000000..2f66fe8ab3d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/WorkersForm.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/Workers-test.js b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/Workers-test.js new file mode 100644 index 00000000000..5ad06439c4a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/Workers-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/WorkersForm-test.js b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/WorkersForm-test.js new file mode 100644 index 00000000000..f821ba6fb32 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/WorkersForm-test.js @@ -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(); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Workers-test.js.snap b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Workers-test.js.snap new file mode 100644 index 00000000000..7951ffa70e3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Workers-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/WorkersForm-test.js.snap b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/WorkersForm-test.js.snap new file mode 100644 index 00000000000..35ba8f86128 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/WorkersForm-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/less/init/forms.less b/server/sonar-web/src/main/less/init/forms.less index 7852cb8b2c7..1a0a37e5e32 100644 --- a/server/sonar-web/src/main/less/init/forms.less +++ b/server/sonar-web/src/main/less/init/forms.less @@ -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; |