Sfoglia il codice sorgente

SONAR-15376 Make secured settings hidden when set

tags/9.1.0.47736
Mathieu Suen 2 anni fa
parent
commit
805294024f
46 ha cambiato i file con 1025 aggiunte e 418 eliminazioni
  1. 1
    1
      server/sonar-web/src/main/js/apps/about/actions.ts
  2. 1
    1
      server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx
  3. 7
    5
      server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts
  4. 20
    20
      server/sonar-web/src/main/js/apps/settings/components/Definition.tsx
  5. 3
    6
      server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx
  6. 2
    2
      server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx
  7. 1
    1
      server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
  8. 6
    4
      server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx
  9. 3
    2
      server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionActions-test.tsx
  10. 3
    3
      server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-test.tsx
  11. 44
    40
      server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap
  12. 0
    0
      server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsApp-test.tsx.snap
  13. 3
    0
      server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SubCategoryDefinitionsList-test.tsx.snap
  14. 23
    6
      server/sonar-web/src/main/js/apps/settings/components/inputs/Input.tsx
  15. 1
    1
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.tsx
  16. 5
    70
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.tsx
  17. 103
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSecured.tsx
  18. 1
    1
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.tsx
  19. 6
    8
      server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx
  20. 23
    40
      server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.tsx
  21. 27
    14
      server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.tsx
  22. 7
    6
      server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.tsx
  23. 28
    32
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.tsx
  24. 9
    1
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.tsx
  25. 9
    1
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForJSON-test.tsx
  26. 8
    1
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.tsx
  27. 13
    67
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.tsx
  28. 96
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSecured-test.tsx
  29. 2
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.tsx
  30. 8
    1
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.tsx
  31. 9
    1
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.tsx
  32. 8
    5
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx
  33. 46
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/PrimitiveInput-test.tsx
  34. 3
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.tsx
  35. 320
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/__snapshots__/PrimitiveInput-test.tsx.snap
  36. 5
    7
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx
  37. 36
    24
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap
  38. 1
    1
      server/sonar-web/src/main/js/apps/settings/routes.ts
  39. 29
    13
      server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts
  40. 42
    0
      server/sonar-web/src/main/js/apps/settings/store/__tests__/rootReducer-test.ts
  41. 6
    6
      server/sonar-web/src/main/js/apps/settings/store/actions.ts
  42. 10
    5
      server/sonar-web/src/main/js/apps/settings/store/rootReducer.ts
  43. 6
    4
      server/sonar-web/src/main/js/apps/settings/store/values.ts
  44. 9
    1
      server/sonar-web/src/main/js/apps/settings/utils.ts
  45. 18
    3
      server/sonar-web/src/main/js/helpers/mocks/settings.ts
  46. 14
    14
      server/sonar-web/src/main/js/types/settings.ts

+ 1
- 1
server/sonar-web/src/main/js/apps/about/actions.ts Vedi File

@@ -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));
});
};
}

+ 1
- 1
server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx Vedi File

@@ -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']);
}
}


+ 7
- 5
server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts Vedi File

@@ -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'
};

+ 20
- 20
server/sonar-web/src/main/js/apps/settings/components/Definition.tsx Vedi File

@@ -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>
);

+ 3
- 6
server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx Vedi File

@@ -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 (
<>

server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx → server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx Vedi File

@@ -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);

+ 1
- 1
server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx Vedi File

@@ -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);
}


+ 6
- 4
server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx Vedi File

@@ -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);

+ 3
- 2
server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionActions-test.tsx Vedi File

@@ -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'
};

server/sonar-web/src/main/js/apps/settings/components/__tests__/AppContainer-test.tsx → server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-test.tsx Vedi File

@@ -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()}

+ 44
- 40
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap Vedi File

@@ -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>
`;

server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AppContainer-test.tsx.snap → server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsApp-test.tsx.snap Vedi File


+ 3
- 0
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SubCategoryDefinitionsList-test.tsx.snap Vedi File

@@ -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",

+ 23
- 6
server/sonar-web/src/main/js/apps/settings/components/inputs/Input.tsx Vedi File

@@ -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)} />;
}

+ 1
- 1
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.tsx Vedi File

@@ -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} />;
}

+ 5
- 70
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.tsx Vedi File

@@ -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" />
);
}

+ 103
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSecured.tsx Vedi File

@@ -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>
</>
);
}
}

+ 1
- 1
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.tsx Vedi File

@@ -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} />;
}

+ 6
- 8
server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx Vedi File

@@ -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}
/>


+ 23
- 40
server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.tsx Vedi File

@@ -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} />;
}

+ 27
- 14
server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.tsx Vedi File

@@ -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

+ 7
- 6
server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.tsx Vedi File

@@ -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}
/>
);
}

+ 28
- 32
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.tsx Vedi File

@@ -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} />);
}

+ 9
- 1
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.tsx Vedi File

@@ -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}
/>
);
}

+ 9
- 1
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForJSON-test.tsx Vedi File

@@ -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}
/>
);
}

+ 8
- 1
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.tsx Vedi File

@@ -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');

+ 13
- 67
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.tsx Vedi File

@@ -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();
});

+ 96
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSecured-test.tsx Vedi File

@@ -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}
/>
);
}

+ 2
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.tsx Vedi File

@@ -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}
/>

+ 8
- 1
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.tsx Vedi File

@@ -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');

+ 9
- 1
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.tsx Vedi File

@@ -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()}
/>
);
}

+ 8
- 5
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx Vedi File

@@ -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']}

+ 46
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/PrimitiveInput-test.tsx Vedi File

@@ -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}
/>
);
}

+ 3
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.tsx Vedi File

@@ -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');

+ 320
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/__snapshots__/PrimitiveInput-test.tsx.snap Vedi File

@@ -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",
]
}
/>
`;

+ 5
- 7
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx Vedi File

@@ -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)

+ 36
- 24
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap Vedi File

@@ -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>

+ 1
- 1
server/sonar-web/src/main/js/apps/settings/routes.ts Vedi File

@@ -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',

+ 29
- 13
server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts Vedi File

@@ -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();


+ 42
- 0
server/sonar-web/src/main/js/apps/settings/store/__tests__/rootReducer-test.ts Vedi File

@@ -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);
});

+ 6
- 6
server/sonar-web/src/main/js/apps/settings/store/actions.ts Vedi File

@@ -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));

+ 10
- 5
server/sonar-web/src/main/js/apps/settings/store/rootReducer.ts Vedi File

@@ -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) {

+ 6
- 4
server/sonar-web/src/main/js/apps/settings/store/values.ts Vedi File

@@ -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(

+ 9
- 1
server/sonar-web/src/main/js/apps/settings/utils.ts Vedi File

@@ -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);
}

+ 18
- 3
server/sonar-web/src/main/js/helpers/mocks/settings.ts Vedi File

@@ -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: [],

+ 14
- 14
server/sonar-web/src/main/js/types/settings.ts Vedi File

@@ -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;

Loading…
Annulla
Salva