From 805294024fab786fca6076daeadd18afaadef26b Mon Sep 17 00:00:00 2001 From: Mathieu Suen Date: Wed, 8 Sep 2021 16:51:02 +0200 Subject: [PATCH] SONAR-15376 Make secured settings hidden when set --- .../src/main/js/apps/about/actions.ts | 2 +- .../apps/audit-logs/components/AuditApp.tsx | 2 +- .../js/apps/settings/__tests__/utils-test.ts | 12 +- .../apps/settings/components/Definition.tsx | 40 +-- .../settings/components/DefinitionActions.tsx | 9 +- .../{AppContainer.tsx => SettingsApp.tsx} | 4 +- .../components/SubCategoryDefinitionsList.tsx | 2 +- .../components/__tests__/Definition-test.tsx | 10 +- .../__tests__/DefinitionActions-test.tsx | 5 +- ...ontainer-test.tsx => SettingsApp-test.tsx} | 6 +- .../__snapshots__/Definition-test.tsx.snap | 84 ++--- ...est.tsx.snap => SettingsApp-test.tsx.snap} | 0 .../SubCategoryDefinitionsList-test.tsx.snap | 3 + .../apps/settings/components/inputs/Input.tsx | 29 +- .../components/inputs/InputForNumber.tsx | 2 +- .../components/inputs/InputForPassword.tsx | 75 +--- .../components/inputs/InputForSecured.tsx | 103 ++++++ .../components/inputs/InputForString.tsx | 2 +- .../components/inputs/MultiValueInput.tsx | 14 +- .../components/inputs/PrimitiveInput.tsx | 63 ++-- .../components/inputs/PropertySetInput.tsx | 41 ++- .../components/inputs/SimpleInput.tsx | 13 +- .../inputs/__tests__/Input-test.tsx | 60 ++-- .../inputs/__tests__/InputForBoolean-test.tsx | 10 +- .../inputs/__tests__/InputForJSON-test.tsx | 10 +- .../inputs/__tests__/InputForNumber-test.tsx | 9 +- .../__tests__/InputForPassword-test.tsx | 80 +---- .../inputs/__tests__/InputForSecured-test.tsx | 96 ++++++ .../InputForSingleSelectList-test.tsx | 2 + .../inputs/__tests__/InputForString-test.tsx | 9 +- .../inputs/__tests__/InputForText-test.tsx | 10 +- .../inputs/__tests__/MultiValueInput-test.tsx | 13 +- .../inputs/__tests__/PrimitiveInput-test.tsx | 46 +++ .../inputs/__tests__/SimpleInput-test.tsx | 3 + .../PrimitiveInput-test.tsx.snap | 320 ++++++++++++++++++ .../AlmSpecificForm.tsx | 12 +- .../AlmSpecificForm-test.tsx.snap | 60 ++-- .../src/main/js/apps/settings/routes.ts | 2 +- .../settings/store/__tests__/actions-test.ts | 42 ++- .../store/__tests__/rootReducer-test.ts | 42 +++ .../main/js/apps/settings/store/actions.ts | 12 +- .../js/apps/settings/store/rootReducer.ts | 15 +- .../src/main/js/apps/settings/store/values.ts | 10 +- .../src/main/js/apps/settings/utils.ts | 10 +- .../src/main/js/helpers/mocks/settings.ts | 21 +- .../sonar-web/src/main/js/types/settings.ts | 28 +- 46 files changed, 1025 insertions(+), 418 deletions(-) rename server/sonar-web/src/main/js/apps/settings/components/{AppContainer.tsx => SettingsApp.tsx} (96%) rename server/sonar-web/src/main/js/apps/settings/components/__tests__/{AppContainer-test.tsx => SettingsApp-test.tsx} (96%) rename server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/{AppContainer-test.tsx.snap => SettingsApp-test.tsx.snap} (100%) create mode 100644 server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSecured.tsx create mode 100644 server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSecured-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/PrimitiveInput-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/__snapshots__/PrimitiveInput-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/settings/store/__tests__/rootReducer-test.ts 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 { 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 { )} - - - - +
+ + + ); 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 } 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 similarity index 96% rename from server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx rename to 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 { +export class SettingsApp extends React.PureComponent { 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('onChange')(5); + wrapper.find(Input).prop('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('onCancel')(); + wrapper.find(Input).prop('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('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('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 similarity index 96% rename from server/sonar-web/src/main/js/apps/settings/components/__tests__/AppContainer-test.tsx rename to 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 = {}) { +function shallowRender(props: Partial = {}) { return shallow( - - + - + + /> + `; 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 similarity index 100% rename from server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AppContainer-test.tsx.snap rename to 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 = PrimitiveInput; if (isCategoryDefinition(definition) && definition.multiValues) { - return ; + Input = MultiValueInput; + } + + if (definition.type === SettingType.PROPERTY_SET) { + Input = PropertySetInput; } - if (definition.type === 'PROPERTY_SET') { - return ; + if (isSecuredDefinition(definition)) { + return ; } - return ; + return ; } 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 ; + return ; } 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) => { - this.props.onChange(event.target.value); - }; - - handleChangeClick = () => { - this.setState({ changing: true }); - }; - - renderInput() { - return ( -
- - -
- ); - } - - render() { - if (this.state.changing) { - return this.renderInput(); - } - - return ( - <> - - - - ); - } +export default function InputForPassword(props: DefaultSpecializedInputProps) { + return ( + + ); } 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; +} + +export default class InputForSecured extends React.PureComponent { + 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 + <> + + + + ); + } + + render() { + if (this.state.changing) { + return this.renderInput(); + } + + return ( + <> + + + + ); + } +} 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 ; + return ; } 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 { +export default class MultiValueInput extends React.PureComponent { ensureValue = () => { return this.props.value || []; }; @@ -40,17 +40,15 @@ export default class MultiValueInput extends React.PureComponent 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; -} = { - 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 { + return function Wrapped(props: DefaultSpecializedInputProps) { + return ; + }; } -export default class PrimitiveInput extends React.PureComponent { - 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; + } = { + 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 ( - - ); - } + const InputComponent = (definition.type && typeMapping[definition.type]) || InputForString; - const InputComponent = (definition.type && typeMapping[definition.type]) || InputForString; - return ; - } + return ; } 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 { +export default class PropertySetInput extends React.PureComponent { ensureValue() { return this.props.value || []; } @@ -42,23 +47,31 @@ export default class PropertySetInput extends React.PureComponent {isCategoryDefinition(definition) && - definition.fields.map(field => ( - - this.handleInputChange(index, field.key, value)} - setting={{ ...setting, definition: field, value: fieldValues[field.key] }} - value={fieldValues[field.key]} - /> - - ))} + definition.fields.map(field => { + const newSetting = { + ...setting, + definition: field, + value: fieldValues[field.key] + }; + return ( + + this.handleInputChange(index, field.key, value)} + setting={newSetting} + value={fieldValues[field.key]} + /> + + ); + })} {!isLast && ( { }; render() { + const { autoComplete, autoFocus, className, name, value = '', type } = this.props; return ( ); } 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 = {}) { - return shallow( - - ); + return shallow(); } 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 = {}) { return shallow( - + ); } 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 = {}) { return shallow( - + ); } 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( - + ).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 = {}) { - return shallow( + const simpleInput = shallow( - ); -} + ).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 = {}) { + return shallow( + + ); +} 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 = {}) { 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( - + ).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 = {}) { return shallow( - + ); } 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, values: string[]) => { @@ -87,9 +88,11 @@ it('should add new value', () => { expect(onChange).toBeCalledWith(['foo', 'bar']); }); -function shallowRender(props: Partial = {}) { +function shallowRender(props: Partial = {}) { return shallow( { + const setting = mockSetting({ definition: mockDefinition({ type }) }); + expect(shallowRender({ setting })).toMatchSnapshot(); + } +); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} 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`] = ` + +`; + +exports[`should render correctly for FLOAT 1`] = ` + +`; + +exports[`should render correctly for INTEGER 1`] = ` + +`; + +exports[`should render correctly for JSON 1`] = ` + +`; + +exports[`should render correctly for LICENSE 1`] = ` + +`; + +exports[`should render correctly for LONG 1`] = ` + +`; + +exports[`should render correctly for PASSWORD 1`] = ` + +`; + +exports[`should render correctly for PROPERTY_SET 1`] = ` + +`; + +exports[`should render correctly for SINGLE_SELECT_LIST 1`] = ` + +`; + +exports[`should render correctly for STRING 1`] = ` + +`; + +exports[`should render correctly for TEXT 1`] = ` + +`; 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 }),
- onFieldChange(propKey, v)} - value={value} - /> +
+ onFieldChange(propKey, v)} value={value} /> + {value == null && {translate('settings.not_set')}} +
{inputExtra}
, 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`] = `
- +
+ +
@@ -535,12 +538,15 @@ exports[`it should render correctly for github if an instance URL is provided 1`
- +
+ +
@@ -626,12 +632,15 @@ exports[`it should render correctly for github if an instance URL is provided 2`
- +
+ +
@@ -828,12 +837,15 @@ exports[`should render the monorepo field when the feature is supported 1`] = `
- +
+ +
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 & { +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 = {} @@ -36,30 +42,39 @@ export function mockSetting(overrides: Partial = {}): 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 = {}) { + return { + key: 'test', + ...overrides + }; +} + export function mockSettingWithCategory( overrides: Partial = {} ): 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; -- 2.39.5