import throwGlobalError from '../app/utils/throwGlobalError';
import { isCategoryDefinition } from '../apps/settings/utils';
import { BranchParameters } from '../types/branch-like';
+import { SettingCategoryDefinition, SettingDefinition, SettingValue } from '../types/settings';
-export function getDefinitions(component?: string): Promise<T.SettingCategoryDefinition[]> {
+export function getDefinitions(component?: string): Promise<SettingCategoryDefinition[]> {
return getJSON('/api/settings/list_definitions', { component }).then(
r => r.definitions,
throwGlobalError
export function getValues(
data: { keys: string; component?: string } & BranchParameters
-): Promise<T.SettingValue[]> {
+): Promise<SettingValue[]> {
return getJSON('/api/settings/values', data).then(r => r.settings);
}
export function setSettingValue(
- definition: T.SettingDefinition,
+ definition: SettingDefinition,
value: any,
component?: string
): Promise<void> {
* 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,
+ SettingFieldDefinition
+} from '../../../types/settings';
import { getDefaultValue, getEmptyValue, sanitizeTranslation } from '../utils';
const fields = [
- { key: 'foo', type: 'STRING' } as T.SettingFieldDefinition,
- { key: 'bar', type: 'SINGLE_SELECT_LIST' } as T.SettingFieldDefinition
+ { key: 'foo', type: 'STRING' } as SettingFieldDefinition,
+ { key: 'bar', type: 'SINGLE_SELECT_LIST' } as SettingFieldDefinition
];
-const settingDefinition: T.SettingCategoryDefinition = {
+const settingDefinition: SettingCategoryDefinition = {
category: 'test',
fields: [],
key: 'test',
describe('#getEmptyValue()', () => {
it('should work for property sets', () => {
- const setting: T.SettingCategoryDefinition = {
+ const setting: SettingCategoryDefinition = {
...settingDefinition,
type: 'PROPERTY_SET',
fields
});
it('should work for multi values string', () => {
- const setting: T.SettingCategoryDefinition = {
+ const setting: SettingCategoryDefinition = {
...settingDefinition,
type: 'STRING',
multiValues: true
});
it('should work for multi values boolean', () => {
- const setting: T.SettingCategoryDefinition = {
+ const setting: SettingCategoryDefinition = {
...settingDefinition,
type: 'BOOLEAN',
multiValues: true
});
describe('#getDefaultValue()', () => {
- const check = (parentValue?: string, expected?: string) => {
- const setting: T.Setting = {
- definition: { key: 'test', options: [], type: 'BOOLEAN' },
- parentValue,
- key: 'test'
- };
- expect(getDefaultValue(setting)).toEqual(expected);
- };
-
- it('should work for boolean field when passing "true"', () =>
- check('true', 'settings.boolean.true'));
- it('should work for boolean field when passing "false"', () =>
- check('false', 'settings.boolean.false'));
+ it.each([
+ ['true', 'settings.boolean.true'],
+ ['false', 'settings.boolean.false']
+ ])(
+ 'should work for boolean field when passing "%s"',
+ (parentValue?: string, expected?: string) => {
+ const setting: Setting = {
+ definition: { key: 'test', options: [], type: 'BOOLEAN' },
+ parentValue,
+ key: 'test'
+ };
+ expect(getDefaultValue(setting)).toEqual(expected);
+ }
+ );
});
describe('sanitizeTranslation', () => {
isSettingsAppLoading,
Store
} from '../../../store/rootReducer';
+import { Setting } from '../../../types/settings';
import { checkValue, resetValue, saveValue } from '../store/actions';
import { cancelChange, changeValue, passValidation } from '../store/settingsPage';
import {
passValidation: (key: string) => void;
resetValue: (key: string, component?: string) => Promise<void>;
saveValue: (key: string, component?: string) => Promise<void>;
- setting: T.Setting;
+ setting: Setting;
validationMessage?: string;
}
success: boolean;
}
+const SAFE_SET_STATE_DELAY = 3000;
+
export class Definition extends React.PureComponent<Props, State> {
timeout?: number;
mounted = false;
return this.props.resetValue(definition.key, componentKey).then(() => {
this.props.cancelChange(definition.key);
this.safeSetState({ success: true });
- this.timeout = window.setTimeout(() => this.safeSetState({ success: false }), 3000);
+ this.timeout = window.setTimeout(
+ () => this.safeSetState({ success: false }),
+ SAFE_SET_STATE_DELAY
+ );
});
};
this.props.saveValue(setting.definition.key, component && component.key).then(
() => {
this.safeSetState({ success: true });
- this.timeout = window.setTimeout(() => this.safeSetState({ success: false }), 3000);
+ this.timeout = window.setTimeout(
+ () => this.safeSetState({ success: false }),
+ SAFE_SET_STATE_DELAY
+ );
},
- () => {}
+ () => {
+ /* Do nothing */
+ }
);
}
};
import { Button, ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
import Modal from 'sonar-ui-common/components/controls/Modal';
import { translate } from 'sonar-ui-common/helpers/l10n';
+import { Setting } from '../../../types/settings';
import { getDefaultValue, getSettingValue, isEmptyValue } from '../utils';
type Props = {
onCancel: () => void;
onReset: () => void;
onSave: () => void;
- setting: T.Setting;
+ setting: Setting;
};
type State = { reseting: boolean };
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { Setting } from '../../../types/settings';
import Definition from './Definition';
interface Props {
component?: T.Component;
- settings: T.Setting[];
+ settings: Setting[];
}
export default function DefinitionsList({ component, settings }: Props) {
*/
import { groupBy, isEqual, sortBy } from 'lodash';
import * as React from 'react';
+import { Setting, SettingCategoryDefinition } from '../../../types/settings';
import { getSubCategoryDescription, getSubCategoryName, sanitizeTranslation } from '../utils';
import DefinitionsList from './DefinitionsList';
import EmailForm from './EmailForm';
category: string;
component?: T.Component;
fetchValues: Function;
- settings: Array<T.Setting & { definition: T.SettingCategoryDefinition }>;
+ settings: Array<Setting & { definition: SettingCategoryDefinition }>;
subCategory?: string;
}
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { Setting } from '../../../../types/settings';
import { Definition } from '../Definition';
-const setting: T.Setting = {
+const setting: Setting = {
key: 'foo',
value: '42',
inherited: true,
}
};
+beforeAll(() => {
+ jest.useFakeTimers();
+});
+
+afterAll(() => {
+ jest.useRealTimers();
+});
+
it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
await waitAndUpdate(wrapper);
expect(saveValue).toHaveBeenCalledWith(setting.definition.key, undefined);
expect(wrapper.find('AlertSuccessIcon').exists()).toBe(true);
+ expect(wrapper.state().success).toBe(true);
+ jest.runAllTimers();
+ expect(wrapper.state().success).toBe(false);
+});
+
+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')();
+ await waitAndUpdate(wrapper);
+ expect(resetValue).toHaveBeenCalledWith(setting.definition.key, undefined);
+ expect(cancelChange).toHaveBeenCalledWith(setting.definition.key);
+ expect(wrapper.state().success).toBe(true);
+ jest.runAllTimers();
+ expect(wrapper.state().success).toBe(false);
});
function shallowRender(props: Partial<Definition['props']> = {}) {
- return shallow(
+ return shallow<Definition>(
<Definition
cancelChange={jest.fn()}
changeValue={jest.fn()}
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { SettingCategoryDefinition } from '../../../../types/settings';
import DefinitionActions from '../DefinitionActions';
-const definition: T.SettingCategoryDefinition = {
+const definition: SettingCategoryDefinition = {
category: 'baz',
description: 'lorem',
fields: [],
*/
import * as React from 'react';
import Select from 'sonar-ui-common/components/controls/Select';
+import { SettingCategoryDefinition } from '../../../../types/settings';
import { DefaultSpecializedInputProps } from '../../utils';
-type Props = DefaultSpecializedInputProps & Pick<T.SettingCategoryDefinition, 'options'>;
+type Props = DefaultSpecializedInputProps & Pick<SettingCategoryDefinition, 'options'>;
export default class InputForSingleSelectList extends React.PureComponent<Props> {
handleInputChange = ({ value }: { value: string }) => {
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { SettingType } from '../../../../types/settings';
import {
DefaultInputProps,
DefaultSpecializedInputProps,
import InputForText from './InputForText';
const typeMapping: {
- [type in T.SettingType]?: React.ComponentType<DefaultSpecializedInputProps>;
+ [type in SettingType]?: React.ComponentType<DefaultSpecializedInputProps>;
} = {
STRING: InputForString,
TEXT: InputForText,
+ JSON: InputForText,
PASSWORD: InputForPassword,
BOOLEAN: InputForBoolean,
INTEGER: InputForNumber,
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { Setting, SettingCategoryDefinition } from '../../../../../types/settings';
import { DefaultInputProps } from '../../../utils';
import Input from '../Input';
key: 'example'
};
-const settingDefinition: T.SettingCategoryDefinition = {
+const settingDefinition: SettingCategoryDefinition = {
category: 'general',
fields: [],
key: 'example',
});
it('should render PropertySetInput', () => {
- const setting: T.Setting = {
+ const setting: Setting = {
...settingValue,
definition: { ...settingDefinition, type: 'PROPERTY_SET' }
};
import { shallow, ShallowWrapper } from 'enzyme';
import * as React from 'react';
import { click } from 'sonar-ui-common/helpers/testUtils';
+import { SettingCategoryDefinition } from '../../../../../types/settings';
import { DefaultInputProps } from '../../../utils';
import MultiValueInput from '../MultiValueInput';
import PrimitiveInput from '../PrimitiveInput';
key: 'example'
};
-const settingDefinition: T.SettingCategoryDefinition = {
+const settingDefinition: SettingCategoryDefinition = {
category: 'general',
fields: [],
key: 'example',
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { fetchSettings } from '../actions';
+import {
+ getSettingsAppChangedValue,
+ getSettingsAppDefinition
+} from '../../../../store/rootReducer';
+import { checkValue, fetchSettings } from '../actions';
import { receiveDefinitions } from '../definitions';
jest.mock('../../../../api/settings', () => ({
receiveDefinitions: jest.fn()
}));
+jest.mock('../../../../store/rootReducer', () => ({
+ getSettingsAppDefinition: jest.fn(),
+ getSettingsAppChangedValue: jest.fn()
+}));
+
it('#fetchSettings should filter LICENSE type settings', async () => {
const dispatch = jest.fn();
}
]);
});
+
+describe('checkValue', () => {
+ const dispatch = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should correctly identify empty strings', () => {
+ (getSettingsAppDefinition as jest.Mock).mockReturnValue({
+ defaultValue: 'hello',
+ type: 'TEXT'
+ });
+
+ (getSettingsAppChangedValue as jest.Mock).mockReturnValue(undefined);
+ const key = 'key';
+ expect(checkValue(key)(dispatch, jest.fn())).toBe(false);
+ expect(dispatch).toBeCalledWith({
+ type: 'settingsPage/FAIL_VALIDATION',
+ key,
+ message: 'settings.state.value_cant_be_empty'
+ });
+ });
+
+ it('should correctly identify empty with no default', () => {
+ (getSettingsAppDefinition as jest.Mock).mockReturnValue({
+ type: 'TEXT'
+ });
+
+ (getSettingsAppChangedValue as jest.Mock).mockReturnValue(undefined);
+
+ const key = 'key';
+ expect(checkValue(key)(dispatch, jest.fn())).toBe(false);
+ expect(dispatch).toBeCalledWith({
+ type: 'settingsPage/FAIL_VALIDATION',
+ key,
+ message: 'settings.state.value_cant_be_empty_no_default'
+ });
+ });
+
+ it('should correctly identify non-empty strings', () => {
+ (getSettingsAppDefinition as jest.Mock).mockReturnValue({
+ type: 'TEXT'
+ });
+
+ (getSettingsAppChangedValue as jest.Mock).mockReturnValue('not empty');
+ const key = 'key';
+ expect(checkValue(key)(dispatch, jest.fn())).toBe(true);
+ expect(dispatch).toBeCalledWith({
+ type: 'settingsPage/PASS_VALIDATION',
+ key
+ });
+ });
+
+ it('should correctly identify misformed JSON', () => {
+ (getSettingsAppDefinition as jest.Mock).mockReturnValue({
+ type: 'JSON'
+ });
+
+ (getSettingsAppChangedValue as jest.Mock).mockReturnValue('{JSON: "asd;{');
+ const key = 'key';
+ expect(checkValue(key)(dispatch, jest.fn())).toBe(false);
+ expect(dispatch).toBeCalledWith({
+ type: 'settingsPage/FAIL_VALIDATION',
+ key,
+ message: 'Unexpected token J in JSON at position 1'
+ });
+ });
+
+ it('should correctly identify correct JSON', () => {
+ (getSettingsAppDefinition as jest.Mock).mockReturnValue({
+ type: 'JSON'
+ });
+
+ (getSettingsAppChangedValue as jest.Mock).mockReturnValue(
+ '{"number": 42, "question": "answer"}'
+ );
+ const key = 'key';
+ expect(checkValue(key)(dispatch, jest.fn())).toBe(true);
+ expect(dispatch).toBeCalledWith({
+ type: 'settingsPage/PASS_VALIDATION',
+ key
+ });
+ });
+});
return false;
}
+ if (definition.type === 'JSON') {
+ try {
+ JSON.parse(value);
+ } catch (e) {
+ dispatch(failValidation(key, e.message));
+ return false;
+ }
+ }
+
dispatch(passValidation(key));
return true;
};
*/
import { keyBy, sortBy, uniqBy } from 'lodash';
import { ActionType } from '../../../store/utils/actions';
+import { SettingCategoryDefinition } from '../../../types/settings';
import { DEFAULT_CATEGORY, getCategoryName } from '../utils';
const enum Actions {
type Action = ActionType<typeof receiveDefinitions, Actions.ReceiveDefinitions>;
-export type State = T.Dict<T.SettingCategoryDefinition>;
+export type State = T.Dict<SettingCategoryDefinition>;
-export function receiveDefinitions(definitions: T.SettingCategoryDefinition[]) {
+export function receiveDefinitions(definitions: SettingCategoryDefinition[]) {
return { type: Actions.ReceiveDefinitions, definitions };
}
import { combineReducers } from 'redux';
import { Action as AppStateAction, Actions as AppStateActions } from '../../../store/appState';
import { ActionType } from '../../../store/utils/actions';
+import { SettingValue } from '../../../types/settings';
enum Actions {
receiveValues = 'RECEIVE_VALUES'
type Action = ActionType<typeof receiveValues, Actions.receiveValues>;
-type SettingsState = T.Dict<T.SettingValue>;
+type SettingsState = T.Dict<SettingValue>;
export interface State {
components: T.Dict<SettingsState>;
export default combineReducers({ components, global });
-export function getValue(
- state: State,
- key: string,
- component?: string
-): T.SettingValue | undefined {
+export function getValue(state: State, key: string, component?: string): SettingValue | undefined {
if (component) {
return state.components[component] && state.components[component][key];
}
*/
import { sanitize } from 'dompurify';
import { hasMessage, translate } from 'sonar-ui-common/helpers/l10n';
+import { Setting, SettingCategoryDefinition, SettingDefinition } from '../../types/settings';
export const DEFAULT_CATEGORY = 'general';
onCancel?: () => void;
onChange: (value: any) => void;
onSave?: () => void;
- setting: T.Setting;
+ setting: Setting;
value: any;
}
});
}
-export function getPropertyName(definition: T.SettingDefinition) {
+export function getPropertyName(definition: SettingDefinition) {
const key = `property.${definition.key}.name`;
return hasMessage(key) ? translate(key) : definition.name;
}
-export function getPropertyDescription(definition: T.SettingDefinition) {
+export function getPropertyDescription(definition: SettingDefinition) {
const key = `property.${definition.key}.description`;
return hasMessage(key) ? translate(key) : definition.description;
}
return hasMessage(key) ? translate(key) : null;
}
-export function getUniqueName(definition: T.SettingDefinition, index?: string) {
+export function getUniqueName(definition: SettingDefinition, index?: string) {
const indexSuffix = index ? `[${index}]` : '';
return `settings[${definition.key}]${indexSuffix}`;
}
-export function getSettingValue({ definition, fieldValues, value, values }: T.Setting) {
+export function getSettingValue({ definition, fieldValues, value, values }: Setting) {
if (isCategoryDefinition(definition) && definition.multiValues) {
return values;
} else if (definition.type === 'PROPERTY_SET') {
}
}
-export function isEmptyValue(definition: T.SettingDefinition, value: any) {
+export function isEmptyValue(definition: SettingDefinition, value: any) {
if (value == null) {
return true;
} else if (definition.type === 'BOOLEAN') {
}
}
-export function isCategoryDefinition(
- item: T.SettingDefinition
-): item is T.SettingCategoryDefinition {
+export function isCategoryDefinition(item: SettingDefinition): item is SettingCategoryDefinition {
return Boolean((item as any).fields);
}
-export function getEmptyValue(item: T.SettingDefinition | T.SettingCategoryDefinition): any {
+export function getEmptyValue(item: SettingDefinition | SettingCategoryDefinition): any {
if (isCategoryDefinition(item)) {
if (item.multiValues) {
return [getEmptyValue({ ...item, multiValues: false })];
return '';
}
-export function isDefaultOrInherited(setting: T.Setting) {
+export function isDefaultOrInherited(setting: Setting) {
return Boolean(setting.inherited);
}
-export function getDefaultValue(setting: T.Setting) {
+export function getDefaultValue(setting: Setting) {
const { definition, parentFieldValues, parentValue, parentValues } = setting;
if (definition.type === 'PASSWORD') {
DefaultProjectVisibility = 'projects.default.visibility',
ServerBaseUrl = 'sonar.core.serverBaseURL'
}
+
+export type Setting = SettingValue & { definition: SettingDefinition };
+
+export type SettingType =
+ | 'STRING'
+ | 'TEXT'
+ | 'JSON'
+ | 'PASSWORD'
+ | 'BOOLEAN'
+ | 'FLOAT'
+ | 'INTEGER'
+ | 'LICENSE'
+ | 'LONG'
+ | 'SINGLE_SELECT_LIST'
+ | 'PROPERTY_SET';
+
+export interface SettingDefinition {
+ description?: string;
+ key: string;
+ multiValues?: boolean;
+ name?: string;
+ options: string[];
+ type?: SettingType;
+}
+
+export interface SettingFieldDefinition extends SettingDefinition {
+ description: string;
+ name: string;
+}
+
+export interface SettingCategoryDefinition extends SettingDefinition {
+ category: string;
+ defaultValue?: string;
+ deprecatedKey?: string;
+ fields: SettingFieldDefinition[];
+ multiValues?: boolean;
+ subCategory: string;
+}
+
+export interface SettingValue {
+ fieldValues?: Array<T.Dict<string>>;
+ inherited?: boolean;
+ key: string;
+ parentFieldValues?: Array<T.Dict<string>>;
+ parentValue?: string;
+ parentValues?: string[];
+ value?: string;
+ values?: string[];
+}
export type RuleType = 'BUG' | 'VULNERABILITY' | 'CODE_SMELL' | 'SECURITY_HOTSPOT' | 'UNKNOWN';
- export type Setting = SettingValue & { definition: SettingDefinition };
-
- export type SettingType =
- | 'STRING'
- | 'TEXT'
- | 'PASSWORD'
- | 'BOOLEAN'
- | 'FLOAT'
- | 'INTEGER'
- | 'LICENSE'
- | 'LONG'
- | 'SINGLE_SELECT_LIST'
- | 'PROPERTY_SET';
-
- export interface SettingDefinition {
- description?: string;
- key: string;
- multiValues?: boolean;
- name?: string;
- options: string[];
- type?: SettingType;
- }
-
- export interface SettingFieldDefinition extends SettingDefinition {
- description: string;
- name: string;
- }
-
- export interface SettingCategoryDefinition extends SettingDefinition {
- category: string;
- defaultValue?: string;
- deprecatedKey?: string;
- fields: SettingFieldDefinition[];
- multiValues?: boolean;
- subCategory: string;
- }
-
- export interface SettingValue {
- fieldValues?: Array<T.Dict<string>>;
- inherited?: boolean;
- key: string;
- parentFieldValues?: Array<T.Dict<string>>;
- parentValue?: string;
- parentValues?: string[];
- value?: string;
- values?: string[];
- }
-
export interface Snippet {
start: number;
end: number;