diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2021-09-08 16:51:02 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-09-15 20:03:23 +0000 |
commit | 805294024fab786fca6076daeadd18afaadef26b (patch) | |
tree | 57aae18c8cceff39aa91a065b8a6ce098cade991 /server/sonar-web/src/main/js | |
parent | b8ed6aa4f3025e69be637ac47d0fb26643a69377 (diff) | |
download | sonarqube-805294024fab786fca6076daeadd18afaadef26b.tar.gz sonarqube-805294024fab786fca6076daeadd18afaadef26b.zip |
SONAR-15376 Make secured settings hidden when set
Diffstat (limited to 'server/sonar-web/src/main/js')
46 files changed, 1025 insertions, 418 deletions
diff --git a/server/sonar-web/src/main/js/apps/about/actions.ts b/server/sonar-web/src/main/js/apps/about/actions.ts index 02c0e9fea40..b1cd8ec733f 100644 --- a/server/sonar-web/src/main/js/apps/about/actions.ts +++ b/server/sonar-web/src/main/js/apps/about/actions.ts @@ -25,7 +25,7 @@ export function fetchAboutPageSettings() { return (dispatch: Dispatch) => { const keys = ['sonar.lf.aboutText']; return getValues({ keys: keys.join() }).then(values => { - dispatch(receiveValues(values)); + dispatch(receiveValues(keys, values)); }); }; } diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx index b33011d4050..651c516b531 100644 --- a/server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx +++ b/server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx @@ -47,7 +47,7 @@ export class AuditApp extends React.PureComponent<Props, State> { componentDidMount() { const { hasGovernanceExtension } = this.props; if (hasGovernanceExtension) { - this.props.fetchValues('sonar.dbcleaner.auditHousekeeping'); + this.props.fetchValues(['sonar.dbcleaner.auditHousekeeping']); } } diff --git a/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts index 38d70015b55..5e9861a8cdd 100644 --- a/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts @@ -21,7 +21,8 @@ import { mockDefinition } from '../../../helpers/mocks/settings'; import { Setting, SettingCategoryDefinition, - SettingFieldDefinition + SettingFieldDefinition, + SettingType } from '../../../types/settings'; import { buildSettingLink, getDefaultValue, getEmptyValue } from '../utils'; @@ -42,7 +43,7 @@ describe('#getEmptyValue()', () => { it('should work for property sets', () => { const setting: SettingCategoryDefinition = { ...settingDefinition, - type: 'PROPERTY_SET', + type: SettingType.PROPERTY_SET, fields }; expect(getEmptyValue(setting)).toEqual([{ foo: '', bar: null }]); @@ -51,7 +52,7 @@ describe('#getEmptyValue()', () => { it('should work for multi values string', () => { const setting: SettingCategoryDefinition = { ...settingDefinition, - type: 'STRING', + type: SettingType.STRING, multiValues: true }; expect(getEmptyValue(setting)).toEqual(['']); @@ -60,7 +61,7 @@ describe('#getEmptyValue()', () => { it('should work for multi values boolean', () => { const setting: SettingCategoryDefinition = { ...settingDefinition, - type: 'BOOLEAN', + type: SettingType.BOOLEAN, multiValues: true }; expect(getEmptyValue(setting)).toEqual([null]); @@ -75,7 +76,8 @@ describe('#getDefaultValue()', () => { 'should work for boolean field when passing "%s"', (parentValue?: string, expected?: string) => { const setting: Setting = { - definition: { key: 'test', options: [], type: 'BOOLEAN' }, + hasValue: true, + definition: { key: 'test', options: [], type: SettingType.BOOLEAN }, parentValue, key: 'test' }; diff --git a/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx index 7f9994de439..f237d48a304 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx @@ -189,26 +189,26 @@ export class Definition extends React.PureComponent<Props, State> { </span> )} </div> - - <Input - hasValueChanged={hasValueChanged} - onCancel={this.handleCancel} - onChange={this.handleChange} - onSave={this.handleSave} - setting={setting} - value={effectiveValue} - /> - - <DefinitionActions - changedValue={changedValue} - hasError={hasError} - hasValueChanged={hasValueChanged} - isDefault={isDefault} - onCancel={this.handleCancel} - onReset={this.handleReset} - onSave={this.handleSave} - setting={setting} - /> + <form> + <Input + hasValueChanged={hasValueChanged} + onCancel={this.handleCancel} + onChange={this.handleChange} + onSave={this.handleSave} + setting={setting} + value={effectiveValue} + /> + <DefinitionActions + changedValue={changedValue} + hasError={hasError} + hasValueChanged={hasValueChanged} + isDefault={isDefault} + onCancel={this.handleCancel} + onReset={this.handleReset} + onSave={this.handleSave} + setting={setting} + /> + </form> </div> </div> ); diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx b/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx index 6ccf74d70a2..c995f082b7f 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx @@ -22,7 +22,7 @@ import { Button, ResetButtonLink, SubmitButton } from '../../../components/contr import Modal from '../../../components/controls/Modal'; import { translate } from '../../../helpers/l10n'; import { Setting } from '../../../types/settings'; -import { getDefaultValue, getSettingValue, isEmptyValue } from '../utils'; +import { getDefaultValue, isEmptyValue } from '../utils'; type Props = { changedValue: string; @@ -74,13 +74,10 @@ export default class DefinitionActions extends React.PureComponent<Props, State> } render() { - const { setting, isDefault, changedValue, hasValueChanged } = this.props; - - const hasValueToResetTo = !isEmptyValue(setting.definition, getSettingValue(setting)); + const { setting, changedValue, isDefault, hasValueChanged } = this.props; const hasBeenChangedToEmptyValue = changedValue != null && isEmptyValue(setting.definition, changedValue); - const showReset = - hasValueToResetTo && (hasBeenChangedToEmptyValue || (!isDefault && !hasValueChanged)); + const showReset = hasBeenChangedToEmptyValue || (!isDefault && setting.hasValue); return ( <> diff --git a/server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx b/server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx index 685699d15a6..5149787010a 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx @@ -50,7 +50,7 @@ interface State { loading: boolean; } -export class App extends React.PureComponent<Props & WithRouterProps, State> { +export class SettingsApp extends React.PureComponent<Props & WithRouterProps, State> { mounted = false; state: State = { loading: true }; @@ -150,4 +150,4 @@ const mapStateToProps = (state: Store) => ({ const mapDispatchToProps = { fetchSettings: fetchSettings as any }; -export default connect(mapStateToProps, mapDispatchToProps)(App); +export default connect(mapStateToProps, mapDispatchToProps)(SettingsApp); diff --git a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx index dfec70e0e86..be3b88a7c78 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx @@ -75,7 +75,7 @@ export class SubCategoryDefinitionsList extends React.PureComponent< }; fetchValues() { - const keys = this.props.settings.map(setting => setting.definition.key).join(); + const keys = this.props.settings.map(setting => setting.definition.key); return this.props.fetchValues(keys, this.props.component && this.props.component.key); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx index 56841809b8b..06ec031c4fb 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx @@ -22,6 +22,8 @@ import * as React from 'react'; import { mockSetting } from '../../../../helpers/mocks/settings'; import { waitAndUpdate } from '../../../../helpers/testUtils'; import { Definition } from '../Definition'; +import DefinitionActions from '../DefinitionActions'; +import Input from '../inputs/Input'; const setting = mockSetting(); @@ -42,7 +44,7 @@ it('should correctly handle change of value', () => { const changeValue = jest.fn(); const checkValue = jest.fn(); const wrapper = shallowRender({ changeValue, checkValue }); - wrapper.find('Input').prop<Function>('onChange')(5); + wrapper.find(Input).prop<Function>('onChange')(5); expect(changeValue).toHaveBeenCalledWith(setting.definition.key, 5); expect(checkValue).toHaveBeenCalledWith(setting.definition.key); }); @@ -51,7 +53,7 @@ it('should correctly cancel value change', () => { const cancelChange = jest.fn(); const passValidation = jest.fn(); const wrapper = shallowRender({ cancelChange, passValidation }); - wrapper.find('Input').prop<Function>('onCancel')(); + wrapper.find(Input).prop<Function>('onCancel')(); expect(cancelChange).toHaveBeenCalledWith(setting.definition.key); expect(passValidation).toHaveBeenCalledWith(setting.definition.key); }); @@ -59,7 +61,7 @@ it('should correctly cancel value change', () => { it('should correctly save value change', async () => { const saveValue = jest.fn().mockResolvedValue({}); const wrapper = shallowRender({ changedValue: 10, saveValue }); - wrapper.find('DefinitionActions').prop<Function>('onSave')(); + wrapper.find(DefinitionActions).prop('onSave')(); await waitAndUpdate(wrapper); expect(saveValue).toHaveBeenCalledWith(setting.definition.key, undefined); expect(wrapper.find('AlertSuccessIcon').exists()).toBe(true); @@ -72,7 +74,7 @@ it('should correctly reset', async () => { const cancelChange = jest.fn(); const resetValue = jest.fn().mockResolvedValue({}); const wrapper = shallowRender({ cancelChange, changedValue: 10, resetValue }); - wrapper.find('DefinitionActions').prop<Function>('onReset')(); + wrapper.find(DefinitionActions).prop('onReset')(); await waitAndUpdate(wrapper); expect(resetValue).toHaveBeenCalledWith(setting.definition.key, undefined); expect(cancelChange).toHaveBeenCalledWith(setting.definition.key); diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionActions-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionActions-test.tsx index 76efb7f237d..cd408abedae 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionActions-test.tsx @@ -19,7 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { SettingCategoryDefinition } from '../../../../types/settings'; +import { SettingCategoryDefinition, SettingType } from '../../../../types/settings'; import DefinitionActions from '../DefinitionActions'; const definition: SettingCategoryDefinition = { @@ -30,11 +30,12 @@ const definition: SettingCategoryDefinition = { name: 'foobar', options: [], subCategory: 'bar', - type: 'STRING' + type: SettingType.STRING }; const settings = { key: 'key', + hasValue: true, definition, value: 'baz' }; diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/AppContainer-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-test.tsx index 03a755a6277..71752a0ae87 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/AppContainer-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-test.tsx @@ -35,7 +35,7 @@ import { NEW_CODE_PERIOD_CATEGORY, PULL_REQUEST_DECORATION_BINDING_CATEGORY } from '../AdditionalCategoryKeys'; -import { App } from '../AppContainer'; +import { SettingsApp } from '../SettingsApp'; jest.mock('../../../../helpers/pages', () => ({ addSideBarClass: jest.fn(), @@ -105,9 +105,9 @@ it('should render pull request decoration binding correctly', async () => { expect(wrapper).toMatchSnapshot(); }); -function shallowRender(props: Partial<App['props']> = {}) { +function shallowRender(props: Partial<SettingsApp['props']> = {}) { return shallow( - <App + <SettingsApp defaultCategory="general" fetchSettings={jest.fn().mockResolvedValue({})} location={mockLocation()} diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap index 1e27f6b5fa9..c7425dafd23 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap @@ -34,50 +34,54 @@ exports[`should render correctly 1`] = ` <div className="settings-definition-state" /> - <Input - hasValueChanged={false} - onCancel={[Function]} - onChange={[Function]} - onSave={[Function]} - setting={ - Object { - "definition": Object { - "description": "When Foo then Bar", + <form> + <Input + hasValueChanged={false} + onCancel={[Function]} + onChange={[Function]} + onSave={[Function]} + setting={ + Object { + "definition": Object { + "description": "When Foo then Bar", + "key": "foo", + "name": "Foo setting", + "options": Array [], + "type": "INTEGER", + }, + "hasValue": true, + "inherited": true, "key": "foo", - "name": "Foo setting", - "options": Array [], - "type": "INTEGER", - }, - "inherited": true, - "key": "foo", - "value": "42", + "value": "42", + } } - } - value="42" - /> - <DefinitionActions - changedValue={null} - hasError={false} - hasValueChanged={false} - isDefault={true} - onCancel={[Function]} - onReset={[Function]} - onSave={[Function]} - setting={ - Object { - "definition": Object { - "description": "When Foo then Bar", + value="42" + /> + <DefinitionActions + changedValue={null} + hasError={false} + hasValueChanged={false} + isDefault={true} + onCancel={[Function]} + onReset={[Function]} + onSave={[Function]} + setting={ + Object { + "definition": Object { + "description": "When Foo then Bar", + "key": "foo", + "name": "Foo setting", + "options": Array [], + "type": "INTEGER", + }, + "hasValue": true, + "inherited": true, "key": "foo", - "name": "Foo setting", - "options": Array [], - "type": "INTEGER", - }, - "inherited": true, - "key": "foo", - "value": "42", + "value": "42", + } } - } - /> + /> + </form> </div> </div> `; diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AppContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsApp-test.tsx.snap index 03291c1f12b..03291c1f12b 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AppContainer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsApp-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SubCategoryDefinitionsList-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SubCategoryDefinitionsList-test.tsx.snap index cc29119b1e9..c0449cc8a04 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SubCategoryDefinitionsList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SubCategoryDefinitionsList-test.tsx.snap @@ -28,6 +28,7 @@ exports[`should render correctly 1`] = ` "subCategory": "email", "type": "INTEGER", }, + "hasValue": true, "inherited": true, "key": "foo", "value": "42", @@ -59,6 +60,7 @@ exports[`should render correctly 1`] = ` "options": Array [], "subCategory": "qg", }, + "hasValue": true, "inherited": true, "key": "foo", "value": "42", @@ -96,6 +98,7 @@ exports[`should render correctly: subcategory 1`] = ` "options": Array [], "subCategory": "qg", }, + "hasValue": true, "inherited": true, "key": "foo", "value": "42", diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.tsx index 32e18f71f19..953c4212556 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.tsx @@ -18,21 +18,38 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { DefaultInputProps, isCategoryDefinition } from '../../utils'; +import { SettingType } from '../../../../types/settings'; +import { + DefaultInputProps, + DefaultSpecializedInputProps, + getUniqueName, + isCategoryDefinition, + isDefaultOrInherited, + isSecuredDefinition +} from '../../utils'; +import InputForSecured from './InputForSecured'; import MultiValueInput from './MultiValueInput'; import PrimitiveInput from './PrimitiveInput'; import PropertySetInput from './PropertySetInput'; export default function Input(props: DefaultInputProps) { - const { definition } = props.setting; + const { setting } = props; + const { definition } = setting; + const name = getUniqueName(definition); + + let Input: React.ComponentType<DefaultSpecializedInputProps> = PrimitiveInput; if (isCategoryDefinition(definition) && definition.multiValues) { - return <MultiValueInput {...props} />; + Input = MultiValueInput; + } + + if (definition.type === SettingType.PROPERTY_SET) { + Input = PropertySetInput; } - if (definition.type === 'PROPERTY_SET') { - return <PropertySetInput {...props} />; + if (isSecuredDefinition(definition)) { + return <InputForSecured input={Input} {...props} />; } - return <PrimitiveInput {...props} />; + return <Input {...props} name={name} isDefault={isDefaultOrInherited(setting)} />; } diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.tsx index 2c5818efcdd..b7be3d7573c 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.tsx @@ -22,5 +22,5 @@ import { DefaultSpecializedInputProps } from '../../utils'; import SimpleInput from './SimpleInput'; export default function InputForNumber(props: DefaultSpecializedInputProps) { - return <SimpleInput {...props} className="input-small" type="text" />; + return <SimpleInput className="input-small" type="text" {...props} />; } diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.tsx index c63b3106ea4..b93e4ac2384 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.tsx @@ -18,76 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { colors } from '../../../../app/theme'; -import { Button } from '../../../../components/controls/buttons'; -import LockIcon from '../../../../components/icons/LockIcon'; -import { translate } from '../../../../helpers/l10n'; import { DefaultSpecializedInputProps } from '../../utils'; +import SimpleInput from './SimpleInput'; -interface State { - changing: boolean; -} - -export default class InputForPassword extends React.PureComponent< - DefaultSpecializedInputProps, - State -> { - state: State = { - changing: !this.props.value - }; - - componentWillReceiveProps(nextProps: DefaultSpecializedInputProps) { - /* - * Reset `changing` if: - * - the value is reset (valueChanged -> !valueChanged) - * or - * - the value changes from outside the input (i.e. store update/reset/cancel) - */ - if ( - (this.props.hasValueChanged || this.props.value !== nextProps.value) && - !nextProps.hasValueChanged - ) { - this.setState({ changing: !nextProps.value }); - } - } - - handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { - this.props.onChange(event.target.value); - }; - - handleChangeClick = () => { - this.setState({ changing: true }); - }; - - renderInput() { - return ( - <form> - <input className="hidden" type="password" /> - <input - autoComplete="off" - autoFocus={this.state.changing && this.props.value} - className="js-password-input settings-large-input text-top" - name={this.props.name} - onChange={this.handleInputChange} - type="password" - value={this.props.value} - /> - </form> - ); - } - - render() { - if (this.state.changing) { - return this.renderInput(); - } - - return ( - <> - <LockIcon className="text-middle big-spacer-right" fill={colors.gray60} /> - <Button className="text-middle" onClick={this.handleChangeClick}> - {translate('change_verb')} - </Button> - </> - ); - } +export default function InputForPassword(props: DefaultSpecializedInputProps) { + return ( + <SimpleInput {...props} className="settings-large-input" type="password" autoComplete="off" /> + ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSecured.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSecured.tsx new file mode 100644 index 00000000000..eddff155556 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSecured.tsx @@ -0,0 +1,103 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { colors } from '../../../../app/theme'; +import { Button } from '../../../../components/controls/buttons'; +import LockIcon from '../../../../components/icons/LockIcon'; +import { translate } from '../../../../helpers/l10n'; +import { + DefaultInputProps, + DefaultSpecializedInputProps, + getUniqueName, + isDefaultOrInherited +} from '../../utils'; + +interface State { + changing: boolean; +} + +interface Props extends DefaultInputProps { + input: React.ComponentType<DefaultSpecializedInputProps>; +} + +export default class InputForSecured extends React.PureComponent<Props, State> { + state: State = { + changing: !this.props.setting.hasValue + }; + + componentWillReceiveProps(nextProps: Props) { + /* + * Reset `changing` if: + * - the value is reset (valueChanged -> !valueChanged) + * or + * - the value changes from outside the input (i.e. store update/reset/cancel) + */ + if ( + (this.props.hasValueChanged || this.props.setting !== nextProps.setting) && + !nextProps.hasValueChanged + ) { + this.setState({ changing: !nextProps.setting.hasValue }); + } + } + + handleInputChange = (value: string) => { + this.props.onChange(value); + }; + + handleChangeClick = () => { + this.setState({ changing: true }); + }; + + renderInput() { + const { input: Input, setting, value } = this.props; + const name = getUniqueName(setting.definition); + return ( + // The input hidden will prevent browser asking for saving login information + <> + <input className="hidden" type="password" /> + <Input + autoComplete="off" + className="js-setting-input settings-large-input" + isDefault={isDefaultOrInherited(setting)} + name={name} + onChange={this.handleInputChange} + setting={setting} + type="password" + value={value} + /> + </> + ); + } + + render() { + if (this.state.changing) { + return this.renderInput(); + } + + return ( + <> + <LockIcon className="text-middle big-spacer-right" fill={colors.gray60} /> + <Button className="text-middle" onClick={this.handleChangeClick}> + {translate('change_verb')} + </Button> + </> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.tsx index 32ab643f671..0084038c5de 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.tsx @@ -22,5 +22,5 @@ import { DefaultSpecializedInputProps } from '../../utils'; import SimpleInput from './SimpleInput'; export default function InputForString(props: DefaultSpecializedInputProps) { - return <SimpleInput {...props} className="settings-large-input" type="text" />; + return <SimpleInput className="settings-large-input" type="text" {...props} />; } diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx index e6bb87e44ae..6a40749310d 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx @@ -19,10 +19,10 @@ */ import * as React from 'react'; import { DeleteButton } from '../../../../components/controls/buttons'; -import { DefaultInputProps, getEmptyValue } from '../../utils'; +import { DefaultSpecializedInputProps, getEmptyValue } from '../../utils'; import PrimitiveInput from './PrimitiveInput'; -export default class MultiValueInput extends React.PureComponent<DefaultInputProps> { +export default class MultiValueInput extends React.PureComponent<DefaultSpecializedInputProps> { ensureValue = () => { return this.props.value || []; }; @@ -40,17 +40,15 @@ export default class MultiValueInput extends React.PureComponent<DefaultInputPro }; renderInput(value: any, index: number, isLast: boolean) { - const { setting } = this.props; + const { setting, isDefault, name } = this.props; return ( <li className="spacer-bottom" key={index}> <PrimitiveInput + isDefault={isDefault} + name={name} hasValueChanged={this.props.hasValueChanged} onChange={value => this.handleSingleInputChange(index, value)} - setting={{ - ...setting, - definition: { ...setting.definition, multiValues: false }, - values: undefined - }} + setting={setting} value={value} /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.tsx index 59a221e3bc7..e3b94af38e8 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.tsx @@ -19,12 +19,7 @@ */ import * as React from 'react'; import { SettingType } from '../../../../types/settings'; -import { - DefaultInputProps, - DefaultSpecializedInputProps, - getUniqueName, - isDefaultOrInherited -} from '../../utils'; +import { DefaultSpecializedInputProps } from '../../utils'; import InputForBoolean from './InputForBoolean'; import InputForJSON from './InputForJSON'; import InputForNumber from './InputForNumber'; @@ -33,42 +28,30 @@ import InputForSingleSelectList from './InputForSingleSelectList'; import InputForString from './InputForString'; import InputForText from './InputForText'; -const typeMapping: { - [type in SettingType]?: React.ComponentType<DefaultSpecializedInputProps>; -} = { - STRING: InputForString, - TEXT: InputForText, - JSON: InputForJSON, - PASSWORD: InputForPassword, - BOOLEAN: InputForBoolean, - INTEGER: InputForNumber, - LONG: InputForNumber, - FLOAT: InputForNumber -}; - -interface Props extends DefaultInputProps { - name?: string; +function withOptions(options: string[]): React.ComponentType<DefaultSpecializedInputProps> { + return function Wrapped(props: DefaultSpecializedInputProps) { + return <InputForSingleSelectList options={options} {...props} />; + }; } -export default class PrimitiveInput extends React.PureComponent<Props> { - render() { - const { setting, ...other } = this.props; - const { definition } = setting; - - const name = this.props.name || getUniqueName(definition); +export default function PrimitiveInput(props: DefaultSpecializedInputProps) { + const { setting, name, isDefault, ...other } = props; + const { definition } = setting; + const typeMapping: { + [type in SettingType]?: React.ComponentType<DefaultSpecializedInputProps>; + } = { + STRING: InputForString, + TEXT: InputForText, + JSON: InputForJSON, + PASSWORD: InputForPassword, + BOOLEAN: InputForBoolean, + INTEGER: InputForNumber, + LONG: InputForNumber, + FLOAT: InputForNumber, + SINGLE_SELECT_LIST: withOptions(definition.options) + }; - if (definition.type === 'SINGLE_SELECT_LIST') { - return ( - <InputForSingleSelectList - isDefault={isDefaultOrInherited(setting)} - name={name} - options={definition.options} - {...other} - /> - ); - } + const InputComponent = (definition.type && typeMapping[definition.type]) || InputForString; - const InputComponent = (definition.type && typeMapping[definition.type]) || InputForString; - return <InputComponent isDefault={isDefaultOrInherited(setting)} name={name} {...other} />; - } + return <InputComponent isDefault={isDefault} name={name} setting={setting} {...other} />; } diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.tsx index 7db52e8b289..2b9efd2e7d9 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.tsx @@ -19,10 +19,15 @@ */ import * as React from 'react'; import { DeleteButton } from '../../../../components/controls/buttons'; -import { DefaultInputProps, getEmptyValue, getUniqueName, isCategoryDefinition } from '../../utils'; +import { + DefaultSpecializedInputProps, + getEmptyValue, + getUniqueName, + isCategoryDefinition +} from '../../utils'; import PrimitiveInput from './PrimitiveInput'; -export default class PropertySetInput extends React.PureComponent<DefaultInputProps> { +export default class PropertySetInput extends React.PureComponent<DefaultSpecializedInputProps> { ensureValue() { return this.props.value || []; } @@ -42,23 +47,31 @@ export default class PropertySetInput extends React.PureComponent<DefaultInputPr }; renderFields(fieldValues: any, index: number, isLast: boolean) { - const { setting } = this.props; + const { setting, isDefault } = this.props; const { definition } = setting; return ( <tr key={index}> {isCategoryDefinition(definition) && - definition.fields.map(field => ( - <td key={field.key}> - <PrimitiveInput - hasValueChanged={this.props.hasValueChanged} - name={getUniqueName(definition, field.key)} - onChange={value => this.handleInputChange(index, field.key, value)} - setting={{ ...setting, definition: field, value: fieldValues[field.key] }} - value={fieldValues[field.key]} - /> - </td> - ))} + definition.fields.map(field => { + const newSetting = { + ...setting, + definition: field, + value: fieldValues[field.key] + }; + return ( + <td key={field.key}> + <PrimitiveInput + isDefault={isDefault} + hasValueChanged={this.props.hasValueChanged} + name={getUniqueName(definition, field.key)} + onChange={value => this.handleInputChange(index, field.key, value)} + setting={newSetting} + value={fieldValues[field.key]} + /> + </td> + ); + })} <td className="thin nowrap text-middle"> {!isLast && ( <DeleteButton diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.tsx index 20c1d0bad81..2c5df6a506f 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.tsx @@ -22,8 +22,6 @@ import * as React from 'react'; import { DefaultSpecializedInputProps } from '../../utils'; interface Props extends DefaultSpecializedInputProps { - className?: string; - type: string; value: string | number; } @@ -41,14 +39,17 @@ export default class SimpleInput extends React.PureComponent<Props> { }; render() { + const { autoComplete, autoFocus, className, name, value = '', type } = this.props; return ( <input - className={classNames('text-top', this.props.className)} - name={this.props.name} + autoComplete={autoComplete} + autoFocus={autoFocus} + className={classNames('text-top', className)} + name={name} onChange={this.handleInputChange} onKeyDown={this.handleKeyDown} - type={this.props.type} - value={this.props.value || ''} + type={type} + value={value} /> ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.tsx index c800f03dee0..4e5aac8760b 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.tsx @@ -19,38 +19,42 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { Setting, SettingCategoryDefinition } from '../../../../../types/settings'; +import { mockDefinition, mockSetting } from '../../../../../helpers/mocks/settings'; +import { Setting, SettingType } from '../../../../../types/settings'; import { DefaultInputProps } from '../../../utils'; import Input from '../Input'; - -const settingValue = { - key: 'example' -}; - -const settingDefinition: SettingCategoryDefinition = { - category: 'general', - fields: [], - key: 'example', - options: [], - subCategory: 'Branches', - type: 'STRING' -}; +import InputForSecured from '../InputForSecured'; +import MultiValueInput from '../MultiValueInput'; +import PrimitiveInput from '../PrimitiveInput'; +import PropertySetInput from '../PropertySetInput'; it('should render PrimitiveInput', () => { - const setting = { ...settingValue, definition: settingDefinition }; const onChange = jest.fn(); - const input = shallowRender({ onChange, setting }).find('PrimitiveInput'); + const input = shallowRender({ onChange }).find(PrimitiveInput); + expect(input.length).toBe(1); + expect(input.prop('value')).toBe('foo'); + expect(input.prop('onChange')).toBe(onChange); +}); + +it('should render Secured input', () => { + const setting: Setting = mockSetting({ + key: 'foo.secured', + definition: mockDefinition({ key: 'foo.secured', type: SettingType.PROPERTY_SET }) + }); + const onChange = jest.fn(); + const input = shallowRender({ onChange, setting }).find(InputForSecured); expect(input.length).toBe(1); - expect(input.prop('setting')).toBe(setting); expect(input.prop('value')).toBe('foo'); expect(input.prop('onChange')).toBe(onChange); }); it('should render MultiValueInput', () => { - const setting = { ...settingValue, definition: { ...settingDefinition, multiValues: true } }; + const setting = mockSetting({ + definition: mockDefinition({ multiValues: true }) + }); const onChange = jest.fn(); const value = ['foo', 'bar']; - const input = shallowRender({ onChange, setting, value }).find('MultiValueInput'); + const input = shallowRender({ onChange, setting, value }).find(MultiValueInput); expect(input.length).toBe(1); expect(input.prop('setting')).toBe(setting); expect(input.prop('value')).toBe(value); @@ -58,14 +62,13 @@ it('should render MultiValueInput', () => { }); it('should render PropertySetInput', () => { - const setting: Setting = { - ...settingValue, - definition: { ...settingDefinition, type: 'PROPERTY_SET' } - }; + const setting: Setting = mockSetting({ + definition: mockDefinition({ type: SettingType.PROPERTY_SET }) + }); const onChange = jest.fn(); const value = [{ foo: 'bar' }]; - const input = shallowRender({ onChange, setting, value }).find('PropertySetInput'); + const input = shallowRender({ onChange, setting, value }).find(PropertySetInput); expect(input.length).toBe(1); expect(input.prop('setting')).toBe(setting); expect(input.prop('value')).toBe(value); @@ -73,12 +76,5 @@ it('should render PropertySetInput', () => { }); function shallowRender(props: Partial<DefaultInputProps> = {}) { - return shallow( - <Input - onChange={jest.fn()} - setting={{ ...settingValue, definition: settingDefinition }} - value="foo" - {...props} - /> - ); + return shallow(<Input onChange={jest.fn()} setting={mockSetting()} value="foo" {...props} />); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.tsx index 31685630cc7..65a8da2f87d 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockSetting } from '../../../../../helpers/mocks/settings'; import { DefaultSpecializedInputProps } from '../../../utils'; import InputForBoolean from '../InputForBoolean'; @@ -57,6 +58,13 @@ it('should call onChange', () => { function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) { return shallow( - <InputForBoolean isDefault={false} name="foo" onChange={jest.fn()} value={true} {...props} /> + <InputForBoolean + isDefault={false} + name="foo" + onChange={jest.fn()} + setting={mockSetting()} + value={true} + {...props} + /> ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForJSON-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForJSON-test.tsx index 2884097b537..f91b74b8716 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForJSON-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForJSON-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockSetting } from '../../../../../helpers/mocks/settings'; import { change } from '../../../../../helpers/testUtils'; import { DefaultSpecializedInputProps } from '../../../utils'; import InputForJSON from '../InputForJSON'; @@ -67,6 +68,13 @@ it('should handle ignore formatting if empty', () => { function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) { return shallow<InputForJSON>( - <InputForJSON isDefault={false} name="foo" onChange={jest.fn()} value="" {...props} /> + <InputForJSON + isDefault={false} + name="foo" + onChange={jest.fn()} + setting={mockSetting()} + value="" + {...props} + /> ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.tsx index ca1682699e2..ec65800f021 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.tsx @@ -19,13 +19,20 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockSetting } from '../../../../../helpers/mocks/settings'; import InputForNumber from '../InputForNumber'; import SimpleInput from '../SimpleInput'; it('should render SimpleInput', () => { const onChange = jest.fn(); const simpleInput = shallow( - <InputForNumber isDefault={false} name="foo" onChange={onChange} value={17} /> + <InputForNumber + isDefault={false} + name="foo" + onChange={onChange} + setting={mockSetting()} + value={17} + /> ).find(SimpleInput); expect(simpleInput.length).toBe(1); expect(simpleInput.prop('name')).toBe('foo'); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.tsx index bb1716d9879..216332ca6a2 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.tsx @@ -19,78 +19,24 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { change, click, submit } from '../../../../../helpers/testUtils'; -import { DefaultSpecializedInputProps } from '../../../utils'; +import { mockSetting } from '../../../../../helpers/mocks/settings'; import InputForPassword from '../InputForPassword'; +import SimpleInput from '../SimpleInput'; -it('should render lock icon, but no form', () => { +it('should render SimpleInput', () => { const onChange = jest.fn(); - const input = shallowRender({ onChange }); - - expect(input.find('LockIcon').length).toBe(1); - expect(input.find('form').length).toBe(0); -}); - -it('should open form', () => { - const onChange = jest.fn(); - const input = shallowRender({ onChange }); - const button = input.find('Button'); - expect(button.length).toBe(1); - - click(button); - expect(input.find('form').length).toBe(1); -}); - -it('should set value', () => { - const onChange = jest.fn(() => Promise.resolve()); - const input = shallowRender({ onChange }); - - click(input.find('Button')); - change(input.find('.js-password-input'), 'secret'); - submit(input.find('form')); - expect(onChange).toBeCalledWith('secret'); -}); - -it('should show form when empty, and enable handle typing', () => { - const input = shallowRender({ value: '' }); - const onChange = (value: string) => input.setProps({ hasValueChanged: true, value }); - input.setProps({ onChange }); - - expect(input.find('form').length).toBe(1); - change(input.find('input.js-password-input'), 'hello'); - expect(input.find('form').length).toBe(1); - expect(input.find('input.js-password-input').prop('value')).toBe('hello'); -}); - -it('should handle value reset', () => { - const input = shallowRender({ hasValueChanged: true, value: 'whatever' }); - input.setState({ changing: true }); - - // reset - input.setProps({ hasValueChanged: false, value: 'original' }); - - expect(input.state('changing')).toBe(false); -}); - -it('should handle value reset to empty', () => { - const input = shallowRender({ hasValueChanged: true, value: 'whatever' }); - input.setState({ changing: true }); - - // outside change - input.setProps({ hasValueChanged: false, value: '' }); - - expect(input.state('changing')).toBe(true); -}); - -function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) { - return shallow<InputForPassword>( + const simpleInput = shallow( <InputForPassword - hasValueChanged={false} isDefault={false} name="foo" - onChange={jest.fn()} + onChange={onChange} + setting={mockSetting()} value="bar" - {...props} /> - ); -} + ).find(SimpleInput); + expect(simpleInput.length).toBe(1); + expect(simpleInput.prop('name')).toBe('foo'); + expect(simpleInput.prop('value')).toBe('bar'); + expect(simpleInput.prop('type')).toBe('password'); + expect(simpleInput.prop('onChange')).toBeDefined(); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSecured-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSecured-test.tsx new file mode 100644 index 00000000000..1d9eeb30f56 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSecured-test.tsx @@ -0,0 +1,96 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { shallow } from 'enzyme'; +import * as React from 'react'; +import { mockSetting } from '../../../../../helpers/mocks/settings'; +import { change, click } from '../../../../../helpers/testUtils'; +import InputForSecured from '../InputForSecured'; +import InputForString from '../InputForString'; + +it('should render lock icon, but no form', () => { + const onChange = jest.fn(); + const input = shallowRender({ onChange }); + + expect(input.find('LockIcon').length).toBe(1); + expect(input.find('input').length).toBe(0); +}); + +it('should open form', () => { + const onChange = jest.fn(); + const input = shallowRender({ onChange }); + const button = input.find('Button'); + expect(button.length).toBe(1); + + click(button); + expect(input.find('input').length).toBe(1); +}); + +it('should set value', () => { + const onChange = jest.fn(() => Promise.resolve()); + const input = shallowRender({ onChange }); + + click(input.find('Button')); + change(input.find(InputForString), 'secret'); + expect(onChange).toBeCalledWith('secret'); +}); + +it('should show input when empty, and enable handle typing', () => { + const input = shallowRender({ setting: mockSetting({ hasValue: false }) }); + const onChange = (value: string) => input.setProps({ hasValueChanged: true, value }); + input.setProps({ onChange }); + + expect(input.find('input').length).toBe(1); + change(input.find(InputForString), 'hello'); + expect(input.find('input').length).toBe(1); + expect(input.find(InputForString).prop('value')).toBe('hello'); +}); + +it('should handle value reset', () => { + const input = shallowRender({ hasValueChanged: true, value: 'whatever' }); + input.setState({ changing: true }); + + // reset + input.setProps({ hasValueChanged: false, value: 'original' }); + + expect(input.state('changing')).toBe(false); +}); + +it('should handle value reset to empty', () => { + const input = shallowRender({ hasValueChanged: true, value: 'whatever' }); + input.setState({ changing: true }); + + // outside change + input.setProps({ hasValueChanged: false, setting: mockSetting({ hasValue: false }) }); + + expect(input.state('changing')).toBe(true); +}); + +function shallowRender(props: Partial<InputForSecured['props']> = {}) { + return shallow<InputForSecured>( + <InputForSecured + input={InputForString} + hasValueChanged={false} + onChange={jest.fn()} + setting={mockSetting()} + value="bar" + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.tsx index 8dde11bfbc0..00d5fcb467a 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockSetting } from '../../../../../helpers/mocks/settings'; import { DefaultSpecializedInputProps } from '../../../utils'; import InputForSingleSelectList from '../InputForSingleSelectList'; @@ -53,6 +54,7 @@ function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) { name="foo" onChange={jest.fn()} options={['foo', 'bar', 'baz']} + setting={mockSetting()} value="bar" {...props} /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.tsx index 154d4dcd3d4..65e5235c4be 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.tsx @@ -19,13 +19,20 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockSetting } from '../../../../../helpers/mocks/settings'; import InputForString from '../InputForString'; import SimpleInput from '../SimpleInput'; it('should render SimpleInput', () => { const onChange = jest.fn(); const simpleInput = shallow( - <InputForString isDefault={false} name="foo" onChange={onChange} value="bar" /> + <InputForString + isDefault={false} + name="foo" + onChange={onChange} + setting={mockSetting()} + value="bar" + /> ).find(SimpleInput); expect(simpleInput.length).toBe(1); expect(simpleInput.prop('name')).toBe('foo'); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.tsx index 4b9a42653fb..1d12b2d64a0 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockSetting } from '../../../../../helpers/mocks/settings'; import { change } from '../../../../../helpers/testUtils'; import { DefaultSpecializedInputProps } from '../../../utils'; import InputForText from '../InputForText'; @@ -44,6 +45,13 @@ it('should call onChange', () => { function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) { return shallow( - <InputForText isDefault={false} name="foo" onChange={jest.fn()} value="bar" {...props} /> + <InputForText + isDefault={false} + name="foo" + onChange={jest.fn()} + value="bar" + {...props} + setting={mockSetting()} + /> ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx index 7f5dd70dc64..483dc0c30b7 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx @@ -20,13 +20,14 @@ import { shallow, ShallowWrapper } from 'enzyme'; import * as React from 'react'; import { click } from '../../../../../helpers/testUtils'; -import { SettingCategoryDefinition } from '../../../../../types/settings'; -import { DefaultInputProps } from '../../../utils'; +import { SettingCategoryDefinition, SettingType } from '../../../../../types/settings'; +import { DefaultSpecializedInputProps } from '../../../utils'; import MultiValueInput from '../MultiValueInput'; import PrimitiveInput from '../PrimitiveInput'; const settingValue = { - key: 'example' + key: 'example', + hasValue: true }; const settingDefinition: SettingCategoryDefinition = { @@ -36,7 +37,7 @@ const settingDefinition: SettingCategoryDefinition = { multiValues: true, options: [], subCategory: 'Branches', - type: 'STRING' + type: SettingType.STRING }; const assertValues = (inputs: ShallowWrapper<any>, values: string[]) => { @@ -87,9 +88,11 @@ it('should add new value', () => { expect(onChange).toBeCalledWith(['foo', 'bar']); }); -function shallowRender(props: Partial<DefaultInputProps> = {}) { +function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) { return shallow( <MultiValueInput + isDefault={true} + name="bar" onChange={jest.fn()} setting={{ ...settingValue, definition: settingDefinition }} value={['foo']} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/PrimitiveInput-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/PrimitiveInput-test.tsx new file mode 100644 index 00000000000..a09e8814af8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/PrimitiveInput-test.tsx @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { shallow } from 'enzyme'; +import * as React from 'react'; +import { mockDefinition, mockSetting } from '../../../../../helpers/mocks/settings'; +import { SettingType } from '../../../../../types/settings'; +import { DefaultSpecializedInputProps } from '../../../utils'; +import PrimitiveInput from '../PrimitiveInput'; + +it.each(Object.values(SettingType).map(Array.of))( + 'should render correctly for %s', + (type: SettingType) => { + const setting = mockSetting({ definition: mockDefinition({ type }) }); + expect(shallowRender({ setting })).toMatchSnapshot(); + } +); + +function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) { + return shallow( + <PrimitiveInput + isDefault={true} + name="name" + onChange={jest.fn()} + setting={mockSetting()} + value={['foo']} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.tsx index 2f6125c6732..f61a4ee4ce5 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockSetting } from '../../../../../helpers/mocks/settings'; import { change } from '../../../../../helpers/testUtils'; import SimpleInput from '../SimpleInput'; @@ -31,6 +32,7 @@ it('should render input', () => { name="foo" onChange={onChange} type="text" + setting={mockSetting()} value="bar" /> ).find('input'); @@ -51,6 +53,7 @@ it('should call onChange', () => { name="foo" onChange={onChange} type="text" + setting={mockSetting()} value="bar" /> ).find('input'); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/__snapshots__/PrimitiveInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/__snapshots__/PrimitiveInput-test.tsx.snap new file mode 100644 index 00000000000..f8968e76239 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/__snapshots__/PrimitiveInput-test.tsx.snap @@ -0,0 +1,320 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly for BOOLEAN 1`] = ` +<InputForBoolean + isDefault={true} + name="name" + onChange={[MockFunction]} + setting={ + Object { + "definition": Object { + "category": "foo category", + "fields": Array [], + "key": "foo", + "options": Array [], + "subCategory": "foo subCat", + "type": "BOOLEAN", + }, + "hasValue": true, + "inherited": true, + "key": "foo", + "value": "42", + } + } + value={ + Array [ + "foo", + ] + } +/> +`; + +exports[`should render correctly for FLOAT 1`] = ` +<InputForNumber + isDefault={true} + name="name" + onChange={[MockFunction]} + setting={ + Object { + "definition": Object { + "category": "foo category", + "fields": Array [], + "key": "foo", + "options": Array [], + "subCategory": "foo subCat", + "type": "FLOAT", + }, + "hasValue": true, + "inherited": true, + "key": "foo", + "value": "42", + } + } + value={ + Array [ + "foo", + ] + } +/> +`; + +exports[`should render correctly for INTEGER 1`] = ` +<InputForNumber + isDefault={true} + name="name" + onChange={[MockFunction]} + setting={ + Object { + "definition": Object { + "category": "foo category", + "fields": Array [], + "key": "foo", + "options": Array [], + "subCategory": "foo subCat", + "type": "INTEGER", + }, + "hasValue": true, + "inherited": true, + "key": "foo", + "value": "42", + } + } + value={ + Array [ + "foo", + ] + } +/> +`; + +exports[`should render correctly for JSON 1`] = ` +<InputForJSON + isDefault={true} + name="name" + onChange={[MockFunction]} + setting={ + Object { + "definition": Object { + "category": "foo category", + "fields": Array [], + "key": "foo", + "options": Array [], + "subCategory": "foo subCat", + "type": "JSON", + }, + "hasValue": true, + "inherited": true, + "key": "foo", + "value": "42", + } + } + value={ + Array [ + "foo", + ] + } +/> +`; + +exports[`should render correctly for LICENSE 1`] = ` +<InputForString + isDefault={true} + name="name" + onChange={[MockFunction]} + setting={ + Object { + "definition": Object { + "category": "foo category", + "fields": Array [], + "key": "foo", + "options": Array [], + "subCategory": "foo subCat", + "type": "LICENSE", + }, + "hasValue": true, + "inherited": true, + "key": "foo", + "value": "42", + } + } + value={ + Array [ + "foo", + ] + } +/> +`; + +exports[`should render correctly for LONG 1`] = ` +<InputForNumber + isDefault={true} + name="name" + onChange={[MockFunction]} + setting={ + Object { + "definition": Object { + "category": "foo category", + "fields": Array [], + "key": "foo", + "options": Array [], + "subCategory": "foo subCat", + "type": "LONG", + }, + "hasValue": true, + "inherited": true, + "key": "foo", + "value": "42", + } + } + value={ + Array [ + "foo", + ] + } +/> +`; + +exports[`should render correctly for PASSWORD 1`] = ` +<InputForPassword + isDefault={true} + name="name" + onChange={[MockFunction]} + setting={ + Object { + "definition": Object { + "category": "foo category", + "fields": Array [], + "key": "foo", + "options": Array [], + "subCategory": "foo subCat", + "type": "PASSWORD", + }, + "hasValue": true, + "inherited": true, + "key": "foo", + "value": "42", + } + } + value={ + Array [ + "foo", + ] + } +/> +`; + +exports[`should render correctly for PROPERTY_SET 1`] = ` +<InputForString + isDefault={true} + name="name" + onChange={[MockFunction]} + setting={ + Object { + "definition": Object { + "category": "foo category", + "fields": Array [], + "key": "foo", + "options": Array [], + "subCategory": "foo subCat", + "type": "PROPERTY_SET", + }, + "hasValue": true, + "inherited": true, + "key": "foo", + "value": "42", + } + } + value={ + Array [ + "foo", + ] + } +/> +`; + +exports[`should render correctly for SINGLE_SELECT_LIST 1`] = ` +<Wrapped + isDefault={true} + name="name" + onChange={[MockFunction]} + setting={ + Object { + "definition": Object { + "category": "foo category", + "fields": Array [], + "key": "foo", + "options": Array [], + "subCategory": "foo subCat", + "type": "SINGLE_SELECT_LIST", + }, + "hasValue": true, + "inherited": true, + "key": "foo", + "value": "42", + } + } + value={ + Array [ + "foo", + ] + } +/> +`; + +exports[`should render correctly for STRING 1`] = ` +<InputForString + isDefault={true} + name="name" + onChange={[MockFunction]} + setting={ + Object { + "definition": Object { + "category": "foo category", + "fields": Array [], + "key": "foo", + "options": Array [], + "subCategory": "foo subCat", + "type": "STRING", + }, + "hasValue": true, + "inherited": true, + "key": "foo", + "value": "42", + } + } + value={ + Array [ + "foo", + ] + } +/> +`; + +exports[`should render correctly for TEXT 1`] = ` +<InputForText + isDefault={true} + name="name" + onChange={[MockFunction]} + setting={ + Object { + "definition": Object { + "category": "foo category", + "fields": Array [], + "key": "foo", + "options": Array [], + "subCategory": "foo subCat", + "type": "TEXT", + }, + "hasValue": true, + "inherited": true, + "key": "foo", + "value": "42", + } + } + value={ + Array [ + "foo", + ] + } +/> +`; diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx index db66a5ebe05..ddd645f28a0 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router'; +import Toggle from '../../../../components/controls/Toggle'; import { Alert } from '../../../../components/ui/Alert'; import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker'; import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; @@ -30,7 +31,6 @@ import { AlmSettingsInstance, ProjectAlmBindingResponse } from '../../../../types/alm-settings'; -import InputForBoolean from '../inputs/InputForBoolean'; export interface AlmSpecificFormProps { alm: AlmKeys; @@ -108,12 +108,10 @@ function renderBooleanField( return renderFieldWrapper( renderLabel({ ...props, optional: true }), <div className="display-flex-center big-spacer-top"> - <InputForBoolean - isDefault={true} - name={id} - onChange={v => onFieldChange(propKey, v)} - value={value} - /> + <div className="display-inline-block text-top"> + <Toggle name={id} onChange={v => onFieldChange(propKey, v)} value={value} /> + {value == null && <span className="spacer-left note">{translate('settings.not_set')}</span>} + </div> {inputExtra} </div>, renderHelp(props) diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap index 1cb1269f6dd..77a82861402 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap @@ -444,12 +444,15 @@ exports[`it should render correctly for github 1`] = ` <div className="display-flex-center big-spacer-top" > - <InputForBoolean - isDefault={true} - name="github.summary_comment_setting" - onChange={[Function]} - value={true} - /> + <div + className="display-inline-block text-top" + > + <Toggle + name="github.summary_comment_setting" + onChange={[Function]} + value={true} + /> + </div> </div> </div> </div> @@ -535,12 +538,15 @@ exports[`it should render correctly for github if an instance URL is provided 1` <div className="display-flex-center big-spacer-top" > - <InputForBoolean - isDefault={true} - name="github.summary_comment_setting" - onChange={[Function]} - value={true} - /> + <div + className="display-inline-block text-top" + > + <Toggle + name="github.summary_comment_setting" + onChange={[Function]} + value={true} + /> + </div> </div> </div> </div> @@ -626,12 +632,15 @@ exports[`it should render correctly for github if an instance URL is provided 2` <div className="display-flex-center big-spacer-top" > - <InputForBoolean - isDefault={true} - name="github.summary_comment_setting" - onChange={[Function]} - value={true} - /> + <div + className="display-inline-block text-top" + > + <Toggle + name="github.summary_comment_setting" + onChange={[Function]} + value={true} + /> + </div> </div> </div> </div> @@ -828,12 +837,15 @@ exports[`should render the monorepo field when the feature is supported 1`] = ` <div className="display-flex-center big-spacer-top" > - <InputForBoolean - isDefault={true} - name="monorepo" - onChange={[Function]} - value={false} - /> + <div + className="display-inline-block text-top" + > + <Toggle + name="monorepo" + onChange={[Function]} + value={false} + /> + </div> </div> </div> </div> diff --git a/server/sonar-web/src/main/js/apps/settings/routes.ts b/server/sonar-web/src/main/js/apps/settings/routes.ts index 53d18a0ed1a..326d588b054 100644 --- a/server/sonar-web/src/main/js/apps/settings/routes.ts +++ b/server/sonar-web/src/main/js/apps/settings/routes.ts @@ -21,7 +21,7 @@ import { lazyLoadComponent } from '../../components/lazyLoadComponent'; const routes = [ { - indexRoute: { component: lazyLoadComponent(() => import('./components/AppContainer')) } + indexRoute: { component: lazyLoadComponent(() => import('./components/SettingsApp')) } }, { path: 'encryption', diff --git a/server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts b/server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts index 9944b748fc9..8862df66f2d 100644 --- a/server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts +++ b/server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts @@ -21,21 +21,25 @@ import { getSettingsAppChangedValue, getSettingsAppDefinition } from '../../../../store/rootReducer'; -import { checkValue, fetchSettings } from '../actions'; +import { checkValue, fetchSettings, fetchValues } from '../actions'; import { receiveDefinitions } from '../definitions'; -jest.mock('../../../../api/settings', () => ({ - getDefinitions: jest.fn().mockResolvedValue([ - { - key: 'SETTINGS_1_KEY', - type: 'SETTINGS_1_TYPE' - }, - { - key: 'SETTINGS_2_KEY', - type: 'LICENSE' - } - ]) -})); +jest.mock('../../../../api/settings', () => { + const { mockSettingValue } = jest.requireActual('../../../../helpers/mocks/settings'); + return { + getValues: jest.fn().mockResolvedValue([mockSettingValue()]), + getDefinitions: jest.fn().mockResolvedValue([ + { + key: 'SETTINGS_1_KEY', + type: 'SETTINGS_1_TYPE' + }, + { + key: 'SETTINGS_2_KEY', + type: 'LICENSE' + } + ]) + }; +}); jest.mock('../definitions', () => ({ receiveDefinitions: jest.fn() @@ -59,6 +63,18 @@ it('#fetchSettings should filter LICENSE type settings', async () => { ]); }); +it('should fetchValue correclty', async () => { + const dispatch = jest.fn(); + await fetchValues(['test'], 'foo')(dispatch); + expect(dispatch).toHaveBeenCalledWith({ + component: 'foo', + settings: [{ key: 'test' }], + type: 'RECEIVE_VALUES', + updateKeys: ['test'] + }); + expect(dispatch).toHaveBeenCalledWith({ type: 'CLOSE_ALL_GLOBAL_MESSAGES' }); +}); + describe('checkValue', () => { const dispatch = jest.fn(); diff --git a/server/sonar-web/src/main/js/apps/settings/store/__tests__/rootReducer-test.ts b/server/sonar-web/src/main/js/apps/settings/store/__tests__/rootReducer-test.ts new file mode 100644 index 00000000000..6dbfdc3f7e3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/__tests__/rootReducer-test.ts @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { getSettingsForCategory } from '../rootReducer'; + +it('Should correclty assert if value is set', () => { + const settings = getSettingsForCategory( + { + definitions: { + foo: { category: 'cat', key: 'foo', fields: [], options: [], subCategory: 'test' }, + bar: { category: 'cat', key: 'bar', fields: [], options: [], subCategory: 'test' } + }, + globalMessages: [], + settingsPage: { + changedValues: {}, + loading: {}, + validationMessages: {} + }, + values: { components: {}, global: { foo: { key: 'foo' } } } + }, + 'cat' + ); + expect(settings[0].hasValue).toBe(true); + expect(settings[1].hasValue).toBe(false); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/store/actions.ts b/server/sonar-web/src/main/js/apps/settings/store/actions.ts index ae7dc188a00..44b67e835e1 100644 --- a/server/sonar-web/src/main/js/apps/settings/store/actions.ts +++ b/server/sonar-web/src/main/js/apps/settings/store/actions.ts @@ -65,10 +65,10 @@ export function fetchSettings(component?: string) { }; } -export function fetchValues(keys: string, component?: string) { +export function fetchValues(keys: string[], component?: string) { return (dispatch: Dispatch) => - getValues({ keys, component }).then(settings => { - dispatch(receiveValues(settings, component)); + getValues({ keys: keys.join(), component }).then(settings => { + dispatch(receiveValues(keys, settings, component)); dispatch(closeAllGlobalMessages()); }); } @@ -131,7 +131,7 @@ export function saveValue(key: string, component?: string) { return setSettingValue(definition, value, component) .then(() => getValues({ keys: key, component })) .then(values => { - dispatch(receiveValues(values, component)); + dispatch(receiveValues([key], values, component)); dispatch(cancelChange(key)); dispatch(passValidation(key)); dispatch(stopLoading(key)); @@ -148,9 +148,9 @@ export function resetValue(key: string, component?: string) { .then(() => getValues({ keys: key, component })) .then(values => { if (values.length > 0) { - dispatch(receiveValues(values, component)); + dispatch(receiveValues([key], values, component)); } else { - dispatch(receiveValues([{ key }], component)); + dispatch(receiveValues([key], [], component)); } dispatch(passValidation(key)); dispatch(stopLoading(key)); diff --git a/server/sonar-web/src/main/js/apps/settings/store/rootReducer.ts b/server/sonar-web/src/main/js/apps/settings/store/rootReducer.ts index 0f8f236c802..3b00673beb6 100644 --- a/server/sonar-web/src/main/js/apps/settings/store/rootReducer.ts +++ b/server/sonar-web/src/main/js/apps/settings/store/rootReducer.ts @@ -53,11 +53,16 @@ export function getValue(state: State, key: string, component?: string) { } export function getSettingsForCategory(state: State, category: string, component?: string) { - return fromDefinitions.getDefinitionsForCategory(state.definitions, category).map(definition => ({ - key: definition.key, - ...getValue(state, definition.key, component), - definition - })); + return fromDefinitions.getDefinitionsForCategory(state.definitions, category).map(definition => { + const value = getValue(state, definition.key, component); + const hasValue = value !== undefined && value.inherited !== true; + return { + key: definition.key, + hasValue, + ...value, + definition + }; + }); } export function getChangedValue(state: State, key: string) { diff --git a/server/sonar-web/src/main/js/apps/settings/store/values.ts b/server/sonar-web/src/main/js/apps/settings/store/values.ts index 8b00453b9b6..1146d12b359 100644 --- a/server/sonar-web/src/main/js/apps/settings/store/values.ts +++ b/server/sonar-web/src/main/js/apps/settings/store/values.ts @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { keyBy } from 'lodash'; +import { keyBy, omit } from 'lodash'; import { combineReducers } from 'redux'; import { Action as AppStateAction, Actions as AppStateActions } from '../../../store/appState'; import { ActionType } from '../../../store/utils/actions'; @@ -37,10 +37,11 @@ export interface State { } export function receiveValues( + updateKeys: string[], settings: Array<{ key: string; value?: string }>, component?: string ) { - return { type: Actions.receiveValues, settings, component }; + return { type: Actions.receiveValues, updateKeys, settings, component }; } function components(state: State['components'] = {}, action: Action) { @@ -50,7 +51,7 @@ function components(state: State['components'] = {}, action: Action) { } if (action.type === Actions.receiveValues) { const settingsByKey = keyBy(action.settings, 'key'); - return { ...state, [key]: { ...(state[key] || {}), ...settingsByKey } }; + return { ...state, [key]: { ...omit(state[key] || {}, action.updateKeys), ...settingsByKey } }; } return state; } @@ -61,8 +62,9 @@ function global(state: State['components'] = {}, action: Action | AppStateAction return state; } const settingsByKey = keyBy(action.settings, 'key'); - return { ...state, ...settingsByKey }; + return { ...omit(state, action.updateKeys), ...settingsByKey }; } + if (action.type === AppStateActions.SetAppState) { const settingsByKey: SettingsState = {}; Object.keys(action.appState.settings).forEach( diff --git a/server/sonar-web/src/main/js/apps/settings/utils.ts b/server/sonar-web/src/main/js/apps/settings/utils.ts index 9fd4d106634..5589c5e532d 100644 --- a/server/sonar-web/src/main/js/apps/settings/utils.ts +++ b/server/sonar-web/src/main/js/apps/settings/utils.ts @@ -25,12 +25,16 @@ import { Setting, SettingCategoryDefinition, SettingDefinition } from '../../typ export const DEFAULT_CATEGORY = 'general'; -export type DefaultSpecializedInputProps = T.Omit<DefaultInputProps, 'setting'> & { +export type DefaultSpecializedInputProps = DefaultInputProps & { + className?: string; + autoComplete?: string; isDefault: boolean; name: string; + type?: string; }; export interface DefaultInputProps { + autoFocus?: boolean; hasValueChanged?: boolean; onCancel?: () => void; onChange: (value: any) => void; @@ -89,6 +93,10 @@ export function isEmptyValue(definition: SettingDefinition, value: any) { } } +export function isSecuredDefinition(item: SettingDefinition): boolean { + return item.key.endsWith('.secured'); +} + export function isCategoryDefinition(item: SettingDefinition): item is SettingCategoryDefinition { return Boolean((item as any).fields); } diff --git a/server/sonar-web/src/main/js/helpers/mocks/settings.ts b/server/sonar-web/src/main/js/helpers/mocks/settings.ts index 14065d55238..898122d1d4d 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/settings.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/settings.ts @@ -17,7 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { Setting, SettingCategoryDefinition, SettingWithCategory } from '../../types/settings'; +import { + Setting, + SettingCategoryDefinition, + SettingType, + SettingValue, + SettingWithCategory +} from '../../types/settings'; export function mockDefinition( overrides: Partial<SettingCategoryDefinition> = {} @@ -36,30 +42,39 @@ export function mockSetting(overrides: Partial<Setting> = {}): Setting { return { key: 'foo', value: '42', + hasValue: true, inherited: true, definition: { key: 'foo', name: 'Foo setting', description: 'When Foo then Bar', - type: 'INTEGER', + type: SettingType.INTEGER, options: [] }, ...overrides }; } +export function mockSettingValue(overrides: Partial<SettingValue> = {}) { + return { + key: 'test', + ...overrides + }; +} + export function mockSettingWithCategory( overrides: Partial<SettingWithCategory> = {} ): SettingWithCategory { return { key: 'foo', value: '42', + hasValue: true, inherited: true, definition: { key: 'foo', name: 'Foo setting', description: 'When Foo then Bar', - type: 'INTEGER', + type: SettingType.INTEGER, options: [], category: 'general', fields: [], diff --git a/server/sonar-web/src/main/js/types/settings.ts b/server/sonar-web/src/main/js/types/settings.ts index b272b5b9943..db89872a345 100644 --- a/server/sonar-web/src/main/js/types/settings.ts +++ b/server/sonar-web/src/main/js/types/settings.ts @@ -25,22 +25,22 @@ export const enum SettingsKey { ProjectReportFrequency = 'sonar.governance.report.project.branch.frequency' } -export type Setting = SettingValue & { definition: SettingDefinition }; +export type Setting = SettingValue & { definition: SettingDefinition; hasValue: boolean }; export type SettingWithCategory = Setting & { definition: SettingCategoryDefinition }; -export type SettingType = - | 'STRING' - | 'TEXT' - | 'JSON' - | 'PASSWORD' - | 'BOOLEAN' - | 'FLOAT' - | 'INTEGER' - | 'LICENSE' - | 'LONG' - | 'SINGLE_SELECT_LIST' - | 'PROPERTY_SET'; - +export enum SettingType { + STRING = 'STRING', + TEXT = 'TEXT', + JSON = 'JSON', + PASSWORD = 'PASSWORD', + BOOLEAN = 'BOOLEAN', + FLOAT = 'FLOAT', + INTEGER = 'INTEGER', + LICENSE = 'LICENSE', + LONG = 'LONG', + SINGLE_SELECT_LIST = 'SINGLE_SELECT_LIST', + PROPERTY_SET = 'PROPERTY_SET' +} export interface SettingDefinition { description?: string; key: string; |