diff --git a/frontend/src/metabase/admin/settings/components/SettingsLdapForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsLdapForm.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4791c2010f8155ee59dfd891107b32869c14fd1b --- /dev/null +++ b/frontend/src/metabase/admin/settings/components/SettingsLdapForm.jsx @@ -0,0 +1,183 @@ +import React, { Component, PropTypes } from "react"; +import cx from "classnames"; +import _ from "underscore"; + +import MetabaseUtils from "metabase/lib/utils"; +import SettingsSetting from "./SettingsSetting.jsx"; + +export default class SettingsLdapForm extends Component { + + constructor(props, context) { + super(props, context); + + this.state = { + dirty: false, + formData: {}, + submitting: "default", + valid: false, + validationErrors: {} + } + } + + static propTypes = { + elements: PropTypes.array.isRequired, + formErrors: PropTypes.object, + updateLdapSettings: PropTypes.func.isRequired + }; + + componentWillMount() { + // this gives us an opportunity to load up our formData with any existing values for elements + let formData = {}; + this.props.elements.forEach(function(element) { + formData[element.key] = element.value; + }); + + this.setState({formData}); + } + + componentDidMount() { + this.validateForm(); + } + + componentDidUpdate() { + this.validateForm(); + } + + setSubmitting(submitting) { + this.setState({submitting}); + } + + setFormErrors(formErrors) { + this.setState({formErrors}); + } + + // return null if element passes validation, otherwise return an error message + validateElement([validationType, validationMessage], value, element) { + if (MetabaseUtils.isEmpty(value)) return; + + switch (validationType) { + case "integer": + return isNaN(parseInt(value)) ? (validationMessage || "That's not a valid integer") : null; + } + } + + validateForm() { + let { elements } = this.props; + let { formData } = this.state; + + let valid = true, + validationErrors = {}; + + elements.forEach(function(element) { + // test for required elements + if (element.required && MetabaseUtils.isEmpty(formData[element.key])) { + valid = false; + } + + if (element.validations) { + element.validations.forEach(function(validation) { + validationErrors[element.key] = this.validateElement(validation, formData[element.key], element); + if (validationErrors[element.key]) valid = false; + }, this); + } + }, this); + + if (this.state.valid !== valid || !_.isEqual(this.state.validationErrors, validationErrors)) { + this.setState({ valid, validationErrors }); + } + } + + handleChangeEvent(element, value, event) { + this.setState({ + dirty: true, + formData: { ...this.state.formData, [element.key]: (MetabaseUtils.isEmpty(value)) ? null : value } + }); + } + + handleFormErrors(error) { + // parse and format + let formErrors = {}; + if (error.data && error.data.message) { + formErrors.message = error.data.message; + } else { + formErrors.message = "Looks like we ran into some problems"; + } + + if (error.data && error.data.errors) { + formErrors.elements = error.data.errors; + } + + return formErrors; + } + + updateLdapSettings(e) { + e.preventDefault(); + + this.setState({ + formErrors: null, + submitting: "working" + }); + + let { formData, valid } = this.state; + + if (valid) { + this.props.updateLdapSettings(formData).then(() => { + this.setState({ + dirty: false, + submitting: "success" + }); + + // show a confirmation for 3 seconds, then return to normal + setTimeout(() => this.setState({ submitting: "default" }), 3000); + }, (error) => { + this.setState({ + submitting: "default", + formErrors: this.handleFormErrors(error) + }); + }); + } + } + + render() { + let { elements } = this.props; + let { formData, formErrors, submitting, valid, validationErrors } = this.state; + + let settings = elements.map((element, index) => { + // merge together data from a couple places to provide a complete view of the Element state + let errorMessage = (formErrors && formErrors.elements) ? formErrors.elements[element.key] : validationErrors[element.key]; + let value = formData[element.key] == null ? element.defaultValue : formData[element.key]; + + return ( + <SettingsSetting + key={element.key} + setting={{ ...element, value }} + updateSetting={this.handleChangeEvent.bind(this, element)} + errorMessage={errorMessage} + /> + ); + }); + + let saveSettingsButtonStates = { + default: "Save changes", + working: "Saving...", + success: "Changes saved!" + }; + + let disabled = (!valid || submitting !== "default"), + saveButtonText = saveSettingsButtonStates[submitting]; + + return ( + <form noValidate> + <ul> + {settings} + <li className="m2 mb4"> + <button className={cx("Button mr2", {"Button--primary": !disabled}, {"Button--success-new": submitting === "success"})} disabled={disabled} onClick={this.updateLdapSettings.bind(this)}> + {saveButtonText} + </button> + { formErrors && formErrors.message ? <span className="pl2 text-error text-bold">{formErrors.message}</span> : null } + </li> + </ul> + </form> + ); + } +} diff --git a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx index 83e121977c4634a7a59b361963bfa73e21636f57..08b1f8deb083688194929d24a6dbf28cca39670d 100644 --- a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx +++ b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx @@ -11,6 +11,7 @@ import AdminLayout from "metabase/components/AdminLayout.jsx"; import SettingsSetting from "../components/SettingsSetting.jsx"; import SettingsEmailForm from "../components/SettingsEmailForm.jsx"; import SettingsSlackForm from "../components/SettingsSlackForm.jsx"; +import SettingsLdapForm from "../components/SettingsLdapForm.jsx"; import SettingsSetupList from "../components/SettingsSetupList.jsx"; import SettingsUpdatesForm from "../components/SettingsUpdatesForm.jsx"; import SettingsSingleSignOnForm from "../components/SettingsSingleSignOnForm.jsx"; @@ -51,6 +52,7 @@ export default class SettingsEditorApp extends Component { updateSetting: PropTypes.func.isRequired, updateEmailSettings: PropTypes.func.isRequired, updateSlackSettings: PropTypes.func.isRequired, + updateLdapSettings: PropTypes.func.isRequired, sendTestEmail: PropTypes.func.isRequired }; @@ -136,8 +138,15 @@ export default class SettingsEditorApp extends Component { updateSetting={this.updateSetting} /> ); + } else if (activeSection.name === "LDAP") { + return ( + <SettingsLdapForm + ref="ldapForm" + elements={activeSection.settings} + updateLdapSettings={this.props.updateLdapSettings} + /> + ); } else { - return ( <ul> {activeSection.settings diff --git a/frontend/src/metabase/admin/settings/selectors.js b/frontend/src/metabase/admin/settings/selectors.js index c0bf672f6e863d624d7222e4c0aeebad2ba2d04c..8f6f5287565d1b8f51042b958e624136b4b55e2c 100644 --- a/frontend/src/metabase/admin/settings/selectors.js +++ b/frontend/src/metabase/admin/settings/selectors.js @@ -178,7 +178,6 @@ const SECTIONS = [ display_name: "LDAP Port", placeholder: "389", type: "string", - required: true, validations: [["integer", "That's not a valid port number"]] }, { @@ -187,7 +186,7 @@ const SECTIONS = [ description: null, type: "radio", options: { none: "None", ssl: "SSL", starttls: "StartTLS" }, - defaultValue: 'none' + defaultValue: "none" }, { key: "ldap-bind-dn", @@ -210,26 +209,22 @@ const SECTIONS = [ { key: "ldap-user-filter", display_name: "User filter", - type: "string", - required: true + type: "string" }, { key: "ldap-attribute-email", display_name: "User email attribute", - type: "string", - required: true + type: "string" }, { key: "ldap-attribute-firstname", display_name: "User first name attribute", - type: "string", - required: true + type: "string" }, { key: "ldap-attribute-lastname", display_name: "User last name attribute", - type: "string", - required: true + type: "string" } ] }, diff --git a/frontend/src/metabase/admin/settings/settings.js b/frontend/src/metabase/admin/settings/settings.js index cc63d4829c5fc1a0545b1fe181c2c951900b4cc2..9a27de73fa8314b552fb38663658d93981497ea5 100644 --- a/frontend/src/metabase/admin/settings/settings.js +++ b/frontend/src/metabase/admin/settings/settings.js @@ -1,7 +1,7 @@ import { createThunkAction, handleActions, combineReducers } from "metabase/lib/redux"; -import { SettingsApi, EmailApi, SlackApi } from "metabase/services"; +import { SettingsApi, EmailApi, SlackApi, LdapApi } from "metabase/services"; import { refreshSiteSettings } from "metabase/redux/settings"; @@ -71,6 +71,20 @@ export const updateSlackSettings = createThunkAction(UPDATE_SLACK_SETTINGS, func }; }, {}); +// updateEmailSettings +export const UPDATE_LDAP_SETTINGS = "metabase/admin/settings/UPDATE_LDAP_SETTINGS"; +export const updateLdapSettings = createThunkAction(UPDATE_LDAP_SETTINGS, function(settings) { + return async function(dispatch, getState) { + try { + await LdapApi.updateSettings(settings); + await dispatch(refreshSiteSettings()); + } catch(error) { + console.log("error updating LDAP settings", settings, error); + throw error; + } + }; +}); + export const RELOAD_SETTINGS = "metabase/admin/settings/RELOAD_SETTINGS"; export const reloadSettings = createThunkAction(RELOAD_SETTINGS, function() { return async function(dispatch, getState) { diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js index 7705b6c16e9de928820cfaf621fab1616a4b815b..08b90259c62106126814fe5a1e61ace5657a736c 100644 --- a/frontend/src/metabase/services.js +++ b/frontend/src/metabase/services.js @@ -85,6 +85,10 @@ export const SlackApi = { updateSettings: PUT("/api/slack/settings"), }; +export const LdapApi = { + updateSettings: PUT("/api/ldap/settings") +}; + export const MetabaseApi = { db_list: GET("/api/database"), db_list_with_tables: GET("/api/database?include_tables=true"),