import { getValues, resetSettingValue, setSettingValue } from '../../../../api/settings';
import { SubmitButton } from '../../../../components/controls/buttons';
import Tooltip from '../../../../components/controls/Tooltip';
+import { Location, withRouter } from '../../../../components/hoc/withRouter';
+import AlertSuccessIcon from '../../../../components/icons/AlertSuccessIcon';
+import AlertWarnIcon from '../../../../components/icons/AlertWarnIcon';
import DetachIcon from '../../../../components/icons/DetachIcon';
import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
-import { translate } from '../../../../helpers/l10n';
-import { parseError } from '../../../../helpers/request';
+import { translate, translateWithParameters } from '../../../../helpers/l10n';
+import { isSuccessStatus, parseError } from '../../../../helpers/request';
import { getBaseUrl } from '../../../../helpers/system';
import { ExtendedSettingDefinition, SettingType, SettingValue } from '../../../../types/settings';
import SamlFormField from './SamlFormField';
interface SamlAuthenticationProps {
definitions: ExtendedSettingDefinition[];
+ location: Location;
}
interface SamlAuthenticationState {
dirtyFields: string[];
securedFieldsSubmitted: string[];
error: { [key: string]: string };
+ success?: boolean;
}
const CONFIG_TEST_PATH = '/api/saml/validation_init';
SamlAuthenticationProps,
SamlAuthenticationState
> {
+ formFieldRef: React.RefObject<HTMLDivElement> = React.createRef();
+
constructor(props: SamlAuthenticationProps) {
super(props);
const settingValue = props.definitions.map(def => {
this.loadSettingValues(keys);
}
+ componentDidUpdate(prevProps: SamlAuthenticationProps) {
+ const { location } = this.props;
+ if (this.formFieldRef.current && prevProps.location.hash !== location.hash) {
+ this.formFieldRef.current.scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ inline: 'nearest'
+ });
+ }
+ }
+
onFieldChange = (id: string, value: string | boolean) => {
const { settingValue, dirtyFields } = this.state;
const updatedSettingValue = settingValue?.map(set => {
return;
}
- this.setState({ submitting: true, error: {} });
+ this.setState({ submitting: true, error: {}, success: false });
const promises: Promise<void>[] = [];
- settingValue?.forEach(set => {
- const definition = definitions.find(def => def.key === set.key);
- if (definition && set.value !== undefined && dirtyFields.includes(set.key)) {
+ dirtyFields.forEach(field => {
+ const definition = definitions.find(def => def.key === field);
+ const value = settingValue.find(def => def.key === field)?.value;
+ if (definition && value !== undefined) {
const apiCall =
- set.value.length > 0
- ? setSettingValue(definition, set.value)
+ value.length > 0
+ ? setSettingValue(definition, value)
: resetSettingValue({ keys: definition.key });
- const promise = apiCall.catch(async e => {
+
+ promises.push(apiCall);
+ }
+ });
+
+ await Promise.all(promises.map(p => p.catch(e => e))).then(data => {
+ const dataWithError = data
+ .map((data, index) => ({ data, index }))
+ .filter(d => d.data !== undefined && !isSuccessStatus(d.data.status));
+ if (dataWithError.length > 0) {
+ dataWithError.forEach(async d => {
+ const validationMessage = await parseError(d.data as Response);
const { error } = this.state;
- const validationMessage = await parseError(e as Response);
this.setState({
- submitting: false,
- dirtyFields: [],
- error: { ...error, ...{ [set.key]: validationMessage } }
+ error: { ...error, ...{ [dirtyFields[d.index]]: validationMessage } }
});
});
- promises.push(promise);
}
+ this.setState({ success: dirtyFields.length !== dataWithError.length });
});
- await Promise.all(promises);
await this.loadSettingValues(dirtyFields.join(','));
-
this.setState({ submitting: false, dirtyFields: [] });
};
allowEnabling = () => {
- const { settingValue, securedFieldsSubmitted } = this.state;
+ const { settingValue } = this.state;
const enabledFlagSettingValue = settingValue.find(set => set.key === SAML_ENABLED_FIELD);
+
if (enabledFlagSettingValue && enabledFlagSettingValue.value === 'true') {
return true;
}
- for (const setting of settingValue) {
- const isMandatory = !OPTIONAL_FIELDS.includes(setting.key);
- const isSecured = this.isSecuredField(setting.key);
- const isSecuredAndNotSubmitted = isSecured && !securedFieldsSubmitted.includes(setting.key);
- const isNotSecuredAndNotSubmitted =
- !isSecured && (setting.value === '' || setting.value === undefined);
- if (isMandatory && (isSecuredAndNotSubmitted || isNotSecuredAndNotSubmitted)) {
- return false;
- }
- }
- return true;
+ return this.getEmptyRequiredFields().length === 0;
};
onEnableFlagChange = (value: boolean) => {
return null;
};
+ getEmptyRequiredFields = () => {
+ const { settingValue, securedFieldsSubmitted } = this.state;
+ const { definitions } = this.props;
+
+ const updatedRequiredFields: string[] = [];
+
+ for (const setting of settingValue) {
+ const isMandatory = !OPTIONAL_FIELDS.includes(setting.key);
+ const isSecured = this.isSecuredField(setting.key);
+ const isSecuredAndNotSubmitted = isSecured && !securedFieldsSubmitted.includes(setting.key);
+ const isNotSecuredAndNotSubmitted =
+ !isSecured && (setting.value === '' || setting.value === undefined);
+ if (isMandatory && (isSecuredAndNotSubmitted || isNotSecuredAndNotSubmitted)) {
+ const settingDef = definitions.find(def => def.key === setting.key);
+
+ if (settingDef && settingDef.name) {
+ updatedRequiredFields.push(settingDef.name);
+ }
+ }
+ }
+ return updatedRequiredFields;
+ };
+
render() {
const { definitions } = this.props;
- const { submitting, settingValue, securedFieldsSubmitted, error, dirtyFields } = this.state;
+ const {
+ submitting,
+ settingValue,
+ securedFieldsSubmitted,
+ error,
+ dirtyFields,
+ success
+ } = this.state;
const enabledFlagDefinition = definitions.find(def => def.key === SAML_ENABLED_FIELD);
const formIsIncomplete = !this.allowEnabling();
- const preventTestingConfig = formIsIncomplete || dirtyFields.length > 0;
+ const preventTestingConfig = this.getEmptyRequiredFields().length > 0 || dirtyFields.length > 0;
return (
<div>
return null;
}
return (
- <SamlFormField
- settingValue={settingValue?.find(set => set.key === def.key)}
- definition={def}
- mandatory={!OPTIONAL_FIELDS.includes(def.key)}
- onFieldChange={this.onFieldChange}
- showSecuredTextArea={
- !securedFieldsSubmitted.includes(def.key) || dirtyFields.includes(def.key)
- }
- error={error}
+ <div
key={def.key}
- />
+ ref={this.props.location.hash.substring(1) === def.key ? this.formFieldRef : null}>
+ <SamlFormField
+ settingValue={settingValue?.find(set => set.key === def.key)}
+ definition={def}
+ mandatory={!OPTIONAL_FIELDS.includes(def.key)}
+ onFieldChange={this.onFieldChange}
+ showSecuredTextArea={
+ !securedFieldsSubmitted.includes(def.key) || dirtyFields.includes(def.key)
+ }
+ error={error}
+ />
+ </div>
);
})}
- <div className="fixed-footer padded-left padded-right">
+ <div className="fixed-footer padded">
{enabledFlagDefinition && (
- <div>
- <label className="h3 spacer-right">{enabledFlagDefinition.name}</label>
- <SamlToggleField
- definition={enabledFlagDefinition}
- settingValue={settingValue?.find(set => set.key === enabledFlagDefinition.key)}
- toggleDisabled={formIsIncomplete}
- onChange={this.onEnableFlagChange}
- />
- </div>
+ <Tooltip
+ overlay={
+ this.allowEnabling()
+ ? null
+ : translateWithParameters(
+ 'settings.authentication.saml.tooltip.required_fields',
+ this.getEmptyRequiredFields().join(', ')
+ )
+ }>
+ <div className="display-inline-flex-center">
+ <label className="h3 spacer-right">{enabledFlagDefinition.name}</label>
+ <SamlToggleField
+ definition={enabledFlagDefinition}
+ settingValue={settingValue?.find(set => set.key === enabledFlagDefinition.key)}
+ toggleDisabled={formIsIncomplete}
+ onChange={this.onEnableFlagChange}
+ />
+ </div>
+ </Tooltip>
)}
- <div>
+ <div className="display-inline-flex-center">
+ {success && (
+ <div className="spacer-right">
+ <Tooltip
+ overlay={
+ Object.keys(error).length > 0
+ ? translateWithParameters(
+ 'settings.authentication.saml.form.save_warn',
+ Object.keys(error).length
+ )
+ : null
+ }>
+ {Object.keys(error).length > 0 ? (
+ <span>
+ <AlertWarnIcon className="spacer-right" />
+ {translate('settings.authentication.saml.form.save_partial')}
+ </span>
+ ) : (
+ <span>
+ <AlertSuccessIcon className="spacer-right" />
+ {translate('settings.authentication.saml.form.save_success')}
+ </span>
+ )}
+ {}
+ </Tooltip>
+ </div>
+ )}
<SubmitButton className="button-primary spacer-right" onClick={this.onSaveConfig}>
{translate('settings.authentication.saml.form.save')}
<DeferredSpinner className="spacer-left" loading={submitting} />
}
}
-export default SamlAuthentication;
+export default withRouter(SamlAuthentication);
jest.mock('../../../../../api/settings');
+const mockDefinitionFields = [
+ mockDefinition({
+ key: 'test1',
+ category: 'authentication',
+ subCategory: 'saml',
+ name: 'test1',
+ description: 'desc1'
+ }),
+ mockDefinition({
+ key: 'test2',
+ category: 'authentication',
+ subCategory: 'saml',
+ name: 'test2',
+ description: 'desc2'
+ }),
+ mockDefinition({
+ key: 'sonar.auth.saml.certificate.secured',
+ category: 'authentication',
+ subCategory: 'saml',
+ name: 'Certificate',
+ description: 'Secured certificate',
+ type: SettingType.PASSWORD
+ }),
+ mockDefinition({
+ key: 'sonar.auth.saml.enabled',
+ category: 'authentication',
+ subCategory: 'saml',
+ name: 'Enabled',
+ description: 'To enable the flag',
+ type: SettingType.BOOLEAN
+ })
+];
+
let handler: AuthenticationServiceMock;
beforeEach(() => {
it('should allow user to edit fields and save configuration', async () => {
const user = userEvent.setup();
- const definitions = [
- mockDefinition({
- key: 'test1',
- category: 'authentication',
- subCategory: 'saml',
- name: 'test1',
- description: 'desc1'
- }),
- mockDefinition({
- key: 'test2',
- category: 'authentication',
- subCategory: 'saml',
- name: 'test2',
- description: 'desc2'
- }),
- mockDefinition({
- key: 'sonar.auth.saml.certificate.secured',
- category: 'authentication',
- subCategory: 'saml',
- name: 'Certificate',
- description: 'Secured certificate',
- type: SettingType.PASSWORD
- }),
- mockDefinition({
- key: 'sonar.auth.saml.enabled',
- category: 'authentication',
- subCategory: 'saml',
- name: 'Enabled',
- description: 'To enable the flag',
- type: SettingType.BOOLEAN
- })
- ];
+ const definitions = mockDefinitionFields;
renderAuthentication(definitions);
expect(screen.getByRole('button', { name: 'off' })).toHaveAttribute('aria-disabled', 'true');
expect(screen.getByRole('button', { name: 'on' })).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'settings.authentication.saml.form.save' }));
+ expect(screen.getByText('settings.authentication.saml.form.save_success')).toBeInTheDocument();
// check after switching tab that the flag is still enabled
await user.click(screen.getByRole('tab', { name: 'github GitHub' }));
await user.click(screen.getByRole('tab', { name: 'SAML' }));
expect(screen.getByRole('button', { name: 'on' })).toBeInTheDocument();
});
+it('should handle and show error to the user', async () => {
+ const user = userEvent.setup();
+ const definitions = mockDefinitionFields;
+ renderAuthentication(definitions);
+
+ await user.click(screen.getByRole('textbox', { name: 'test1' }));
+ await user.keyboard('value');
+ await user.click(screen.getByRole('textbox', { name: 'test2' }));
+ await user.keyboard('{Control>}a{/Control}error');
+ await user.click(screen.getByRole('button', { name: 'settings.authentication.saml.form.save' }));
+ expect(screen.getByText('settings.authentication.saml.form.save_partial')).toBeInTheDocument();
+});
+
function renderAuthentication(definitions: ExtendedSettingDefinition[]) {
renderComponent(<Authentication definitions={definitions} />);
}