summaryrefslogtreecommitdiffstats
path: root/src/generic-components/input/validation
diff options
context:
space:
mode:
Diffstat (limited to 'src/generic-components/input/validation')
-rw-r--r--src/generic-components/input/validation/ValidationButtons.jsx69
-rw-r--r--src/generic-components/input/validation/ValidationForm.jsx173
-rw-r--r--src/generic-components/input/validation/ValidationInput.jsx354
-rw-r--r--src/generic-components/input/validation/ValidationTab.jsx144
-rw-r--r--src/generic-components/input/validation/ValidationTabs.jsx105
5 files changed, 845 insertions, 0 deletions
diff --git a/src/generic-components/input/validation/ValidationButtons.jsx b/src/generic-components/input/validation/ValidationButtons.jsx
new file mode 100644
index 0000000..d46740d
--- /dev/null
+++ b/src/generic-components/input/validation/ValidationButtons.jsx
@@ -0,0 +1,69 @@
+/*
+ * ============LICENSE_START===================================================
+ * SPARKY (AAI UI service)
+ * ============================================================================
+ * Copyright © 2017 AT&T Intellectual Property.
+ * Copyright © 2017 Amdocs
+ * All rights reserved.
+ * ============================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=====================================================
+ *
+ * ECOMP and OpenECOMP are trademarks
+ * and service marks of AT&T Intellectual Property.
+ */
+
+/**
+ * Holds the buttons for save/reset for forms.
+ * Used by the ValidationForm that changes the state of the buttons according
+ * to its own state.
+ *
+ * properties:
+ * labledButtons - whether or not to use labeled buttons or icons only
+ */
+import React from 'react';
+import i18n from 'utils/i18n/i18n.js';
+import Button from 'react-bootstrap/lib/Button.js';
+import FontAwesome from 'react-fontawesome';
+
+class ValidationButtons extends React.Component {
+
+ static propTypes = {
+ labledButtons: React.PropTypes.bool.isRequired,
+ isReadOnlyMode: React.PropTypes.bool
+ };
+
+ state = {
+ isValid: this.props.formValid
+ };
+
+ render() {
+ var submitBtn = this.props.labledButtons ? i18n('Save') :
+ <FontAwesome className='check' name='check'/>;
+ var closeBtn = this.props.labledButtons ? i18n('Cancel') :
+ <FontAwesome className='close' name='close'/>;
+ return (
+ <div className='validation-buttons'>
+ {!this.props.isReadOnlyMode ?
+ <div>
+ <Button bsStyle='primary' ref='submitbutton' type='submit'
+ disabled={!this.state.isValid}>{submitBtn}</Button>
+ <Button type='reset'>{closeBtn}</Button>
+ </div>
+ : <Button type='reset'>{i18n('Close')}</Button>
+ }
+ </div>
+ );
+ }
+}
+export default ValidationButtons;
diff --git a/src/generic-components/input/validation/ValidationForm.jsx b/src/generic-components/input/validation/ValidationForm.jsx
new file mode 100644
index 0000000..9b16c29
--- /dev/null
+++ b/src/generic-components/input/validation/ValidationForm.jsx
@@ -0,0 +1,173 @@
+/*
+ * ============LICENSE_START===================================================
+ * SPARKY (AAI UI service)
+ * ============================================================================
+ * Copyright © 2017 AT&T Intellectual Property.
+ * Copyright © 2017 Amdocs
+ * All rights reserved.
+ * ============================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=====================================================
+ *
+ * ECOMP and OpenECOMP are trademarks
+ * and service marks of AT&T Intellectual Property.
+ */
+
+/**
+ * ValidationForm should be used in order to have a form that handles it's
+ * internal validation state. All ValidationInputs inside the form are checked
+ * for validity and the styling and submit buttons are updated accordingly.
+ *
+ * The properties that ahould be given to the form:
+ * labledButtons - whether or not use icons only as the form default buttons or
+ * use buttons with labels onSubmit - function for click on the submit button
+ * onReset - function for click on the reset button
+ */
+import React from 'react';
+import ValidationButtons from './ValidationButtons.jsx';
+
+class ValidationForm extends React.Component {
+
+ static childContextTypes = {
+ validationParent: React.PropTypes.any,
+ isReadOnlyMode: React.PropTypes.bool
+ };
+
+ static defaultProps = {
+ hasButtons: true,
+ onSubmit: null,
+ onReset: null,
+ labledButtons: true,
+ onValidChange: null,
+ isValid: true
+ };
+
+ static propTypes = {
+ isValid: React.PropTypes.bool,
+ hasButtons: React.PropTypes.bool,
+ onSubmit: React.PropTypes.func,
+ onReset: React.PropTypes.func,
+ labledButtons: React.PropTypes.bool,
+ onValidChange: React.PropTypes.func
+ };
+
+ state = {
+ isValid: this.props.isValid
+ };
+
+ constructor(props) {
+ super(props);
+ this.validationComponents = [];
+ }
+
+ render() {
+ var buttons = (this.props.hasButtons) ?
+ <ValidationButtons labledButtons={this.props.labledButtons}
+ ref='buttons'
+ isReadOnlyMode={this.props.isReadOnlyMode}/>
+ : null;
+ return (
+ <form {...this.props} onSubmit={event => this.handleFormSubmit(event)}>
+ <div className='validation-form-content'>{this.props.children}</div>
+ {buttons}
+ </form>
+ );
+ }
+
+ handleFormSubmit(event) {
+ event.preventDefault();
+ let isFormValid = true;
+ this.validationComponents.forEach(validationComponent => {
+ const isInputValid = validationComponent.validate().isValid;
+ isFormValid = isInputValid && isFormValid;
+ });
+ if (isFormValid && this.props.onSubmit) {
+ this.props.onSubmit(event);
+ } else if (!isFormValid) {
+ this.setState({isValid: false});
+ }
+ };
+
+ componentDidUpdate(prevProps, prevState) {
+ // only handling this programatically if the validation of the form is done
+ // outside of the view (example with a form that is dependent on the state
+ // of other forms)
+ if (prevProps.isValid !== this.props.isValid) {
+ if (this.props.hasButtons) {
+ this.refs.buttons.setState({isValid: this.state.isValid});
+ }
+ } else if (this.state.isValid !== prevState.isValid) {
+ if (this.props.hasButtons) {
+ this.refs.buttons.setState({isValid: this.state.isValid});
+ }
+ // callback in case form is part of bigger picture in view
+ if (this.props.onValidChange) {
+ this.props.onValidChange(this.state.isValid);
+ }
+ }
+ }
+
+ componentDidMount() {
+ if (this.props.hasButtons) {
+ this.refs.buttons.setState({isValid: this.state.isValid});
+ }
+ }
+
+
+ getChildContext() {
+ return {
+ validationParent: this,
+ isReadOnlyMode: this.props.isReadOnlyMode
+ };
+ }
+
+
+ /***
+ * Used by ValidationInput in order to let the (parent) form know
+ * the valid state. If there is a change in the state of the form,
+ * the buttons will be updated.
+ *
+ * @param validationComponent
+ * @param isValid
+ */
+ childValidStateChanged(validationComponent, isValid) {
+ if (isValid !== this.state.isValid) {
+ let oldState = this.state.isValid;
+ let newState = isValid &&
+ this.validationComponents.filter(
+ otherValidationComponent => validationComponent !==
+ otherValidationComponent).every(otherValidationComponent => {
+ return otherValidationComponent.isValid();
+ });
+
+ if (oldState !== newState) {
+ this.setState({isValid: newState});
+ }
+ }
+ }
+
+ register(validationComponent) {
+ this.validationComponents.push(validationComponent);
+ }
+
+ unregister(validationComponent) {
+ this.childValidStateChanged(validationComponent, true);
+ this.validationComponents =
+ this.validationComponents.filter(
+ otherValidationComponent => validationComponent !==
+ otherValidationComponent);
+ }
+}
+
+
+export default ValidationForm;
diff --git a/src/generic-components/input/validation/ValidationInput.jsx b/src/generic-components/input/validation/ValidationInput.jsx
new file mode 100644
index 0000000..f030cd6
--- /dev/null
+++ b/src/generic-components/input/validation/ValidationInput.jsx
@@ -0,0 +1,354 @@
+/*
+ * ============LICENSE_START===================================================
+ * SPARKY (AAI UI service)
+ * ============================================================================
+ * Copyright © 2017 AT&T Intellectual Property.
+ * Copyright © 2017 Amdocs
+ * All rights reserved.
+ * ============================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=====================================================
+ *
+ * ECOMP and OpenECOMP are trademarks
+ * and service marks of AT&T Intellectual Property.
+ */
+
+/**
+ * Used for inputs on a validation form.
+ * All properties will be passed on to the input element.
+ *
+ * The following properties can be set for OOB validations and callbacks:
+ - required: Boolean: Should be set to true if the input must have a value
+ - numeric: Boolean : Should be set to true id the input should be an integer
+ - onChange : Function : Will be called to validate the value if the default validations are not sufficient, should return a boolean value
+ indicating whether the value is valid
+ - didUpdateCallback :Function: Will be called after the state has been updated and the component has rerendered. This can be used if
+ there are dependencies between inputs in a form.
+ *
+ * The following properties of the state can be set to determine
+ * the state of the input from outside components:
+ - isValid : Boolean - whether the value is valid
+ - value : value for the input field,
+ - disabled : Boolean,
+ - required : Boolean - whether the input value must be filled out.
+ */
+import React from 'react';
+import ReactDOM from 'react-dom';
+import Validator from 'validator';
+import Input from 'react-bootstrap/lib/Input.js';
+import Overlay from 'react-bootstrap/lib/Overlay.js';
+import Tooltip from 'react-bootstrap/lib/Tooltip.js';
+import isEqual from 'lodash/lang/isEqual.js';
+import i18n from 'utils/i18n/i18n.js';
+
+import InputOptions from '../inputOptions/InputOptions.jsx';
+
+const globalValidationFunctions = {
+ required: value => value !== '',
+ maxLength: (value, length) => Validator.isLength(value, {max: length}),
+ minLength: (value, length) => Validator.isLength(value, {min: length}),
+ pattern: (value, pattern) => Validator.matches(value, pattern),
+ numeric: value => Validator.isNumeric(value),
+ maxValue: (value, maxValue) => value < maxValue,
+ alphanumeric: value => Validator.isAlphanumeric(value),
+ alphanumericWithSpaces: value => Validator.isAlphanumeric(
+ value.replace(/ /g, '')),
+ validateName: value => Validator.isAlphanumeric(
+ value.replace(/\s|\.|\_|\-/g, ''), 'en-US'),
+ validateVendorName: value => Validator.isAlphanumeric(
+ value.replace(/[\x7F-\xFF]|\s/g, ''), 'en-US'),
+ freeEnglishText: value => Validator.isAlphanumeric(
+ value.replace(/\s|\.|\_|\-|\,|\(|\)|\?/g, ''), 'en-US'),
+ email: value => Validator.isEmail(value),
+ ip: value => Validator.isIP(value),
+ url: value => Validator.isURL(value)
+};
+
+const globalValidationMessagingFunctions = {
+ required: () => i18n('Field is required'),
+ maxLength: (value, maxLength) => i18n(
+ 'Field value has exceeded it\'s limit, {maxLength}. current length: {length}',
+ {
+ length: value.length,
+ maxLength
+ }),
+ minLength: (value, minLength) => i18n(
+ 'Field value should contain at least {minLength} characters.', {minLength}),
+ pattern: (value, pattern) => i18n(
+ 'Field value should match the pattern: {pattern}.', {pattern}),
+ numeric: () => i18n('Field value should contain numbers only.'),
+ maxValue: (value, maxValue) => i18n(
+ 'Field value should be less then: {maxValue}.', {maxValue}),
+ alphanumeric: () => i18n(
+ 'Field value should contain letters or digits only.'),
+ alphanumericWithSpaces: () => i18n(
+ 'Field value should contain letters, digits or spaces only.'),
+ validateName: ()=> i18n(
+ 'Field value should contain English letters, digits , spaces, underscores, dashes and dots only.'),
+ validateVendorName: ()=> i18n(
+ 'Field value should contain English letters digits and spaces only.'),
+ freeEnglishText: ()=> i18n(
+ 'Field value should contain English letters, digits , spaces, underscores, dashes and dots only.'),
+ email: () => i18n('Field value should be a valid email address.'),
+ ip: () => i18n('Field value should be a valid ip address.'),
+ url: () => i18n('Field value should be a valid url address.'),
+ general: () => i18n('Field value is invalid.')
+};
+
+class ValidationInput extends React.Component {
+
+ static contextTypes = {
+ validationParent: React.PropTypes.any,
+ isReadOnlyMode: React.PropTypes.bool
+ };
+
+ static defaultProps = {
+ onChange: null,
+ disabled: null,
+ didUpdateCallback: null,
+ validations: {},
+ value: ''
+ };
+
+ static propTypes = {
+ onChange: React.PropTypes.func,
+ disabled: React.PropTypes.bool,
+ didUpdateCallback: React.PropTypes.func,
+ validations: React.PropTypes.object
+ };
+
+
+ state = {
+ isValid: true,
+ style: null,
+ value: this.props.value,
+ error: {},
+ previousErrorMessage: '',
+ wasInvalid: false
+ };
+
+ componentWillReceiveProps({value: nextValue, validations: nextValidaions}) {
+ if (this.state.wasInvalid) {
+ const {validations, value} = this.props;
+ if (value !== nextValue || !isEqual(validations, nextValidaions)) {
+ this.validate(nextValue, nextValidaions);
+ }
+ } else if (this.props.value !== nextValue) {
+ this.setState({value: nextValue});
+ }
+ }
+
+ render() {
+ let {isMultiSelect, onOtherChange, type} = this.props;
+
+ let groupClasses = this.props.groupClassName || '';
+ if (this.props.validations.required) {
+ groupClasses += ' required';
+ }
+ let isReadOnlyMode = this.context.isReadOnlyMode;
+
+ return (
+ <div className='validation-input-wrapper'>
+ {
+ !isMultiSelect && !onOtherChange && type !== 'select'
+ && <Input
+ {...this.props}
+ groupClassName={groupClasses}
+ ref={'_myInput'}
+ value={this.state.value}
+ disabled={isReadOnlyMode || Boolean(this.props.disabled)}
+ bsStyle={this.state.style}
+ onChange={() => this.changedInput()}
+ onBlur={() => this.blurInput()}>
+ {this.props.children}
+ </Input>
+ }
+ {
+ (isMultiSelect || onOtherChange || type === 'select')
+ && <InputOptions onInputChange={() => this.changedInput()}
+ onBlur={() => this.blurInput()}
+ hasError={!this.state.isValid}
+ ref={'_myInput'} {...this.props} />
+ }
+ {this.renderOverlay()}
+ </div>
+ );
+ }
+
+ renderOverlay() {
+ let position = 'right';
+ if (this.props.type === 'text'
+ || this.props.type === 'email'
+ || this.props.type === 'number'
+ || this.props.type === 'password'
+
+ ) {
+ position = 'bottom';
+ }
+
+ let validationMessage = this.state.error.message ||
+ this.state.previousErrorMessage;
+ return (
+ <Overlay
+ show={!this.state.isValid}
+ placement={position}
+ target={() => {let target = ReactDOM.findDOMNode(this.refs._myInput); return target.offsetParent ? target : undefined;}}
+ container={this}>
+ <Tooltip
+ id={`error-${validationMessage.replace(' ','-')}`}
+ className='validation-error-message'>
+ {validationMessage}
+ </Tooltip>
+ </Overlay>
+ );
+ }
+
+ componentDidMount() {
+ if (this.context.validationParent) {
+ this.context.validationParent.register(this);
+ }
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (this.context.validationParent) {
+ if (prevState.isValid !== this.state.isValid) {
+ this.context.validationParent.childValidStateChanged(this,
+ this.state.isValid);
+ }
+ }
+ if (this.props.didUpdateCallback) {
+ this.props.didUpdateCallback();
+ }
+
+ }
+
+ componentWillUnmount() {
+ if (this.context.validationParent) {
+ this.context.validationParent.unregister(this);
+ }
+ }
+
+ /***
+ * Adding same method as the actual input component
+ * @returns {*}
+ */
+ getValue() {
+ if (this.props.type === 'checkbox') {
+ return this.refs._myInput.getChecked();
+ }
+ return this.refs._myInput.getValue();
+ }
+
+ resetValue() {
+ this.setState({value: this.props.value});
+ }
+
+
+ /***
+ * internal method that validated the value. includes callback to the
+ * onChange method
+ * @param value
+ * @param validations - map containing validation id and the limitation
+ * describing the validation.
+ * @returns {object}
+ */
+ validateValue = (value, validations) => {
+ let {customValidationFunction} = validations;
+ let error = {};
+ let isValid = true;
+ for (let validation in validations) {
+ if ('customValidationFunction' !== validation) {
+ if (validations[validation]) {
+ if (!globalValidationFunctions[validation](value,
+ validations[validation])) {
+ error.id = validation;
+ error.message =
+ globalValidationMessagingFunctions[validation](value,
+ validations[validation]);
+ isValid = false;
+ break;
+ }
+ }
+ } else {
+ let customValidationResult = customValidationFunction(value);
+
+ if (customValidationResult !== true) {
+ error.id = 'custom';
+ isValid = false;
+ if (typeof customValidationResult === 'string') {//custom validation error message supplied.
+ error.message = customValidationResult;
+ } else {
+ error.message = globalValidationMessagingFunctions.general();
+ }
+ break;
+ }
+
+
+ }
+ }
+
+ return {
+ isValid,
+ error
+ };
+ };
+
+ /***
+ * Internal method that handles the change event of the input. validates and
+ * updates the state.
+ */
+ changedInput() {
+
+ let {isValid, error} = this.state.wasInvalid ? this.validate() : this.state;
+ let onChange = this.props.onChange;
+ if (onChange) {
+ onChange(this.getValue(), isValid, error);
+ }
+ };
+
+ blurInput() {
+ if (!this.state.wasInvalid) {
+ this.setState({wasInvalid: true});
+ }
+
+ let {isValid, error} = !this.state.wasInvalid
+ ? this.validate()
+ : this.state;
+ let onBlur = this.props.onBlur;
+ if (onBlur) {
+ onBlur(this.getValue(), isValid, error);
+ }
+ };
+
+ validate(value = this.getValue(), validations = this.props.validations) {
+ let validationStatus = this.validateValue(value, validations);
+ let {isValid, error} = validationStatus;
+ let _style = isValid ? null : 'error';
+ this.setState({
+ isValid,
+ error,
+ value,
+ previousErrorMessage: this.state.error.message || '',
+ style: _style,
+ wasInvalid: !isValid || this.state.wasInvalid
+ });
+
+ return validationStatus;
+ }
+
+ isValid() {
+ return this.state.isValid;
+ }
+
+}
+export default ValidationInput;
diff --git a/src/generic-components/input/validation/ValidationTab.jsx b/src/generic-components/input/validation/ValidationTab.jsx
new file mode 100644
index 0000000..a40df6a
--- /dev/null
+++ b/src/generic-components/input/validation/ValidationTab.jsx
@@ -0,0 +1,144 @@
+/*
+ * ============LICENSE_START===================================================
+ * SPARKY (AAI UI service)
+ * ============================================================================
+ * Copyright © 2017 AT&T Intellectual Property.
+ * Copyright © 2017 Amdocs
+ * All rights reserved.
+ * ============================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=====================================================
+ *
+ * ECOMP and OpenECOMP are trademarks
+ * and service marks of AT&T Intellectual Property.
+ */
+
+import React from 'react';
+import Tab from 'react-bootstrap/lib/Tab.js';
+
+export default
+class ValidationTab extends React.Component {
+
+ static propTypes = {
+ children: React.PropTypes.node,
+ eventKey: React.PropTypes.any.isRequired,
+ onValidationStateChange: React.PropTypes.func //This property is assigned
+ // dynamically via
+ // React.cloneElement. lookup
+ // ValidationTabs.jsx.
+ // therefore it cannot be
+ // stated as required!
+ };
+
+ constructor(props) {
+ super(props);
+ this.validationComponents = [];
+ }
+
+ static childContextTypes = {
+ validationParent: React.PropTypes.any
+ };
+
+ static contextTypes = {
+ validationParent: React.PropTypes.any
+ };
+
+ getChildContext() {
+ return {validationParent: this};
+ }
+
+ state = {
+ isValid: true,
+ notifyParent: false
+ };
+
+ componentDidMount() {
+ let validationParent = this.context.validationParent;
+ if (validationParent) {
+ validationParent.register(this);
+ }
+ }
+
+ componentWillUnmount() {
+ let validationParent = this.context.validationParent;
+ if (validationParent) {
+ validationParent.unregister(this);
+ }
+ }
+
+ register(validationComponent) {
+ this.validationComponents.push(validationComponent);
+ }
+
+ unregister(validationComponent) {
+ this.childValidStateChanged(validationComponent, true);
+ this.validationComponents =
+ this.validationComponents.filter(
+ otherValidationComponent => validationComponent !==
+ otherValidationComponent);
+ }
+
+ notifyValidStateChangedToParent(isValid) {
+
+ let validationParent = this.context.validationParent;
+ if (validationParent) {
+ validationParent.childValidStateChanged(this, isValid);
+ }
+ }
+
+ childValidStateChanged(validationComponent, isValid) {
+
+ const currentValidState = this.state.isValid;
+ if (isValid !== currentValidState) {
+ let filteredValidationComponents = this.validationComponents.filter(
+ otherValidationComponent => validationComponent !==
+ otherValidationComponent);
+ let newValidState = isValid &&
+ filteredValidationComponents.every(otherValidationComponent => {
+ return otherValidationComponent.isValid();
+ });
+ this.setState({isValid: newValidState, notifyParent: true});
+ }
+ }
+
+ validate() {
+ let isValid = true;
+ this.validationComponents.forEach(validationComponent => {
+ const isValidationComponentValid = validationComponent.validate().isValid;
+ isValid = isValidationComponentValid && isValid;
+ });
+ this.setState({isValid, notifyParent: false});
+ return {isValid};
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (prevState.isValid !== this.state.isValid) {
+ if (this.state.notifyParent) {
+ this.notifyValidStateChangedToParent(this.state.isValid);
+ }
+ this.props.onValidationStateChange(this.props.eventKey,
+ this.state.isValid);
+ }
+ }
+
+ isValid() {
+ return this.state.isValid;
+ }
+
+ render() {
+ let {children, ...tabProps} = this.props;
+ return (
+ <Tab {...tabProps}>{children}</Tab>
+ );
+ }
+}
diff --git a/src/generic-components/input/validation/ValidationTabs.jsx b/src/generic-components/input/validation/ValidationTabs.jsx
new file mode 100644
index 0000000..ea22f89
--- /dev/null
+++ b/src/generic-components/input/validation/ValidationTabs.jsx
@@ -0,0 +1,105 @@
+/*
+ * ============LICENSE_START===================================================
+ * SPARKY (AAI UI service)
+ * ============================================================================
+ * Copyright © 2017 AT&T Intellectual Property.
+ * Copyright © 2017 Amdocs
+ * All rights reserved.
+ * ============================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=====================================================
+ *
+ * ECOMP and OpenECOMP are trademarks
+ * and service marks of AT&T Intellectual Property.
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import Tabs from 'react-bootstrap/lib/Tabs.js';
+import Overlay from 'react-bootstrap/lib/Overlay.js';
+import Tooltip from 'react-bootstrap/lib/Tooltip.js';
+
+import i18n from 'utils/i18n/i18n.js';
+
+export default
+class ValidationTab extends React.Component {
+
+ static propTypes = {
+ children: React.PropTypes.node
+ };
+
+ state = {
+ invalidTabs: []
+ };
+
+ cloneTab(element) {
+ const {invalidTabs} = this.state;
+ return React.cloneElement(
+ element,
+ {
+ key: element.props.eventKey,
+ tabClassName: invalidTabs.indexOf(element.props.eventKey) > -1
+ ? 'invalid-tab'
+ : 'valid-tab',
+ onValidationStateChange: (
+ eventKey, isValid) => this.validTabStateChanged(eventKey, isValid)
+ }
+ );
+ }
+
+ validTabStateChanged(eventKey, isValid) {
+ let {invalidTabs} = this.state;
+ let invalidTabIndex = invalidTabs.indexOf(eventKey);
+ if (isValid && invalidTabIndex > -1) {
+ this.setState({
+ invalidTabs: invalidTabs.filter(
+ otherEventKey => eventKey !== otherEventKey)
+ });
+ } else if (!isValid && invalidTabIndex === -1) {
+ this.setState({invalidTabs: [...invalidTabs, eventKey]});
+ }
+ }
+
+ showTabsError() {
+ const {invalidTabs} = this.state;
+ return invalidTabs.length >
+ 0 &&
+ (invalidTabs.length > 1 || invalidTabs[0] !== this.props.activeKey);
+ }
+
+ render() {
+ return (
+ <div>
+ <Tabs {...this.props} ref='tabsList'>
+ {this.props.children.map(element => this.cloneTab(element))}
+ </Tabs>
+ <Overlay
+ animation={false}
+ show={this.showTabsError()}
+ placement='bottom'
+ target={() => {
+ let target = ReactDOM.findDOMNode(this.refs.tabsList).querySelector('ul > li.invalid-tab:not(.active):nth-of-type(n)');
+ return target && target.offsetParent ? target : undefined;
+ }
+ }
+ container={this}>
+ <Tooltip
+ id='error-some-tabs-contain-errors'
+ className='validation-error-message'>
+ {i18n('One or more tabs are invalid')}
+ </Tooltip>
+ </Overlay>
+ </div>
+ );
+ }
+}