diff options
Diffstat (limited to 'openecomp-ui/src/nfvo-components')
51 files changed, 2350 insertions, 1654 deletions
diff --git a/openecomp-ui/src/nfvo-components/SubmitErrorResponse.jsx b/openecomp-ui/src/nfvo-components/SubmitErrorResponse.jsx index f2ec1582f3..0759f2c28d 100644 --- a/openecomp-ui/src/nfvo-components/SubmitErrorResponse.jsx +++ b/openecomp-ui/src/nfvo-components/SubmitErrorResponse.jsx @@ -1,9 +1,24 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ import React, {Component} from 'react'; import ListGroupItem from 'react-bootstrap/lib/ListGroupItem.js'; -import ListGroup from 'react-bootstrap/lib/ListGroup.js'; -import Panel from 'react-bootstrap/lib/Panel.js'; import i18n from 'nfvo-utils/i18n/i18n.js'; - +import SVGIcon from 'nfvo-components/icon/SVGIcon.jsx'; +import Icon from 'nfvo-components/icon/Icon.jsx'; +import {Collapse} from 'react-bootstrap'; /** * parsing and showing the following Java Response object * @@ -31,103 +46,117 @@ class SubmitErrorResponse extends Component { render() { - let {validationResponse} = this.props; + let {validationResponse : {vspErrors, licensingDataErrors, questionnaireValidationResult, uploadDataErrors}} = this.props; return ( <div className='submit-error-response-view'> - {validationResponse.vspErrors && this.renderVspErrors(validationResponse.vspErrors)} - {validationResponse.licensingDataErrors && this.renderVspErrors(validationResponse.licensingDataErrors)} - {validationResponse.compilationErrors && this.renderCompilationErrors(validationResponse.compilationErrors)} - {validationResponse.uploadDataErrors && this.renderUploadDataErrors(validationResponse.uploadDataErrors)} - {validationResponse.questionnaireValidationResult && this.renderQuestionnaireValidationResult(validationResponse.questionnaireValidationResult)} + {vspErrors && this.renderVspErrors(vspErrors)} + {licensingDataErrors && this.renderVspErrors(licensingDataErrors)} + {questionnaireValidationResult && this.renderComponentsErrors(questionnaireValidationResult)} + {uploadDataErrors && this.renderUploadDataErrors(uploadDataErrors)} </div> ); } - renderVspErrors(vspErrors) { + renderVspErrors(errors) { return ( - <Panel header={i18n('VSP Errors')} collapsible>{this.parseErrorCodeCollection(vspErrors)}</Panel> + <ErrorBlock errorType={i18n('VSP Errors')}> + <div> + {errors.length && errors.map(error=>{return (<ErrorMessage error={error.message}/>);})} + </div> + </ErrorBlock> ); } - renderLicensingDataErrors(licensingDataErrors) { + + renderComponentsErrors(errors) { return ( - <Panel - header={i18n('Licensing Data Errors')} - collapsible>{this.parseErrorCodeCollection(licensingDataErrors)} - </Panel> + <ErrorBlock errorType={i18n('Components Errors')}> + <div> + {errors.validationData.length && errors.validationData.map(item =>{ return (<ComponentError item={item}/>);})} + </div> + </ErrorBlock> ); } renderUploadDataErrors(uploadDataErrors) { return ( - <Panel - header={i18n('Upload Data Errors')} - collapsible>{this.parseMapOfErrorMessagesList(uploadDataErrors)} - </Panel> + <ErrorBlock errorType={i18n('Upload Data Errors')}> + <div> + <UploadErrorList items={uploadDataErrors}/> + </div> + </ErrorBlock> ); } +} - renderCompilationErrors(compilationErrors) { - return ( - <Panel - header={i18n('Compilation Errors')} - collapsible>{this.parseMapOfErrorMessagesList(compilationErrors)} - </Panel> - ); - } - parseErrorCodeCollection(errors) { - return ( - <ListGroup>{errors.map(error => - <ListGroupItem className='error-code-list-item'> - <div><span>{i18n('Category: ')}</span>{error.category}</div> - <div><span>{i18n('Message: ')}</span>{error.message}</div> - </ListGroupItem> - )}</ListGroup> - ); - } +const ComponentError = ({item}) => { + let i = 0; + return ( + <div> + <div className='component-name-header'>{item.entityName}</div> + {item.errors.map(error => {return(<ErrorMessage key={i++} error={error}/>);})} + </div> + ); +}; - parseMapOfErrorMessagesList(errorMap) { - return ( - <ListGroup> - {Object.keys(errorMap).map(errorStringKey => - <Panel header={errorStringKey} collapsible> - <ListGroup>{errorMap[errorStringKey].map(error => - <ListGroupItem className='error-code-list-item'> - <div><span>{i18n('Level: ')}</span>{error.level}</div> - <div><span>{i18n('Message: ')}</span>{error.message}</div> - </ListGroupItem> - )}</ListGroup> - </Panel> - )} - </ListGroup> - ); +function* entries(obj) { + for (let key of Object.keys(obj)) { + yield {header: key, list: obj[key]}; } +} +const UploadErrorList = ({items}) => { + let generator = entries(items); + + let errors = []; + let i = 0; + for (let item of generator) {errors.push( + <div> + <div className='component-name-header'>{item.header}</div> + {item.list.map(error => <ErrorMessage key={i++} warning={error.level === 'WARNING'} error={error.message}/> )} + </div> + );} + return ( + <div> + {errors} + </div> + ); +}; + +class ErrorBlock extends React.Component { + state = { + collapsed: false + }; - renderQuestionnaireValidationResult(questionnaireValidationResult) { - if (!questionnaireValidationResult.valid) { - return this.parseAndRenderCompositionEntityValidationData(questionnaireValidationResult.validationData); - } - } - - parseAndRenderCompositionEntityValidationData(validationData) { - let {entityType, entityId, errors = [], subEntitiesValidationData = []} = validationData; + render() { + let {errorType, children} = this.props; return ( - <ListGroup> - <Panel header={`${entityType}: ${entityId}`} collapsible> - <ListGroup>{errors.map(error => - <ListGroupItem className='error-code-list-item'> - <div>{error}</div> - </ListGroupItem> - )}</ListGroup> - {subEntitiesValidationData.map(subValidationData => this.parseAndRenderCompositionEntityValidationData(subValidationData))} - </Panel> - </ListGroup> + <div className='error-block'> + <ErrorHeader collapsed={this.state.collapsed} onClick={()=>{this.setState({collapsed: !this.state.collapsed});}} errorType={errorType}/> + <Collapse in={this.state.collapsed}> + {children} + </Collapse> + </div> ); } - - } +const ErrorHeader = ({errorType, collapsed, onClick}) => { + return( + <div onClick={onClick} className='error-block-header'> + <SVGIcon iconClassName={collapsed ? '' : 'right' } name='chevron-down'/> + {errorType} + </div> + ); +}; + +const ErrorMessage = ({error, warning}) => { + return ( + <ListGroupItem className='error-code-list-item'> + <Icon image={warning ? 'warning' : 'error'} label={error}/> + </ListGroupItem> + ); +}; + export default SubmitErrorResponse; diff --git a/openecomp-ui/src/nfvo-components/activity-log/ActivityLog.js b/openecomp-ui/src/nfvo-components/activity-log/ActivityLog.js new file mode 100644 index 0000000000..f7354f96e2 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/activity-log/ActivityLog.js @@ -0,0 +1,27 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import {connect} from 'react-redux'; +import ActivityLogView from './ActivityLogView.jsx'; + +export const mapStateToProps = ({licenseModel: {activityLog}}) => { + + let activities = activityLog; + return { + activities + }; +}; + +export default connect(mapStateToProps, undefined, null, {withRef: true})(ActivityLogView); diff --git a/openecomp-ui/src/nfvo-components/activity-log/ActivityLogActionHelper.js b/openecomp-ui/src/nfvo-components/activity-log/ActivityLogActionHelper.js new file mode 100644 index 0000000000..01a27abbc5 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/activity-log/ActivityLogActionHelper.js @@ -0,0 +1,31 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import RestAPIUtil from 'nfvo-utils/RestAPIUtil.js'; +import Configuration from 'sdc-app/config/Configuration.js'; +import ActivityLogConstants from './ActivityLogConstants.js'; + + +function baseUrl(itemId, versionId) { + const restPrefix = Configuration.get('restPrefix'); + return `${restPrefix}/v1.0/activity-logs/${itemId}/versions/${versionId}`; +} + +export default { + + fetchActivityLog(dispatch, {itemId, versionId}){ + return RestAPIUtil.fetch(baseUrl(itemId, versionId)).then(response => dispatch({type: ActivityLogConstants.ACTIVITY_LOG_UPDATED, response})); + } +}; diff --git a/openecomp-ui/src/nfvo-components/activity-log/ActivityLogConstants.js b/openecomp-ui/src/nfvo-components/activity-log/ActivityLogConstants.js new file mode 100644 index 0000000000..69faf7cbb6 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/activity-log/ActivityLogConstants.js @@ -0,0 +1,23 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import keyMirror from 'nfvo-utils/KeyMirror.js'; + +export default keyMirror({ + + ACTIVITY_LOG_UPDATED: null + +}); + diff --git a/openecomp-ui/src/nfvo-components/activity-log/ActivityLogReducer.js b/openecomp-ui/src/nfvo-components/activity-log/ActivityLogReducer.js new file mode 100644 index 0000000000..fc3dfa1515 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/activity-log/ActivityLogReducer.js @@ -0,0 +1,25 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import ActivityLogConstants from './ActivityLogConstants.js'; + +export default (state = [], action) => { + switch (action.type) { + case ActivityLogConstants.ACTIVITY_LOG_UPDATED: + return [...action.response.results]; + } + + return state; +}; diff --git a/openecomp-ui/src/nfvo-components/activity-log/ActivityLogView.jsx b/openecomp-ui/src/nfvo-components/activity-log/ActivityLogView.jsx new file mode 100644 index 0000000000..6ff3c806a8 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/activity-log/ActivityLogView.jsx @@ -0,0 +1,124 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import React, {Component} from 'react'; +import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger.js'; +import Tooltip from 'react-bootstrap/lib/Tooltip.js'; +import ListEditorView from 'nfvo-components/listEditor/ListEditorView.jsx'; +import SVGIcon from 'nfvo-components/icon/SVGIcon.jsx'; +import i18n from 'nfvo-utils/i18n/i18n.js'; + +function ActivityLogSortableCellHeader({isHeader, data, isDes, onSort}) { + if (isHeader) { + return ( + <span className='date-header' onClick={onSort}> + <span>{data}</span> + <span className={`header-sort-arrow ${isDes ? 'up' : 'down'}`}></span> + </span> + ); + } + return ( + <span className='date-cell'> + <span>{i18n.dateNormal(data, { + year: 'numeric', month: 'numeric', day: 'numeric' + })}</span> + <span>{i18n.dateNormal(data, { + hour: 'numeric', minute: 'numeric', + hour12: true + })}</span> + </span> + ); +} + +function ActivityLogStatus({status, isHeader}) { + if (isHeader) { + return <span>{status}</span>; + } + let {message, success} = status; + return ( + <span> + <span className={`status-icon ${success}`}>{`${success ? i18n('Success') : i18n('Failure')}`}</span> + {success && <SVGIcon name='check-circle'/>} + {!success && <OverlayTrigger placement='bottom' overlay={<Tooltip className='activity-log-message-tooltip' id={'activity-log-message-tooltip'}> + <div className='message-block'>{message}</div> + </Tooltip>}> + <span className='message-further-info-icon'>{'?'}</span> + </OverlayTrigger>} + </span> + ); +} + +export function ActivityListItem({activity, isHeader, isDes, onSort}) { + let {type, timestamp, comment, user, status} = activity; + return ( + <li className={`activity-list-item ${isHeader ? 'header' : ''}`} data-test-id='activity-list-item'> + <div className='table-cell activity-date' data-test-id='activity-date'><ActivityLogSortableCellHeader isHeader={isHeader} data={timestamp} isDes={isDes} onSort={onSort}/></div> + <div className='table-cell activity-action' data-test-id='activity-action'>{type}</div> + <div className='table-cell activity-comment' title={comment} data-test-id='activity-comment'><span>{comment}</span></div> + <div className='table-cell activity-username' data-test-id='activity-username'>{user}</div> + <div className='table-cell activity-status' data-test-id='activity-status'><ActivityLogStatus isHeader={isHeader} status={status}/></div> + </li> + ); +} + +class ActivityLogView extends Component { + + state = { + localFilter: '', + sortDescending: true + }; + + render() { + return ( + <div className='activity-log-view'> + <ListEditorView + title={i18n('Activity Log')} + filterValue={this.state.localFilter} + onFilter={filter => this.setState({localFilter: filter})}> + <ActivityListItem + isHeader={true} + activity={{timestamp: 'Date', type: 'Action', comment: 'Comment', user: 'Username', status: 'Status'}} + isDes={this.state.sortDescending} + onSort={() => this.setState({sortDescending: !this.state.sortDescending})}/> + {this.sortActivities(this.filterActivities(), this.state.sortDescending).map(activity => <ActivityListItem key={activity.id} activity={activity}/>)} + </ListEditorView> + </div> + ); + } + + filterActivities() { + let {activities} = this.props; + let {localFilter} = this.state; + if (localFilter.trim()) { + const filter = new RegExp(escape(localFilter), 'i'); + return activities.filter(({user = '', comment = '', type = ''}) => escape(user).match(filter) || escape(comment).match(filter) || escape(type).match(filter)); + } + else { + return activities; + } + } + + sortActivities(activities) { + if (this.state.sortDescending) { + return activities.sort((a, b) => a.timestamp - b.timestamp); + } + else { + return activities.reverse(); + } + } + +} + +export default ActivityLogView; diff --git a/openecomp-ui/src/nfvo-components/confirmations/ConfirmationModalView.jsx b/openecomp-ui/src/nfvo-components/confirmations/ConfirmationModalView.jsx deleted file mode 100644 index cc971c608c..0000000000 --- a/openecomp-ui/src/nfvo-components/confirmations/ConfirmationModalView.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import Button from 'react-bootstrap/lib/Button.js'; - -import i18n from 'nfvo-utils/i18n/i18n.js'; -import Modal from 'nfvo-components/modal/Modal.jsx'; - -let typeClass = { - 'default': 'primary', - error: 'danger', - warning: 'warning', - success: 'success' -}; - - -class ConfirmationModalView extends React.Component { - - static propTypes = { - show: React.PropTypes.bool, - type: React.PropTypes.oneOf(['default', 'error', 'warning', 'success']), - msg: React.PropTypes.node, - title: React.PropTypes.string, - confirmationDetails: React.PropTypes.object, - confirmationButtonText: React.PropTypes.string, - - }; - - static defaultProps = { - show: false, - type: 'warning', - title: 'Warning', - msg: '', - confirmationButtonText: i18n('Delete') - }; - - render() { - let {title, type, msg, show, confirmationButtonText} = this.props; - - return( - <Modal show={show} className={`notification-modal ${typeClass[type]}`}> - <Modal.Header> - <Modal.Title>{title}</Modal.Title> - </Modal.Header> - <Modal.Body>{msg}</Modal.Body> - <Modal.Footer> - <Button bsStyle={typeClass[type]} onClick={() => this.props.onDeclined(this.props.confirmationDetails)}>{i18n('Cancel')}</Button> - <Button bsStyle={typeClass[type]} onClick={() => this.props.onConfirmed(this.props.confirmationDetails)}>{confirmationButtonText}</Button> - </Modal.Footer> - </Modal> - ); - }; -} - -export default ConfirmationModalView; diff --git a/openecomp-ui/src/nfvo-components/editor/TabulatedEditor.jsx b/openecomp-ui/src/nfvo-components/editor/TabulatedEditor.jsx index 4a106b5ff4..2a0b7d4d2a 100644 --- a/openecomp-ui/src/nfvo-components/editor/TabulatedEditor.jsx +++ b/openecomp-ui/src/nfvo-components/editor/TabulatedEditor.jsx @@ -1,3 +1,18 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ import React from 'react'; import classnames from 'classnames'; @@ -7,10 +22,17 @@ import NavigationSideBar from 'nfvo-components/panel/NavigationSideBar.jsx'; export default class TabulatedEditor extends React.Component { render() { - const {versionControllerProps, navigationBarProps, onToggle, onVersionSwitching, onCreate, onSave, onClose, onVersionControllerAction, onNavigate, children} = this.props; + const {navigationBarProps, onToggle, onVersionSwitching, onCreate, onSave, onClose, onVersionControllerAction, onNavigate, children, meta} = this.props; + let {versionControllerProps} = this.props; const {className = ''} = React.Children.only(children).props; const child = this.prepareChild(); + if(onClose) { + versionControllerProps = { + ...versionControllerProps, + onClose: () => onClose(versionControllerProps) + }; + } return ( <div className='software-product-view'> <div className='software-product-navigation-side-bar'> @@ -19,11 +41,10 @@ export default class TabulatedEditor extends React.Component { <div className='software-product-landing-view-right-side flex-column'> <VersionController {...versionControllerProps} - onVersionSwitching={version => onVersionSwitching(version)} - callVCAction={onVersionControllerAction} + onVersionSwitching={version => onVersionSwitching(version, meta)} + callVCAction={(action, version) => onVersionControllerAction(action, version, meta)} onCreate={onCreate && this.handleCreate} - onSave={onSave && this.handleSave} - onClose={() => onClose(versionControllerProps)}/> + onSave={onSave && this.handleSave}/> <div className={classnames('content-area', `${className}`)}> { child diff --git a/openecomp-ui/src/nfvo-components/grid/GridItem.jsx b/openecomp-ui/src/nfvo-components/grid/GridItem.jsx new file mode 100644 index 0000000000..8819ab78a3 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/grid/GridItem.jsx @@ -0,0 +1,26 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import React from 'react'; + +const GridItem = ({colSpan = 1, children, stretch = false}) => ( + <div className={`grid-col-${colSpan}`}> + <div className={`grid-item${stretch ? '-stretch' : ''}`}> + {children} + </div> + </div> +); + +export default GridItem; diff --git a/openecomp-ui/src/nfvo-components/grid/GridSection.jsx b/openecomp-ui/src/nfvo-components/grid/GridSection.jsx new file mode 100644 index 0000000000..175b3ee082 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/grid/GridSection.jsx @@ -0,0 +1,33 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import React from 'react'; + +const GridSection = ({title, children, titleClassName}) => { + return ( + <div className='grid-section'> + {title && <div className={`section-title ${titleClassName || ''}`}>{title}</div>} + <div className='grid-items'> + {children} + </div> + </div> + ); +}; + +GridSection.propTypes = { + title: React.PropTypes.string, +}; + +export default GridSection; diff --git a/openecomp-ui/src/nfvo-components/icon/Icon.jsx b/openecomp-ui/src/nfvo-components/icon/Icon.jsx new file mode 100644 index 0000000000..125577664b --- /dev/null +++ b/openecomp-ui/src/nfvo-components/icon/Icon.jsx @@ -0,0 +1,45 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import React, { Component, PropTypes } from 'react'; + + +export default class Icon extends Component { + + static propTypes = { + image: PropTypes.string.isRequired, + onClick: PropTypes.func, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + className: PropTypes.string, + iconClassName: PropTypes.string + }; + + static defaultProps = { + label: '', + className: '', + iconClassName: '' + }; + + render() { + let {image, onClick, label, className, iconClassName, ...other} = this.props; + let classes = `icon-component ${className} ${onClick ? 'clickable' : ''}`; + return ( + <div {...other} onClick={onClick} className={classes}> + <span className={`icon ${image} ${iconClassName}`}></span> + <span className='icon-label'>{label}</span> + </div> + ); + } +} diff --git a/openecomp-ui/src/nfvo-components/icon/SVGIcon.jsx b/openecomp-ui/src/nfvo-components/icon/SVGIcon.jsx new file mode 100644 index 0000000000..dd165fb52c --- /dev/null +++ b/openecomp-ui/src/nfvo-components/icon/SVGIcon.jsx @@ -0,0 +1,54 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import React, {PropTypes} from 'react'; +import Configuration from 'sdc-app/config/Configuration.js'; + +export default class SVGIcon extends React.Component { + + static propTypes = { + name: PropTypes.string.isRequired, + onClick: PropTypes.func, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + labelPosition: PropTypes.string, + className: PropTypes.string, + iconClassName: PropTypes.string, + labelClassName: PropTypes.string + }; + + static defaultProps = { + name: '', + label: '', + className: '', + iconClassName: '', + labelClassName: '', + labelPosition: 'bottom' + }; + + render() { + let {name, onClick, label, className, iconClassName, labelClassName, labelPosition, ...other} = this.props; + let classes = `svg-icon-wrapper ${className} ${onClick ? 'clickable' : ''} ${labelPosition}`; + + return ( + <div {...other} onClick={onClick} className={classes}> + <svg className={`svg-icon ${name} ${iconClassName}`} > + <use href={Configuration.get('appContextPath') + '/resources/images/svg/' + this.props.name + '.svg#' + this.props.name + '_icon' } + xlinkHref={Configuration.get('appContextPath') + '/resources/images/svg/' + this.props.name + '.svg#' + this.props.name + '_icon' } /> + </svg> + {label && <span className={`svg-icon-label ${labelClassName}`}>{label}</span>} + </div> + ); + } +} diff --git a/openecomp-ui/src/nfvo-components/icon/SVGIcon.stories.js b/openecomp-ui/src/nfvo-components/icon/SVGIcon.stories.js new file mode 100644 index 0000000000..6675670cea --- /dev/null +++ b/openecomp-ui/src/nfvo-components/icon/SVGIcon.stories.js @@ -0,0 +1,50 @@ +import React from 'react'; +import {storiesOf, action} from '@kadira/storybook'; +import {select, text, withKnobs} from '@kadira/storybook-addon-knobs'; +import SVGIcon from './SVGIcon.jsx'; + +const stories = storiesOf('SVGIcon', module); + +const iconNames = ['locked', + 'pencil', + 'plus-circle', + 'plus', + 'search', + 'sliders', + 'trash-o', + 'unlocked', + 'vendor', + 'version-controller-lock-closed', + 'version-controller-lock-open', + 'version-controller-revert', + 'version-controller-save', + 'version-controller-submit', + 'vlm', + 'vsp' ]; + +function colorChanger() { + return {fill: text('Color', '')}; +} + +function iconName() { + return select('Icon name' , iconNames, iconNames[0]); +} + +stories.addDecorator(withKnobs); + +stories + .add('icon', () => { + return ( + <SVGIcon name={iconName()} style={colorChanger()}/> + ); + }) + .add('icon with label', () => { + return ( + <SVGIcon name={iconName()} label={iconName()} style={colorChanger()}/> + ); + }) + .add('locked clickable', () => { + return ( + <SVGIcon name={iconName()} onClick={action('clicked')} style={colorChanger()}/> + ); + });
\ No newline at end of file diff --git a/openecomp-ui/src/nfvo-components/input/ExpandableInput.jsx b/openecomp-ui/src/nfvo-components/input/ExpandableInput.jsx index 3ac3fcad28..e2ee40fcd2 100644 --- a/openecomp-ui/src/nfvo-components/input/ExpandableInput.jsx +++ b/openecomp-ui/src/nfvo-components/input/ExpandableInput.jsx @@ -1,77 +1,115 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ import React from 'react'; -import FontAwesome from 'react-fontawesome'; -import classnames from 'classnames'; -import Input from 'react-bootstrap/lib/Input'; +import ReactDOM from 'react-dom'; +import SVGIcon from 'nfvo-components/icon/SVGIcon.jsx'; +import Input from 'nfvo-components/input/validation/InputWrapper.jsx'; +const ExpandableInputClosed = ({iconType, onClick}) => ( + <SVGIcon className='expandable-input-wrapper closed' name={iconType} onClick={onClick} /> +); -class ExpandableInput extends React.Component { - constructor(props){ - super(props); - this.state = {showInput: false, value: ''}; - this.toggleInput = this.toggleInput.bind(this); - this.handleFocus = this.handleFocus.bind(this); - this.handleInput = this.handleInput.bind(this); - this.handleClose = this.handleClose.bind(this); +class ExpandableInputOpened extends React.Component { + componentDidMount(){ + this.rawDomNode = ReactDOM.findDOMNode(this.searchInputNode.inputWrapper); + this.rawDomNode.focus(); } - toggleInput(){ - if (!this.state.showInput){ - this.searchInputNode.refs.input.focus(); - } else { - this.setState({showInput: false}); + componentWillReceiveProps(newProps){ + if (!newProps.value){ + if (!(document.activeElement === this.rawDomNode)){ + this.props.handleBlur(); + } } } - handleInput(e){ - let {onChange} = this.props; + handleClose(){ + this.props.onChange(''); + this.rawDomNode.focus(); + } - this.setState({value: e.target.value}); - onChange(e); + handleKeyDown(e){ + if (e.key === 'Escape'){ + e.preventDefault(); + if (this.props.value) { + this.handleClose(); + } else { + this.rawDomNode.blur(); + } + }; } - handleClose(){ - this.handleInput({target: {value: ''}}); - this.searchInputNode.refs.input.focus(); + render() { + let {iconType, value, onChange, handleBlur} = this.props; + return ( + <div className='expandable-input-wrapper opened' key='expandable'> + <Input + type='text' + value={value} + ref={(input) => this.searchInputNode = input} + className='expandable-active' + groupClassName='expandable-input-control' + onChange={e => onChange(e)} + onKeyDown={e => this.handleKeyDown(e)} + onBlur={handleBlur}/> + {value && <SVGIcon onClick={() => this.handleClose()} name='close' />} + {!value && <SVGIcon name={iconType} onClick={handleBlur}/>} + </div> + ); } +} + +class ExpandableInput extends React.Component { - handleFocus(){ - if (!this.state.showInput){ - this.setState({showInput: true}); + static propTypes = { + iconType: React.PropTypes.string, + onChange: React.PropTypes.func, + value: React.PropTypes.string + }; + + state = {showInput: false}; + + closeInput(){ + if (!this.props.value) { + this.setState({showInput: false}); } } getValue(){ - return this.state.value; + return this.props.value; } render(){ - let {iconType} = this.props; - - let inputClasses = classnames({ - 'expandable-active': this.state.showInput, - 'expandable-not-active': !this.state.showInput - }); - - let iconClasses = classnames( - 'expandable-icon', - {'expandable-icon-active': this.state.showInput} - ); - + let {iconType, value, onChange = false} = this.props; return ( - <div className='expandable-input-wrapper'> - <Input - type='text' - value={this.state.value} - ref={(input) => this.searchInputNode = input} - className={inputClasses} - groupClassName='expandable-input-control' - onChange={e => this.handleInput(e)} - onFocus={this.handleFocus}/> - {this.state.showInput && this.state.value && <FontAwesome onClick={this.handleClose} name='close' className='expandable-close-button'/>} - {!this.state.value && <FontAwesome onClick={this.toggleInput} name={iconType} className={iconClasses}/>} + <div className='expandable-input-top'> + {this.state.showInput && + <ExpandableInputOpened + key='open' + iconType={iconType} + onChange={onChange} + value={value} + handleKeyDown={(e) => this.handleKeyDown(e)} + handleBlur={() => this.closeInput()}/> + } + {!this.state.showInput && <ExpandableInputClosed key='closed' iconType={iconType} onClick={() => this.setState({showInput: true})} />} </div> - ); + ); } } + export default ExpandableInput; diff --git a/openecomp-ui/src/nfvo-components/input/SelectInput.jsx b/openecomp-ui/src/nfvo-components/input/SelectInput.jsx index 1036ac41c3..03c727379e 100644 --- a/openecomp-ui/src/nfvo-components/input/SelectInput.jsx +++ b/openecomp-ui/src/nfvo-components/input/SelectInput.jsx @@ -1,3 +1,18 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ /** * The HTML structure here is aligned with bootstrap HTML structure for form elements. * In this way we have proper styling and it is aligned with other form elements on screen. @@ -20,8 +35,9 @@ class SelectInput extends Component { render() { let {label, value, ...other} = this.props; + const dataTestId = this.props['data-test-id'] ? {'data-test-id': this.props['data-test-id']} : {}; return ( - <div className='validation-input-wrapper dropdown-multi-select'> + <div {...dataTestId} className='validation-input-wrapper dropdown-multi-select'> <div className='form-group'> {label && <label className='control-label'>{label}</label>} <Select ref='_myInput' onChange={value => this.onSelectChanged(value)} {...other} value={value} /> diff --git a/openecomp-ui/src/nfvo-components/input/ToggleInput.jsx b/openecomp-ui/src/nfvo-components/input/ToggleInput.jsx index 873d3ded65..7bbafa3696 100644 --- a/openecomp-ui/src/nfvo-components/input/ToggleInput.jsx +++ b/openecomp-ui/src/nfvo-components/input/ToggleInput.jsx @@ -1,3 +1,18 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ import React from 'react'; export default diff --git a/openecomp-ui/src/nfvo-components/input/dualListbox/DualListboxView.jsx b/openecomp-ui/src/nfvo-components/input/dualListbox/DualListboxView.jsx index 171bead9bb..c60d6f777e 100644 --- a/openecomp-ui/src/nfvo-components/input/dualListbox/DualListboxView.jsx +++ b/openecomp-ui/src/nfvo-components/input/dualListbox/DualListboxView.jsx @@ -1,6 +1,21 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ import React from 'react'; -import FontAwesome from 'react-fontawesome'; -import Input from 'react-bootstrap/lib/Input.js'; +import SVGIcon from 'nfvo-components/icon/SVGIcon.jsx'; +import Input from 'nfvo-components/input/validation/InputWrapper.jsx'; class DualListboxView extends React.Component { @@ -30,37 +45,32 @@ class DualListboxView extends React.Component { state = { availableListFilter: '', - selectedValuesListFilter: '' - }; - - static contextTypes = { - isReadOnlyMode: React.PropTypes.bool + selectedValuesListFilter: '', + selectedValues: [] }; render() { - let {availableList, selectedValuesList, filterTitle} = this.props; + let {availableList, selectedValuesList, filterTitle, isReadOnlyMode} = this.props; let {availableListFilter, selectedValuesListFilter} = this.state; - let isReadOnlyMode = this.context.isReadOnlyMode; let unselectedList = availableList.filter(availableItem => !selectedValuesList.find(value => value === availableItem.id)); let selectedList = availableList.filter(availableItem => selectedValuesList.find(value => value === availableItem.id)); selectedList = selectedList.sort((a, b) => selectedValuesList.indexOf(a.id) - selectedValuesList.indexOf(b.id)); - return ( <div className='dual-list-box'> {this.renderListbox(filterTitle.left, unselectedList, { value: availableListFilter, ref: 'availableListFilter', disabled: isReadOnlyMode, - onChange: () => this.setState({availableListFilter: this.refs.availableListFilter.getValue()}) - }, {ref: 'availableValues', disabled: isReadOnlyMode})} + onChange: (value) => this.setState({availableListFilter: value}) + }, {ref: 'availableValues', disabled: isReadOnlyMode, testId: 'available',})} {this.renderOperationsBar(isReadOnlyMode)} {this.renderListbox(filterTitle.right, selectedList, { value: selectedValuesListFilter, ref: 'selectedValuesListFilter', disabled: isReadOnlyMode, - onChange: () => this.setState({selectedValuesListFilter: this.refs.selectedValuesListFilter.getValue()}) - }, {ref: 'selectedValues', disabled: isReadOnlyMode})} + onChange: (value) => this.setState({selectedValuesListFilter: value}) + }, {ref: 'selectedValues', disabled: isReadOnlyMode, testId: 'selected'})} </div> ); } @@ -69,21 +79,25 @@ class DualListboxView extends React.Component { let regExFilter = new RegExp(escape(filterProps.value), 'i'); let matchedItems = list.filter(item => item.name.match(regExFilter)); let unMatchedItems = list.filter(item => !item.name.match(regExFilter)); - - return ( <div className='dual-search-multi-select-section'> <p>{filterTitle}</p> <div className='dual-text-box-search search-wrapper'> - <Input name='search-input-control' type='text' groupClassName='search-input-control' {...filterProps}/> - <FontAwesome name='search' className='search-icon'/> + <Input data-test-id={`${props.testId}-search-input`} + name='search-input-control' type='text' + groupClassName='search-input-control' + {...filterProps}/> + <SVGIcon name='search' className='search-icon'/> </div> <Input multiple + onChange={(event) => this.onSelectItems(event.target.selectedOptions)} groupClassName='dual-list-box-multi-select' type='select' name='dual-list-box-multi-select' - {...props}> + data-test-id={`${props.testId}-select-input`} + disabled={props.disabled} + ref={props.ref}> {matchedItems.map(item => this.renderOption(item.id, item.name))} {matchedItems.length && unMatchedItems.length && <option style={{pointerEvents: 'none'}}>--------------------</option>} {unMatchedItems.map(item => this.renderOption(item.id, item.name))} @@ -92,6 +106,11 @@ class DualListboxView extends React.Component { ); } + onSelectItems(selectedOptions) { + let selectedValues = Object.keys(selectedOptions).map((k) => selectedOptions[k].value); + this.setState({selectedValues}); + } + renderOption(value, name) { return (<option className='dual-list-box-multi-select-text' key={value} value={value}>{name}</option>); } @@ -107,17 +126,19 @@ class DualListboxView extends React.Component { ); } - renderOperationBarButton(onClick, fontAwesomeIconName){ - return (<div className='dual-list-option' onClick={onClick}><FontAwesome name={fontAwesomeIconName}/></div>); + renderOperationBarButton(onClick, iconName){ + return (<div className='dual-list-option' data-test-id={`operation-icon-${iconName}`} onClick={onClick}><SVGIcon name={iconName}/></div>); } addToSelectedList() { - this.props.onChange(this.props.selectedValuesList.concat(this.refs.availableValues.getValue())); + this.props.onChange(this.props.selectedValuesList.concat(this.state.selectedValues)); + this.setState({selectedValues: []}); } removeFromSelectedList() { - const selectedValues = this.refs.selectedValues.getValue(); + const selectedValues = this.state.selectedValues; this.props.onChange(this.props.selectedValuesList.filter(value => !selectedValues.find(selectedValue => selectedValue === value))); + this.setState({selectedValues: []}); } addAllToSelectedList() { diff --git a/openecomp-ui/src/nfvo-components/input/inputOptions/InputOptions.jsx b/openecomp-ui/src/nfvo-components/input/inputOptions/InputOptions.jsx index 5daaffea41..e8aadc4357 100644 --- a/openecomp-ui/src/nfvo-components/input/inputOptions/InputOptions.jsx +++ b/openecomp-ui/src/nfvo-components/input/inputOptions/InputOptions.jsx @@ -1,3 +1,18 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ import React from 'react'; import i18n from 'nfvo-utils/i18n/i18n.js'; import classNames from 'classnames'; @@ -13,15 +28,21 @@ class InputOptions extends React.Component { title: React.PropTypes.string })), isEnabledOther: React.PropTypes.bool, - title: React.PropTypes.string, + label: React.PropTypes.string, selectedValue: React.PropTypes.string, - multiSelectedEnum: React.PropTypes.array, + multiSelectedEnum: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.array + ]), selectedEnum: React.PropTypes.string, otherValue: React.PropTypes.string, onEnumChange: React.PropTypes.func, onOtherChange: React.PropTypes.func, + onBlur: React.PropTypes.func, isRequired: React.PropTypes.bool, - isMultiSelect: React.PropTypes.bool + isMultiSelect: React.PropTypes.bool, + hasError: React.PropTypes.bool, + disabled: React.PropTypes.bool }; @@ -41,7 +62,7 @@ class InputOptions extends React.Component { render() { let {label, isRequired, values, otherValue, onOtherChange, isMultiSelect, onBlur, multiSelectedEnum, selectedEnum, hasError, validations, children} = this.props; - + const dataTestId = this.props['data-test-id'] ? {'data-test-id': this.props['data-test-id']} : {}; let currentMultiSelectedEnum = []; let currentSelectedEnum = ''; let {otherInputDisabled} = this.state; @@ -54,14 +75,18 @@ class InputOptions extends React.Component { else if(selectedEnum){ currentSelectedEnum = selectedEnum; } + if (!onBlur) { + onBlur = () => {}; + } let isReadOnlyMode = this.context.isReadOnlyMode; return( - <div className={classNames('form-group', {'required' : validations.required , 'has-error' : hasError})}> + <div className={classNames('form-group', {'required' : (validations && validations.required) || isRequired, 'has-error' : hasError})}> {label && <label className='control-label'>{label}</label>} {isMultiSelect && otherInputDisabled ? <Select + {...dataTestId} ref='_myInput' value={currentMultiSelectedEnum} className='options-input' @@ -74,18 +99,18 @@ class InputOptions extends React.Component { multi/> : <div className={classNames('input-options',{'has-error' : hasError})}> <select + {...dataTestId} ref={'_myInput'} label={label} className='form-control input-options-select' value={currentSelectedEnum} - style={{'width' : otherInputDisabled ? '100%' : '95px'}} + style={{'width' : otherInputDisabled ? '100%' : '100px'}} onBlur={() => onBlur()} disabled={isReadOnlyMode || Boolean(this.props.disabled)} onChange={ value => this.enumChanged(value)} type='select'> - {values && values.length && values.map(val => this.renderOptions(val))} + {children || (values && values.length && values.map((val, index) => this.renderOptions(val, index)))} {onOtherChange && <option key='other' value={other.OTHER}>{i18n(other.OTHER)}</option>} - {children} </select> {!otherInputDisabled && <div className='input-options-separator'/>} @@ -104,9 +129,9 @@ class InputOptions extends React.Component { ); } - renderOptions(val){ - return( - <option key={val.enum} value={val.enum}>{val.title}</option> + renderOptions(val, index){ + return ( + <option key={index} value={val.enum}>{val.title}</option> ); } @@ -154,9 +179,9 @@ class InputOptions extends React.Component { enumChanged() { let enumValue = this.refs._myInput.value; - let {onEnumChange, isMultiSelect, onChange} = this.props; + let {onEnumChange, onOtherChange, isMultiSelect, onChange} = this.props; this.setState({ - otherInputDisabled: enumValue !== other.OTHER + otherInputDisabled: !Boolean(onOtherChange) || enumValue !== other.OTHER }); let value = isMultiSelect ? [enumValue] : enumValue; @@ -169,7 +194,7 @@ class InputOptions extends React.Component { } multiSelectEnumChanged(enumValue) { - let {onEnumChange} = this.props; + let {onEnumChange, onOtherChange} = this.props; let selectedValues = enumValue.map(enumVal => { return enumVal.value; }); @@ -182,7 +207,7 @@ class InputOptions extends React.Component { } this.setState({ - otherInputDisabled: !selectedValues.includes(i18n(other.OTHER)) + otherInputDisabled: !Boolean(onOtherChange) || !selectedValues.includes(i18n(other.OTHER)) }); onEnumChange(selectedValues); } diff --git a/openecomp-ui/src/nfvo-components/input/validation/Form.jsx b/openecomp-ui/src/nfvo-components/input/validation/Form.jsx new file mode 100644 index 0000000000..47922f86a0 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/input/validation/Form.jsx @@ -0,0 +1,114 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ + +import React from 'react'; +import ValidationButtons from './ValidationButtons.jsx'; + +class Form extends React.Component { + + static defaultProps = { + hasButtons : true, + onSubmit : null, + onReset : null, + labledButtons: true, + onValidChange : null, + isValid: true + }; + + static propTypes = { + isValid : React.PropTypes.bool, + formReady : React.PropTypes.bool, + isReadOnlyMode : React.PropTypes.bool, + hasButtons : React.PropTypes.bool, + onSubmit : React.PropTypes.func, + onReset : React.PropTypes.func, + labledButtons: React.PropTypes.bool, + onValidChange : React.PropTypes.func, + onValidityChanged: React.PropTypes.func, + onValidateForm: React.PropTypes.func + }; + + constructor(props) { + super(props); + } + + + render() { + // eslint-disable-next-line no-unused-vars + let {isValid, formReady, onValidateForm, isReadOnlyMode, hasButtons, onSubmit, labledButtons, onValidChange, onValidityChanged, onDataChanged, children, ...formProps} = this.props; + return ( + <form {...formProps} ref={(form) => this.form = form} onSubmit={event => this.handleFormValidation(event)}> + <div className='validation-form-content'> + <fieldset disabled={isReadOnlyMode}> + {children} + </fieldset> + </div> + {hasButtons && <ValidationButtons labledButtons={labledButtons} ref={(buttons) => this.buttons = buttons} isReadOnlyMode={isReadOnlyMode}/>} + </form> + ); + } + + handleFormValidation(event) { + event.preventDefault(); + if (this.props.onValidateForm && !this.props.formReady){ + return this.props.onValidateForm(); + } else { + return this.handleFormSubmit(event); + } + } + handleFormSubmit(event) { + if (event) { + event.preventDefault(); + } + if(this.props.onSubmit) { + return this.props.onSubmit(event); + } + } + + componentDidMount() { + if (this.props.hasButtons) { + this.buttons.setState({isValid: this.props.isValid}); + } + } + + + + componentDidUpdate(prevProps) { + // 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.buttons.setState({isValid: this.props.isValid}); + } + // callback in case form is part of bigger picture in view + if (this.props.onValidChange) { + this.props.onValidChange(this.props.isValid); + } + + // TODO - maybe this has to be part of componentWillUpdate + if(this.props.onValidityChanged) { + this.props.onValidityChanged(this.props.isValid); + } + } + if (this.props.formReady) { // if form validation succeeded -> continue with submit + this.handleFormSubmit(); + } + } + +} + + +export default Form; diff --git a/openecomp-ui/src/nfvo-components/input/validation/Input.jsx b/openecomp-ui/src/nfvo-components/input/validation/Input.jsx new file mode 100644 index 0000000000..59c35d7993 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/input/validation/Input.jsx @@ -0,0 +1,180 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import classNames from 'classnames'; +import Checkbox from 'react-bootstrap/lib/Checkbox.js'; +import Radio from 'react-bootstrap/lib/Radio.js'; +import FormGroup from 'react-bootstrap/lib/FormGroup.js'; +import FormControl from 'react-bootstrap/lib/FormControl.js'; +import Overlay from 'react-bootstrap/lib/Overlay.js'; +import Tooltip from 'react-bootstrap/lib/Tooltip.js'; + +class Input extends React.Component { + + state = { + value: this.props.value, + checked: this.props.checked, + selectedValues: [] + } + + render() { + const {label, isReadOnlyMode, value, onBlur, onKeyDown, type, disabled, checked, name} = this.props; + // eslint-disable-next-line no-unused-vars + const {groupClassName, isValid = true, errorText, isRequired, ...inputProps} = this.props; + let wrapperClassName = (type !== 'radio') ? 'validation-input-wrapper' : 'form-group'; + if (disabled) { + wrapperClassName += ' disabled'; + } + return( + <div className={wrapperClassName}> + <FormGroup className={classNames('form-group', [groupClassName], {'required' : isRequired , 'has-error' : !isValid})} > + {(label && (type !== 'checkbox' && type !== 'radio')) && <label className='control-label'>{label}</label>} + {(type === 'text' || type === 'number') && + <FormControl + bsClass={'form-control input-options-other'} + onChange={(e) => this.onChange(e)} + disabled={isReadOnlyMode || Boolean(disabled)} + onBlur={onBlur} + onKeyDown={onKeyDown} + value={value || ''} + inputRef={(input) => this.input = input} + type={type} + data-test-id={this.props['data-test-id']}/>} + + {type === 'textarea' && + <FormControl + className='form-control input-options-other' + disabled={isReadOnlyMode || Boolean(disabled)} + value={value || ''} + onBlur={onBlur} + onKeyDown={onKeyDown} + componentClass={type} + onChange={(e) => this.onChange(e)} + inputRef={(input) => this.input = input} + data-test-id={this.props['data-test-id']}/>} + + {type === 'checkbox' && + <Checkbox + className={classNames({'required' : isRequired , 'has-error' : !isValid})} + onChange={(e)=>this.onChangeCheckBox(e)} + disabled={isReadOnlyMode || Boolean(disabled)} + checked={value} + data-test-id={this.props['data-test-id']}>{label}</Checkbox>} + + {type === 'radio' && + <Radio name={name} + checked={checked} + disabled={isReadOnlyMode || Boolean(disabled)} + value={value} + onChange={(e)=>this.onChangeRadio(e)} + data-test-id={this.props['data-test-id']}>{label}</Radio>} + {type === 'select' && + <FormControl onClick={ (e) => this.optionSelect(e) } + componentClass={type} + inputRef={(input) => this.input = input} + name={name} {...inputProps} + data-test-id={this.props['data-test-id']}/>} + </FormGroup> + { this.renderErrorOverlay() } + </div> + ); + } + + getValue() { + return this.props.type !== 'select' ? this.state.value : this.state.selectedValues; + } + + getChecked() { + return this.state.checked; + } + + optionSelect(e) { + let selectedValues = []; + if (e.target.value) { + selectedValues.push(e.target.value); + } + this.setState({ + selectedValues + }); + } + + onChange(e) { + const {onChange, type} = this.props; + let value = e.target.value; + if (type === 'number') { + value = Number(value); + } + this.setState({ + value + }); + onChange(value); + } + + onChangeCheckBox(e) { + let {onChange} = this.props; + this.setState({ + checked: e.target.checked + }); + onChange(e.target.checked); + } + + onChangeRadio(e) { + let {onChange} = this.props; + this.setState({ + checked: e.target.checked + }); + onChange(this.state.value); + } + + focus() { + ReactDOM.findDOMNode(this.input).focus(); + } + + renderErrorOverlay() { + let position = 'right'; + const {errorText = '', isValid = true, type, overlayPos} = this.props; + + if (overlayPos) { + position = overlayPos; + } + else if (type === 'text' + || type === 'email' + || type === 'number' + || type === 'password') { + position = 'bottom'; + } + + return ( + <Overlay + show={!isValid} + placement={position} + target={() => { + let target = ReactDOM.findDOMNode(this.input); + return target.offsetParent ? target : undefined; + }} + container={this}> + <Tooltip + id={`error-${errorText.replace(' ', '-')}`} + className='validation-error-message'> + {errorText} + </Tooltip> + </Overlay> + ); + } + +} +export default Input; diff --git a/openecomp-ui/src/nfvo-components/input/validation/InputOptions.jsx b/openecomp-ui/src/nfvo-components/input/validation/InputOptions.jsx new file mode 100644 index 0000000000..6e54254eb0 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/input/validation/InputOptions.jsx @@ -0,0 +1,279 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import i18n from 'nfvo-utils/i18n/i18n.js'; +import classNames from 'classnames'; +import Select from 'nfvo-components/input/SelectInput.jsx'; +import Overlay from 'react-bootstrap/lib/Overlay.js'; +import Tooltip from 'react-bootstrap/lib/Tooltip.js'; + +export const other = {OTHER: 'Other'}; + +class InputOptions extends React.Component { + + static propTypes = { + values: React.PropTypes.arrayOf(React.PropTypes.shape({ + enum: React.PropTypes.string, + title: React.PropTypes.string + })), + isEnabledOther: React.PropTypes.bool, + label: React.PropTypes.string, + selectedValue: React.PropTypes.string, + multiSelectedEnum: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.array + ]), + selectedEnum: React.PropTypes.string, + otherValue: React.PropTypes.string, + overlayPos: React.PropTypes.string, + onEnumChange: React.PropTypes.func, + onOtherChange: React.PropTypes.func, + onBlur: React.PropTypes.func, + isRequired: React.PropTypes.bool, + isMultiSelect: React.PropTypes.bool, + isValid: React.PropTypes.bool, + disabled: React.PropTypes.bool + }; + + state = { + otherInputDisabled: !this.props.otherValue + }; + + oldProps = { + selectedEnum: '', + otherValue: '', + multiSelectedEnum: [] + }; + + render() { + let {label, isRequired, values, otherValue, onOtherChange, isMultiSelect, onBlur, multiSelectedEnum, selectedEnum, isValid, children, isReadOnlyMode} = this.props; + const dataTestId = this.props['data-test-id'] ? {'data-test-id': this.props['data-test-id']} : {}; + let currentMultiSelectedEnum = []; + let currentSelectedEnum = ''; + let {otherInputDisabled} = this.state; + if (isMultiSelect) { + currentMultiSelectedEnum = multiSelectedEnum; + if(!otherInputDisabled) { + currentSelectedEnum = multiSelectedEnum ? multiSelectedEnum.toString() : undefined; + } + } + else if(selectedEnum){ + currentSelectedEnum = selectedEnum; + } + if (!onBlur) { + onBlur = () => {}; + } + + return( + <div className='validation-input-wrapper' > + <div className={classNames('form-group', {'required' : isRequired, 'has-error' : !isValid})} > + {label && <label className='control-label'>{label}</label>} + {isMultiSelect && otherInputDisabled ? + <Select + {...dataTestId} + ref={(input) => this.input = input} + value={currentMultiSelectedEnum} + className='options-input' + clearable={false} + required={isRequired} + disabled={isReadOnlyMode || Boolean(this.props.disabled)} + onBlur={() => onBlur()} + onMultiSelectChanged={value => this.multiSelectEnumChanged(value)} + options={this.renderMultiSelectOptions(values)} + multi/> : + <div className={classNames('input-options',{'has-error' : !isValid})} > + <select + {...dataTestId} + ref={(input) => this.input = input} + label={label} + className='form-control input-options-select' + value={currentSelectedEnum} + style={{'width' : otherInputDisabled ? '100%' : '100px'}} + onBlur={() => onBlur()} + disabled={isReadOnlyMode || Boolean(this.props.disabled)} + onChange={ value => this.enumChanged(value)} + type='select'> + {children || (values && values.length && values.map((val, index) => this.renderOptions(val, index)))} + {onOtherChange && <option key='other' value={other.OTHER}>{i18n(other.OTHER)}</option>} + </select> + + {!otherInputDisabled && <div className='input-options-separator'/>} + <input + className='form-control input-options-other' + placeholder={i18n('other')} + ref={(otherValue) => this.otherValue = otherValue} + style={{'display' : otherInputDisabled ? 'none' : 'block'}} + disabled={isReadOnlyMode || Boolean(this.props.disabled)} + value={otherValue || ''} + onBlur={() => onBlur()} + onChange={() => this.changedOtherInput()}/> + </div> + } + </div> + { this.renderErrorOverlay() } + </div> + ); + } + + renderOptions(val, index){ + return ( + <option key={index} value={val.enum}>{val.title}</option> + ); + } + + + renderMultiSelectOptions(values) { + let {onOtherChange} = this.props; + let optionsList = []; + if (onOtherChange) { + optionsList = values.map(option => { + return { + label: option.title, + value: option.enum, + }; + }).concat([{ + label: i18n(other.OTHER), + value: i18n(other.OTHER), + }]); + } + else { + optionsList = values.map(option => { + return { + label: option.title, + value: option.enum, + }; + }); + } + if (optionsList.length > 0 && optionsList[0].value === '') { + optionsList.shift(); + } + return optionsList; + } + + renderErrorOverlay() { + let position = 'right'; + const {errorText = '', isValid = true, type, overlayPos} = this.props; + + if (overlayPos) { + position = overlayPos; + } + else if (type === 'text' + || type === 'email' + || type === 'number' + || type === 'password') { + position = 'bottom'; + } + + return ( + <Overlay + show={!isValid} + placement={position} + target={() => { + let {otherInputDisabled} = this.state; + let target = otherInputDisabled ? ReactDOM.findDOMNode(this.input) : ReactDOM.findDOMNode(this.otherValue); + return target.offsetParent ? target : undefined; + }} + container={this}> + <Tooltip + id={`error-${errorText.replace(' ', '-')}`} + className='validation-error-message'> + {errorText} + </Tooltip> + </Overlay> + ); + } + + getValue() { + let res = ''; + let {isMultiSelect} = this.props; + let {otherInputDisabled} = this.state; + + if (otherInputDisabled) { + res = isMultiSelect ? this.input.getValue() : this.input.value; + } else { + res = this.otherValue.value; + } + return res; + } + + enumChanged() { + let enumValue = this.input.value; + let {onEnumChange, onOtherChange, isMultiSelect, onChange} = this.props; + this.setState({ + otherInputDisabled: !Boolean(onOtherChange) || enumValue !== other.OTHER + }); + + let value = isMultiSelect ? [enumValue] : enumValue; + if (onEnumChange) { + onEnumChange(value); + } + if (onChange) { + onChange(value); + } + } + + multiSelectEnumChanged(enumValue) { + let {onEnumChange, onOtherChange} = this.props; + let selectedValues = enumValue.map(enumVal => { + return enumVal.value; + }); + + if (this.state.otherInputDisabled === false) { + selectedValues.shift(); + } + else if (selectedValues.includes(i18n(other.OTHER))) { + selectedValues = [i18n(other.OTHER)]; + } + + this.setState({ + otherInputDisabled: !Boolean(onOtherChange) || !selectedValues.includes(i18n(other.OTHER)) + }); + onEnumChange(selectedValues); + } + + changedOtherInput() { + let {onOtherChange} = this.props; + onOtherChange(this.otherValue.value); + } + + componentDidUpdate() { + let {otherValue, selectedEnum, onInputChange, multiSelectedEnum} = this.props; + if (this.oldProps.otherValue !== otherValue + || this.oldProps.selectedEnum !== selectedEnum + || this.oldProps.multiSelectedEnum !== multiSelectedEnum) { + this.oldProps = { + otherValue, + selectedEnum, + multiSelectedEnum + }; + onInputChange(); + } + } + + static getTitleByName(values, name) { + for (let key of Object.keys(values)) { + let option = values[key].find(option => option.enum === name); + if (option) { + return option.title; + } + } + return name; + } + +} + +export default InputOptions; diff --git a/openecomp-ui/src/nfvo-components/input/validation/InputWrapper.jsx b/openecomp-ui/src/nfvo-components/input/validation/InputWrapper.jsx new file mode 100644 index 0000000000..5ca716cc20 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/input/validation/InputWrapper.jsx @@ -0,0 +1,134 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import classNames from 'classnames'; +import Checkbox from 'react-bootstrap/lib/Checkbox.js'; +import Radio from 'react-bootstrap/lib/Radio.js'; +import FormGroup from 'react-bootstrap/lib/FormGroup.js'; +import FormControl from 'react-bootstrap/lib/FormControl.js'; + +class InputWrapper extends React.Component { + + state = { + value: this.props.value, + checked: this.props.checked, + selectedValues: [] + } + + render() { + const {label, hasError, validations = {}, isReadOnlyMode, value, onBlur, onKeyDown, type, disabled, checked, name} = this.props; + const {groupClassName, ...inputProps} = this.props; + return( + <FormGroup className={classNames('form-group', [groupClassName], {'required' : validations.required , 'has-error' : hasError})} > + {(label && (type !== 'checkbox' && type !== 'radio')) && <label className='control-label'>{label}</label>} + {(type === 'text' || type === 'number') && + <FormControl + bsClass={'form-control input-options-other'} + onChange={(e) => this.onChange(e)} + disabled={isReadOnlyMode || Boolean(disabled)} + onBlur={onBlur} + onKeyDown={onKeyDown} + value={value || ''} + ref={(input) => this.inputWrapper = input} + type={type} + data-test-id={this.props['data-test-id']}/>} + + {type === 'textarea' && + <FormControl + className='form-control input-options-other' + disabled={isReadOnlyMode || Boolean(disabled)} + value={value || ''} + onBlur={onBlur} + onKeyDown={onKeyDown} + componentClass={type} + onChange={(e) => this.onChange(e)} + data-test-id={this.props['data-test-id']}/>} + + {type === 'checkbox' && + <Checkbox + className={classNames({'required' : validations.required , 'has-error' : hasError})} + onChange={(e)=>this.onChangeCheckBox(e)} + disabled={isReadOnlyMode || Boolean(disabled)} + checked={value} + data-test-id={this.props['data-test-id']}>{label}</Checkbox>} + + {type === 'radio' && + <Radio name={name} + checked={checked} + disabled={isReadOnlyMode || Boolean(disabled)} + value={value} + onChange={(e)=>this.onChangeRadio(e)} + data-test-id={this.props['data-test-id']}>{label}</Radio>} + {type === 'select' && + <FormControl onClick={ (e) => this.optionSelect(e) } + componentClass={type} + name={name} {...inputProps} + data-test-id={this.props['data-test-id']}/>} + + </FormGroup> + + ); + } + + getValue() { + return this.props.type !== 'select' ? this.state.value : this.state.selectedValues; + } + + getChecked() { + return this.state.checked; + } + + optionSelect(e) { + let selectedValues = []; + if (e.target.value) { + selectedValues.push(e.target.value); + } + this.setState({ + selectedValues + }); + } + + onChange(e) { + let {onChange} = this.props; + this.setState({ + value: e.target.value + }); + onChange(e.target.value); + } + + onChangeCheckBox(e) { + let {onChange} = this.props; + this.setState({ + checked: e.target.checked + }); + onChange(e.target.checked); + } + + onChangeRadio(e) { + let {onChange} = this.props; + this.setState({ + checked: e.target.checked + }); + onChange(this.state.value); + } + + focus() { + ReactDOM.findDOMNode(this.inputWrapper).focus(); + } + +} +export default InputWrapper; diff --git a/openecomp-ui/src/nfvo-components/input/validation/Tabs.jsx b/openecomp-ui/src/nfvo-components/input/validation/Tabs.jsx new file mode 100644 index 0000000000..95144b1468 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/input/validation/Tabs.jsx @@ -0,0 +1,79 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import {default as BTabs} 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 'nfvo-utils/i18n/i18n.js'; + +export default +class Tabs extends React.Component { + + static propTypes = { + children: React.PropTypes.node + }; + + cloneTab(element) { + const {invalidTabs} = this.props; + return React.cloneElement( + element, + { + key: element.props.eventKey, + tabClassName: invalidTabs.indexOf(element.props.eventKey) > -1 ? 'invalid-tab' : 'valid-tab' + } + ); + } + + showTabsError() { + const {invalidTabs} = this.props; + const showError = ((invalidTabs.length === 1 && invalidTabs[0] !== this.props.activeKey) || (invalidTabs.length > 1)); + return showError; + } + + render() { + // eslint-disable-next-line no-unused-vars + let {invalidTabs, ...tabProps} = this.props; + return ( + <div> + <BTabs {...tabProps} ref='tabsList' id='tabsList' > + {this.props.children.map(element => this.cloneTab(element))} + </BTabs> + <Overlay + animation={false} + show={this.showTabsError()} + placement='bottom' + containerPadding={50} + 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={() => { + let target = ReactDOM.findDOMNode(this.refs.tabsList).querySelector('ul > li.invalid-tab:not(.active):nth-of-type(n)'); + return target && target.offsetParent ? target.offsetParent : this; + }}> + <Tooltip + id='error-some-tabs-contain-errors' + className='validation-error-message'> + {i18n('One or more tabs are invalid')} + </Tooltip> + </Overlay> + </div> + ); + } +} diff --git a/openecomp-ui/src/nfvo-components/input/validation/ValidationButtons.jsx b/openecomp-ui/src/nfvo-components/input/validation/ValidationButtons.jsx index a87c8d6f40..ebb1473c04 100644 --- a/openecomp-ui/src/nfvo-components/input/validation/ValidationButtons.jsx +++ b/openecomp-ui/src/nfvo-components/input/validation/ValidationButtons.jsx @@ -1,3 +1,18 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ /** * Holds the buttons for save/reset for forms. * Used by the ValidationForm that changes the state of the buttons according to its own state. @@ -8,7 +23,7 @@ import React from 'react'; import i18n from 'nfvo-utils/i18n/i18n.js'; import Button from 'react-bootstrap/lib/Button.js'; -import FontAwesome from 'react-fontawesome'; +import SVGIcon from 'nfvo-components/icon/SVGIcon.jsx'; class ValidationButtons extends React.Component { @@ -22,8 +37,8 @@ class ValidationButtons extends React.Component { }; 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'/>; + var submitBtn = this.props.labledButtons ? i18n('Save') : <SVGIcon className='check' name='check'/>; + var closeBtn = this.props.labledButtons ? i18n('Cancel') : <SVGIcon className='close' name='close'/>; return ( <div className='validation-buttons'> {!this.props.isReadOnlyMode ? diff --git a/openecomp-ui/src/nfvo-components/input/validation/ValidationForm.jsx b/openecomp-ui/src/nfvo-components/input/validation/ValidationForm.jsx deleted file mode 100644 index 098ccf1fd4..0000000000 --- a/openecomp-ui/src/nfvo-components/input/validation/ValidationForm.jsx +++ /dev/null @@ -1,200 +0,0 @@ -/** - * 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 JSONSchema from 'nfvo-utils/json/JSONSchema.js'; -import JSONPointer from 'nfvo-utils/json/JSONPointer.js'; -import ValidationButtons from './ValidationButtons.jsx'; - -class ValidationForm extends React.Component { - - static childContextTypes = { - validationParent: React.PropTypes.any, - isReadOnlyMode: React.PropTypes.bool, - validationSchema: React.PropTypes.instanceOf(JSONSchema), - validationData: React.PropTypes.object - }; - - static defaultProps = { - hasButtons : true, - onSubmit : null, - onReset : null, - labledButtons: true, - onValidChange : null, - isValid: true - }; - - static propTypes = { - isValid : React.PropTypes.bool, - isReadOnlyMode : React.PropTypes.bool, - hasButtons : React.PropTypes.bool, - onSubmit : React.PropTypes.func, - onReset : React.PropTypes.func, - labledButtons: React.PropTypes.bool, - onValidChange : React.PropTypes.func, - onValidityChanged: React.PropTypes.func, - schema: React.PropTypes.object, - data: React.PropTypes.object - }; - - state = { - isValid: this.props.isValid - }; - - constructor(props) { - super(props); - this.validationComponents = []; - } - - componentWillMount() { - let {schema, data} = this.props; - if (schema) { - this.processSchema(schema, data); - } - } - - componentWillReceiveProps(nextProps) { - let {schema, data} = this.props; - let {schema: nextSchema, data: nextData} = nextProps; - - if (schema !== nextSchema || data !== nextData) { - if (!schema || !nextSchema) { - throw new Error('ValidationForm: dynamically adding/removing schema is not supported'); - } - - if (schema !== nextSchema) { - this.processSchema(nextSchema, nextData); - } else { - this.setState({data: nextData}); - } - } - } - - processSchema(rawSchema, rawData) { - let schema = new JSONSchema(); - schema.setSchema(rawSchema); - let data = schema.processData(rawData); - this.setState({ - schema, - data - }); - } - - render() { - // eslint-disable-next-line no-unused-vars - let {isValid, isReadOnlyMode, hasButtons, onSubmit, labledButtons, onValidChange, onValidityChanged, schema, data, children, ...formProps} = this.props; - return ( - <form {...formProps} onSubmit={event => this.handleFormSubmit(event)}> - <div className='validation-form-content'>{children}</div> - {hasButtons && <ValidationButtons labledButtons={labledButtons} ref='buttons' isReadOnlyMode={isReadOnlyMode}/>} - </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) { - return this.props.onSubmit(event); - } else if(!isFormValid) { - this.setState({isValid: false}); - } - } - - componentWillUpdate(nextProps, nextState) { - if(this.state.isValid !== nextState.isValid && this.props.onValidityChanged) { - this.props.onValidityChanged(nextState.isValid); - } - } - - 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, - validationSchema: this.state.schema, - validationData: this.state.data - }; - } - - - /*** - * 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) { - if (this.state.schema) { - // TODO: register - } else { - this.validationComponents.push(validationComponent); - } - } - - unregister(validationComponent) { - this.childValidStateChanged(validationComponent, true); - this.validationComponents = this.validationComponents.filter(otherValidationComponent => validationComponent !== otherValidationComponent); - } - - onValueChanged(pointer, value, isValid, error) { - this.props.onDataChanged({ - data: JSONPointer.setValue(this.props.data, pointer, value), - isValid, - error - }); - } -} - - -export default ValidationForm; diff --git a/openecomp-ui/src/nfvo-components/input/validation/ValidationInput.jsx b/openecomp-ui/src/nfvo-components/input/validation/ValidationInput.jsx deleted file mode 100644 index 0f14307645..0000000000 --- a/openecomp-ui/src/nfvo-components/input/validation/ValidationInput.jsx +++ /dev/null @@ -1,509 +0,0 @@ -/** - * 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 FormGroup from 'react-bootstrap/lib/FormGroup.js'; -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/isEqual.js'; -import i18n from 'nfvo-utils/i18n/i18n.js'; -import JSONSchema from 'nfvo-utils/json/JSONSchema.js'; -import JSONPointer from 'nfvo-utils/json/JSONPointer.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 => { - if (value === '') { - // to allow empty value which is not zero - return true; - } - return Validator.isNumeric(value); - }, - maxValue: (value, maxValue) => value < maxValue, - minValue: (value, minValue) => value >= minValue, - 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 than: {maxValue}.', {maxValue}), - minValue: (value, minValue) => i18n('Field value should be at least: {minValue}.', {minValue}), - 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, - validationSchema: React.PropTypes.instanceOf(JSONSchema), - validationData: React.PropTypes.object - }; - - static defaultProps = { - onChange: null, - disabled: null, - didUpdateCallback: null, - validations: {}, - value: '' - }; - - static propTypes = { - type: React.PropTypes.string.isRequired, - onChange: React.PropTypes.func, - disabled: React.PropTypes.bool, - didUpdateCallback: React.PropTypes.func, - validations: React.PropTypes.object, - isMultiSelect: React.PropTypes.bool, - onOtherChange: React.PropTypes.func, - pointer: React.PropTypes.string - }; - - - state = { - isValid: true, - style: null, - value: this.props.value, - error: {}, - previousErrorMessage: '', - wasInvalid: false, - validations: this.props.validations, - isMultiSelect: this.props.isMultiSelect - }; - - componentWillMount() { - if (this.context.validationSchema) { - let {validationSchema: schema, validationData: data} = this.context, - {pointer} = this.props; - - if (!schema.exists(pointer)) { - console.error(`Field doesn't exists in the schema ${pointer}`); - } - - let value = JSONPointer.getValue(data, pointer); - if (value === undefined) { - value = schema.getDefault(pointer); - if (value === undefined) { - value = ''; - } - } - this.setState({value}); - - let enums = schema.getEnum(pointer); - if (enums) { - let values = enums.map(value => ({enum: value, title: value, groupName: pointer})), - isMultiSelect = schema.isArray(pointer); - - if (!isMultiSelect && this.props.type !== 'radiogroup') { - values = [{enum: '', title: i18n('Select...')}, ...values]; - } - if (isMultiSelect && Array.isArray(value) && value.length === 0) { - value = ''; - } - - this.setState({ - isMultiSelect, - values, - onEnumChange: value => this.changedInputOptions(value), - value - }); - } - - this.setState({validations: this.extractValidationsFromSchema(schema, pointer, this.props)}); - } - } - - extractValidationsFromSchema(schema, pointer, props) { - /* props are here to get precedence over the scheme definitions */ - let validations = {}; - - if (schema.isRequired(pointer)) { - validations.required = true; - } - - if (schema.isNumber(pointer)) { - validations.numeric = true; - - const maxValue = props.validations.maxValue || schema.getMaxValue(pointer); - if (maxValue !== undefined) { - validations.maxValue = maxValue; - } - - const minValue = props.validations.minValue || schema.getMinValue(pointer); - if (minValue !== undefined) { - validations.minValue = minValue; - } - } - - - if (schema.isString(pointer)) { - - const pattern = schema.getPattern(pointer); - if (pattern) { - validations.pattern = pattern; - } - - const maxLength = schema.getMaxLength(pointer); - if (maxLength !== undefined) { - validations.maxLength = maxLength; - } - - const minLength = schema.getMinLength(pointer); - if (minLength !== undefined) { - validations.minLength = minLength; - } - } - - return validations; - } - - componentWillReceiveProps({value: nextValue, validations: nextValidations, pointer: nextPointer}, nextContext) { - const {validations, value} = this.props; - const validationsChanged = !isEqual(validations, nextValidations); - if (nextContext.validationSchema) { - if (this.props.pointer !== nextPointer || - this.context.validationData !== nextContext.validationData) { - let currentValue = JSONPointer.getValue(this.context.validationData, this.props.pointer), - nextValue = JSONPointer.getValue(nextContext.validationData, nextPointer); - if(nextValue === undefined) { - nextValue = ''; - } - if (this.state.isMultiSelect && Array.isArray(nextValue) && nextValue.length === 0) { - nextValue = ''; - } - if (currentValue !== nextValue) { - this.setState({value: nextValue}); - } - if (validationsChanged) { - this.setState({ - validations: this.extractValidationsFromSchema(nextContext.validationSchema, nextPointer, {validations: nextValidations}) - }); - } - } - } else { - if (validationsChanged) { - this.setState({validations: nextValidations}); - } - if (this.state.wasInvalid && (value !== nextValue || validationsChanged)) { - this.validate(nextValue, nextValidations); - } else if (value !== nextValue) { - this.setState({value: nextValue}); - } - } - } - - shouldTypeBeNumberBySchemeDefinition(pointer) { - return this.context.validationSchema && - this.context.validationSchema.isNumber(pointer); - } - - hasEnum(pointer) { - return this.context.validationSchema && - this.context.validationSchema.getEnum(pointer); - } - - render() { - let {value, isMultiSelect, values, onEnumChange, style, isValid, validations} = this.state; - let {onOtherChange, type, pointer} = this.props; - if (this.shouldTypeBeNumberBySchemeDefinition(pointer) && !this.hasEnum(pointer)) { - type = 'number'; - } - let props = {...this.props}; - - let groupClasses = this.props.groupClassName || ''; - if (validations.required) { - groupClasses += ' required'; - } - let isReadOnlyMode = this.context.isReadOnlyMode; - - if (value === true && (type === 'checkbox' || type === 'radio')) { - props.checked = true; - } - return ( - <div className='validation-input-wrapper'> - { - !isMultiSelect && !onOtherChange && type !== 'select' && type !== 'radiogroup' - && <Input - {...props} - type={type} - groupClassName={groupClasses} - ref={'_myInput'} - value={value} - disabled={isReadOnlyMode || Boolean(this.props.disabled)} - bsStyle={style} - onChange={() => this.changedInput()} - onBlur={() => this.blurInput()}> - {this.props.children} - </Input> - } - { - type === 'radiogroup' - && <FormGroup> - { - values.map(val => - <Input disabled={isReadOnlyMode || Boolean(this.props.disabled)} - inline={true} - ref={'_myInput' + (typeof val.enum === 'string' ? val.enum.replace(/\W/g, '_') : val.enum)} - value={val.enum} checked={value === val.enum} - type='radio' label={val.title} - name={val.groupName} - onChange={() => this.changedInput()}/> - ) - } - </FormGroup> - } - { - (isMultiSelect || onOtherChange || type === 'select') - && <InputOptions - onInputChange={() => this.changedInput()} - onBlur={() => this.blurInput()} - hasError={!isValid} - ref={'_myInput'} - isMultiSelect={isMultiSelect} - values={values} - onEnumChange={onEnumChange} - selectedEnum={value} - multiSelectedEnum={value} - {...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); - } - } - - isNumberInputElement() { - return this.props.type === 'number' || this.refs._myInput.props.type === 'number'; - } - - /*** - * Adding same method as the actual input component - * @returns {*} - */ - getValue() { - if (this.props.type === 'checkbox') { - return this.refs._myInput.getChecked(); - } - if (this.props.type === 'radiogroup') { - for (let key in this.refs) { // finding the value of the radio button that was checked - if (this.refs[key].getChecked()) { - return this.refs[key].getValue(); - } - } - } - if (this.isNumberInputElement()) { - return Number(this.refs._myInput.getValue()); - } - - 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); - } - if (this.context.validationSchema) { - let value = this.getValue(); - if (this.state.isMultiSelect && value === '') { - value = []; - } - if (this.shouldTypeBeNumberBySchemeDefinition(this.props.pointer)) { - value = Number(value); - } - this.context.validationParent.onValueChanged(this.props.pointer, value, isValid, error); - } - } - - changedInputOptions(value) { - this.context.validationParent.onValueChanged(this.props.pointer, value, true); - } - - 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.state.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/openecomp-ui/src/nfvo-components/input/validation/ValidationTab.jsx b/openecomp-ui/src/nfvo-components/input/validation/ValidationTab.jsx deleted file mode 100644 index 6036518288..0000000000 --- a/openecomp-ui/src/nfvo-components/input/validation/ValidationTab.jsx +++ /dev/null @@ -1,107 +0,0 @@ -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/openecomp-ui/src/nfvo-components/input/validation/ValidationTabs.jsx b/openecomp-ui/src/nfvo-components/input/validation/ValidationTabs.jsx deleted file mode 100644 index 6eda4b9827..0000000000 --- a/openecomp-ui/src/nfvo-components/input/validation/ValidationTabs.jsx +++ /dev/null @@ -1,72 +0,0 @@ -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 'nfvo-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> - ); - } -} diff --git a/openecomp-ui/src/nfvo-components/listEditor/ListEditorItemView.jsx b/openecomp-ui/src/nfvo-components/listEditor/ListEditorItemView.jsx index e8d0fc2536..f6c906b56b 100644 --- a/openecomp-ui/src/nfvo-components/listEditor/ListEditorItemView.jsx +++ b/openecomp-ui/src/nfvo-components/listEditor/ListEditorItemView.jsx @@ -1,7 +1,24 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ import React from 'react'; -import FontAwesome from 'react-fontawesome'; +import classnames from 'classnames'; +import i18n from 'nfvo-utils/i18n/i18n.js'; +import SVGIcon from 'nfvo-components/icon/SVGIcon.jsx'; import store from 'sdc-app/AppStore.js'; -import NotificationConstants from 'nfvo-components/notifications/NotificationConstants.js'; +import {actionTypes as modalActionTypes} from 'nfvo-components/modal/GlobalModalConstants.js'; class ListEditorItem extends React.Component { static propTypes = { @@ -16,14 +33,14 @@ class ListEditorItem extends React.Component { let {onDelete, onSelect, onEdit, children, isReadOnlyMode} = this.props; let isAbilityToDelete = isReadOnlyMode === undefined ? true : !isReadOnlyMode; return ( - <div className='list-editor-item-view'> + <div className={classnames('list-editor-item-view', {'selectable': Boolean(onSelect)})} data-test-id='list-editor-item'> <div className='list-editor-item-view-content' onClick={onSelect}> {children} </div> - <div className='list-editor-item-view-controller'> - {onEdit && <FontAwesome name='sliders' onClick={() => this.onClickedItem(onEdit)}/>} - {onDelete && isAbilityToDelete && <FontAwesome name='trash-o' onClick={() => this.onClickedItem(onDelete)}/>} - </div> + {(onEdit || onDelete) && <div className='list-editor-item-view-controller'> + {onEdit && <SVGIcon name='sliders' onClick={() => this.onClickedItem(onEdit)}/>} + {onDelete && isAbilityToDelete && <SVGIcon name='trash-o' onClick={() => this.onClickedItem(onDelete)}/>} + </div>} </div> ); } @@ -33,8 +50,11 @@ class ListEditorItem extends React.Component { let {isCheckedOut} = this.props; if (isCheckedOut === false) { store.dispatch({ - type: NotificationConstants.NOTIFY_ERROR, - data: {title: 'Error', msg: 'This item is checkedin/submitted, Click Check Out to continue'} + type: modalActionTypes.GLOBAL_MODAL_WARNING, + data: { + title: i18n('Error'), + msg: i18n('This item is checkedin/submitted, Click Check Out to continue') + } }); } else { diff --git a/openecomp-ui/src/nfvo-components/listEditor/ListEditorItemViewField.jsx b/openecomp-ui/src/nfvo-components/listEditor/ListEditorItemViewField.jsx new file mode 100644 index 0000000000..839f9a504a --- /dev/null +++ b/openecomp-ui/src/nfvo-components/listEditor/ListEditorItemViewField.jsx @@ -0,0 +1,24 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import React from 'react'; + +export const ListEditorItemViewField = ({children}) => ( + <div className='list-editor-item-view-field'> + {children} + </div> +); + +export default ListEditorItemViewField; diff --git a/openecomp-ui/src/nfvo-components/listEditor/ListEditorView.jsx b/openecomp-ui/src/nfvo-components/listEditor/ListEditorView.jsx index 1ee91f31f6..cc805e9ada 100644 --- a/openecomp-ui/src/nfvo-components/listEditor/ListEditorView.jsx +++ b/openecomp-ui/src/nfvo-components/listEditor/ListEditorView.jsx @@ -1,12 +1,63 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ import React from 'react'; -import FontAwesome from 'react-fontawesome'; -import Input from 'react-bootstrap/lib/Input.js'; +import classnames from 'classnames'; +import ExpandableInput from 'nfvo-components/input/ExpandableInput.jsx'; +const ListEditorHeader = ({onAdd, isReadOnlyMode, title, plusButtonTitle}) => { + return ( + <div className='list-editor-view-header'> + {title && <div className='list-editor-view-title'>{title}</div>} + <div className={`list-editor-view-add-controller${isReadOnlyMode ? ' disabled' : ''}`}> + { onAdd && + <div className='list-editor-view-add-title' data-test-id='add-button' onClick={onAdd}> + <span>{`+ ${plusButtonTitle}`}</span> + </div> + } + </div> + </div> + ); +}; + +const ListEditorScroller = ({children, twoColumns}) => { + return ( + <div className='list-editor-view-list-scroller'> + <div className={classnames('list-editor-view-list', {'two-columns': twoColumns})}> + {children} + </div> + </div> + ); +}; + +const FilterWrapper = ({onFilter, filterValue}) => { + return ( + <div className='expandble-search-wrapper'> + <ExpandableInput + onChange={onFilter} + iconType='search' + value={filterValue}/> + </div> + ); +}; class ListEditorView extends React.Component { static defaultProps = { - className: '' + className: '', + twoColumns: false }; static propTypes = { @@ -17,45 +68,17 @@ class ListEditorView extends React.Component { onFilter: React.PropTypes.func, className: React.PropTypes.string, isReadOnlyMode: React.PropTypes.bool, - placeholder: React.PropTypes.string + placeholder: React.PropTypes.string, + twoColumns: React.PropTypes.bool }; render() { - let {title, plusButtonTitle, onAdd, children, filterValue, onFilter, className, placeholder, isReadOnlyMode} = this.props; + let {title, plusButtonTitle, onAdd, children, onFilter, className, isReadOnlyMode, twoColumns, filterValue} = this.props; return ( - <div className={`list-editor-view ${className}`}> - {title && onAdd && <div className='list-editor-view-title'>{title}</div>} - <div className='list-editor-view-actions'> - {title && !onAdd && <div className='list-editor-view-title-inline'>{title}</div>} - <div className={`list-editor-view-add-controller${isReadOnlyMode ? ' disabled' : ''}`} > - { onAdd && - <div onClick={onAdd}> - <span className='plus-icon-button pull-left'/> - <span>{plusButtonTitle}</span> - </div> - } - </div> - - { - onFilter && - <div className='list-editor-view-search search-wrapper'> - <Input - ref='filter' - type='text' - value={filterValue} - name='list-editor-view-search' - placeholder={placeholder} - groupClassName='search-input-control' - onChange={() => onFilter(this.refs.filter.getValue())}/> - <FontAwesome name='filter' className='filter-icon'/> - </div> - } - </div> - <div className='list-editor-view-list-scroller'> - <div className='list-editor-view-list'> - {children} - </div> - </div> + <div className={classnames('list-editor-view', className)}> + <ListEditorHeader onAdd={onAdd} isReadOnlyMode={isReadOnlyMode} plusButtonTitle={plusButtonTitle} title={title}/> + {onFilter && (children.length || filterValue) && <FilterWrapper onFilter={onFilter} filterValue={filterValue}/>} + <ListEditorScroller children={children} twoColumns={twoColumns}/> </div> ); } diff --git a/openecomp-ui/src/nfvo-components/listEditor/listEditor.stories.js b/openecomp-ui/src/nfvo-components/listEditor/listEditor.stories.js new file mode 100644 index 0000000000..276b05e270 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/listEditor/listEditor.stories.js @@ -0,0 +1,60 @@ +import React from 'react'; +import {storiesOf, action} from '@kadira/storybook'; +import ListEditorView from './ListEditorView.jsx'; +import ListEditorItemView from './ListEditorItemView.jsx'; +import ListEditorItemViewField from './ListEditorItemViewField.jsx'; +import {text, number} from '@kadira/storybook-addon-knobs'; +import {withKnobs} from '@kadira/storybook-addon-knobs'; + +function makeChildren({onEdit = false, onDelete = false} = {}) { + return ( + [...Array(number('Items', 2)).keys()].map(index => ( + <ListEditorItemView + key={index} + onEdit={onEdit ? onEdit : undefined} + onDelete={onDelete ? onDelete : undefined}> + <ListEditorItemViewField> + <div>{text('field 1', 'Lorum Ipsum')}</div> + </ListEditorItemViewField> + <ListEditorItemViewField> + <div>{text('field 2', 'Lorum Ipsum')}</div> + </ListEditorItemViewField> + </ListEditorItemView>) + ) + ); +} + +const stories = storiesOf('ListEditor', module); +stories.addDecorator(withKnobs); + +stories + .add('regular', () => ( + <ListEditorView title='List Editor'> + {makeChildren()} + </ListEditorView> + )) + .add('two columns', () => ( + <ListEditorView title='List Editor' twoColumns> + {makeChildren()} + </ListEditorView> + )) + .add('with add', () => ( + <ListEditorView title='List Editor' onAdd={action('onAdd')} plusButtonTitle='Add' twoColumns> + {makeChildren()} + </ListEditorView> + )) + .add('with delete', () => ( + <ListEditorView title='List Editor' onAdd={action('onAdd')} plusButtonTitle='Add' twoColumns> + {makeChildren({onDelete: action('onDelete')})} + </ListEditorView> + )) + .add('with edit', () => ( + <ListEditorView title='List Editor' onAdd={action('onAdd')} plusButtonTitle='Add' twoColumns> + {makeChildren({onEdit: action('onEdit')})} + </ListEditorView> + )) + .add('with edit and delete', () => ( + <ListEditorView title='List Editor' onAdd={action('onAdd')} plusButtonTitle='Add' twoColumns> + {makeChildren({onDelete: action('onDelete'), onEdit: action('onEdit')})} + </ListEditorView> + )); diff --git a/openecomp-ui/src/nfvo-components/loader/Loader.jsx b/openecomp-ui/src/nfvo-components/loader/Loader.jsx index cc1ffdb2b3..675b04c8ea 100644 --- a/openecomp-ui/src/nfvo-components/loader/Loader.jsx +++ b/openecomp-ui/src/nfvo-components/loader/Loader.jsx @@ -1,3 +1,18 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ import React from 'react'; import {connect} from 'react-redux'; diff --git a/openecomp-ui/src/nfvo-components/loader/LoaderConstants.js b/openecomp-ui/src/nfvo-components/loader/LoaderConstants.js index e8e4953eb9..7c0c0e2b08 100644 --- a/openecomp-ui/src/nfvo-components/loader/LoaderConstants.js +++ b/openecomp-ui/src/nfvo-components/loader/LoaderConstants.js @@ -1,23 +1,18 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ +/*! * Copyright (C) 2017 AT&T Intellectual Property. 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 - * + * + * 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========================================================= + * 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. */ - import keyMirror from 'nfvo-utils/KeyMirror.js'; export const actionTypes = keyMirror({ diff --git a/openecomp-ui/src/nfvo-components/loader/LoaderReducer.js b/openecomp-ui/src/nfvo-components/loader/LoaderReducer.js index 582eff330d..2eff70a617 100644 --- a/openecomp-ui/src/nfvo-components/loader/LoaderReducer.js +++ b/openecomp-ui/src/nfvo-components/loader/LoaderReducer.js @@ -1,23 +1,18 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ +/*! * Copyright (C) 2017 AT&T Intellectual Property. 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 - * + * + * 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========================================================= + * 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. */ - import {actionTypes} from './LoaderConstants.js'; export default (state = {}, action) => { diff --git a/openecomp-ui/src/nfvo-components/modal/GlobalModal.js b/openecomp-ui/src/nfvo-components/modal/GlobalModal.js new file mode 100644 index 0000000000..65a1ad683b --- /dev/null +++ b/openecomp-ui/src/nfvo-components/modal/GlobalModal.js @@ -0,0 +1,120 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ + +import React from 'react'; +import {connect} from 'react-redux'; + +import Modal from 'nfvo-components/modal/Modal.jsx'; +import Button from 'react-bootstrap/lib/Button.js'; +import i18n from 'nfvo-utils/i18n/i18n.js'; +import {modalContentComponents} from 'sdc-app/common/modal/ModalContentMapper.js'; +import {actionTypes, typeEnum} from './GlobalModalConstants.js'; + + +const typeClass = { + 'default': 'primary', + error: 'danger', + warning: 'warning', + success: 'success' +}; + + +const ModalFooter = ({type, onConfirmed, onDeclined, onClose, confirmationButtonText, cancelButtonText}) => + <Modal.Footer> + <Button bsStyle={typeClass[type]} onClick={onDeclined ? () => { + onDeclined(); + onClose();} : () => onClose()}> + {cancelButtonText} + </Button> + {onConfirmed && <Button bsStyle={typeClass[type]} onClick={() => { + onConfirmed(); + onClose(); + }}>{confirmationButtonText}</Button>} + </Modal.Footer>; + +ModalFooter.defaultProps = { + type: 'default', + confirmationButtonText: i18n('OK'), + cancelButtonText: i18n('Cancel') +}; + +export const mapStateToProps = ({modal}) => { + const show = !!modal; + return { + show, + ...modal + }; +}; + +export const mapActionToProps = (dispatch) => { + return { + onClose: () => dispatch({type: actionTypes.GLOBAL_MODAL_CLOSE}) + }; +}; + + +export class GlobalModalView extends React.Component { + + static propTypes = { + show: React.PropTypes.bool, + type: React.PropTypes.oneOf(['default', 'error', 'warning', 'success']), + title: React.PropTypes.string, + modalComponentProps: React.PropTypes.object, + modalComponentName: React.PropTypes.string, + onConfirmed: React.PropTypes.func, + onDeclined: React.PropTypes.func, + confirmationButtonText: React.PropTypes.string, + cancelButtonText: React.PropTypes.string + }; + + static defaultProps = { + show: false, + type: 'default', + title: '' + }; + + render() { + let {title, type, show, modalComponentName, modalComponentProps, + modalClassName, msg, onConfirmed, onDeclined, confirmationButtonText, cancelButtonText, onClose} = this.props; + const ComponentToRender = modalContentComponents[modalComponentName]; + return ( + <Modal show={show} bsSize={modalComponentProps && modalComponentProps.size} className={`onborading-modal ${modalClassName || ''} ${typeClass[type]}`}> + <Modal.Header> + <Modal.Title>{title}</Modal.Title> + </Modal.Header> + <Modal.Body> + {ComponentToRender ? <ComponentToRender {...modalComponentProps}/> : msg} + </Modal.Body> + {(onConfirmed || onDeclined || type !== typeEnum.DEFAULT) && + <ModalFooter + type={type} + onConfirmed={onConfirmed} + onDeclined={onDeclined} + onClose={onClose} + confirmationButtonText={confirmationButtonText} + cancelButtonText={cancelButtonText}/>} + </Modal> + ); + } + + componentDidUpdate() { + if (this.props.timeout) { + setTimeout(this.props.onClose, this.props.timeout); + } + } +}; + +export default connect(mapStateToProps, mapActionToProps, null, {withRef: true})(GlobalModalView); diff --git a/openecomp-ui/src/nfvo-components/modal/GlobalModalConstants.js b/openecomp-ui/src/nfvo-components/modal/GlobalModalConstants.js new file mode 100644 index 0000000000..0a0ed1fd71 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/modal/GlobalModalConstants.js @@ -0,0 +1,33 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ +import keyMirror from 'nfvo-utils/KeyMirror.js'; + +export const actionTypes = keyMirror({ + GLOBAL_MODAL_SHOW: null, + GLOBAL_MODAL_CLOSE: null, + GLOBAL_MODAL_ERROR: null, + GLOBAL_MODAL_WARNING: null, + GLOBAL_MODAL_SUCCESS: null, + +}); + + +export const typeEnum = { + DEFAULT: 'default', + ERROR: 'error', + WARNING: 'warning', + SUCCESS: 'success' +}; diff --git a/openecomp-ui/src/nfvo-components/modal/GlobalModalReducer.js b/openecomp-ui/src/nfvo-components/modal/GlobalModalReducer.js new file mode 100644 index 0000000000..28674ea569 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/modal/GlobalModalReducer.js @@ -0,0 +1,50 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ + +import {actionTypes, typeEnum} from './GlobalModalConstants.js'; + +export default (state = null, action) => { + switch (action.type) { + case actionTypes.GLOBAL_MODAL_SHOW: + return { + ...action.data + }; + case actionTypes.GLOBAL_MODAL_ERROR: + return { + type: typeEnum.ERROR, + modalClassName: 'notification-modal', + ...action.data + }; + case actionTypes.GLOBAL_MODAL_WARNING: + return { + type: typeEnum.WARNING, + modalClassName: 'notification-modal', + ...action.data + }; + + case actionTypes.GLOBAL_MODAL_SUCCESS: + return { + type: typeEnum.SUCCESS, + modalClassName: 'notification-modal', + ...action.data + }; + + case actionTypes.GLOBAL_MODAL_CLOSE: + return null; + default: + return state; + } +}; diff --git a/openecomp-ui/src/nfvo-components/modal/Modal.jsx b/openecomp-ui/src/nfvo-components/modal/Modal.jsx index be4963ef65..b0f704dba9 100644 --- a/openecomp-ui/src/nfvo-components/modal/Modal.jsx +++ b/openecomp-ui/src/nfvo-components/modal/Modal.jsx @@ -1,3 +1,18 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ import React from 'react'; import ReactDOM from 'react-dom'; import BootstrapModal from 'react-bootstrap/lib/Modal.js'; diff --git a/openecomp-ui/src/nfvo-components/notifications/NotificationConstants.js b/openecomp-ui/src/nfvo-components/notifications/NotificationConstants.js deleted file mode 100644 index 1a53f4c135..0000000000 --- a/openecomp-ui/src/nfvo-components/notifications/NotificationConstants.js +++ /dev/null @@ -1,29 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. 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========================================================= - */ - -import keyMirror from 'nfvo-utils/KeyMirror.js'; - -export default keyMirror({ - NOTIFY_ERROR: null, - NOTIFY_SUCCESS: null, - NOTIFY_WARNING: null, - NOTIFY_INFO: null, - NOTIFY_CLOSE: null -}); diff --git a/openecomp-ui/src/nfvo-components/notifications/NotificationModal.jsx b/openecomp-ui/src/nfvo-components/notifications/NotificationModal.jsx deleted file mode 100644 index 71793097fb..0000000000 --- a/openecomp-ui/src/nfvo-components/notifications/NotificationModal.jsx +++ /dev/null @@ -1,100 +0,0 @@ -/** - * NotificationModal options: - * - * show: whether to show notification or not, - * type: the type of the notification. valid values are: 'default', 'error', 'warning', 'success' - * msg: the notification content. could be a string or node (React component) - * title: the notification title - * timeout: timeout for the notification to fade out. if timeout == 0 then the notification is rendered until the user closes it - * - */ -import React, {Component, PropTypes} from 'react'; -import {connect} from 'react-redux'; -import Button from 'react-bootstrap/lib/Button.js'; - -import i18n from 'nfvo-utils/i18n/i18n.js'; -import Modal from 'nfvo-components/modal/Modal.jsx'; -import SubmitErrorResponse from 'nfvo-components/SubmitErrorResponse.jsx'; -import NotificationConstants from './NotificationConstants.js'; - -let typeClass = { - 'default': 'primary', - error: 'danger', - warning: 'warning', - success: 'success' -}; - -const mapActionsToProps = (dispatch) => { - return {onCloseClick: () => dispatch({type: NotificationConstants.NOTIFY_CLOSE})}; -}; - -const mapStateToProps = ({notification}) => { - - let show = notification !== null && notification.title !== 'Conflict'; - let mapResult = {show}; - if (show) { - mapResult = {show, ...notification}; - } - - return mapResult; -}; - -export class NotificationModal extends Component { - - static propTypes = { - show: PropTypes.bool, - type: PropTypes.oneOf(['default', 'error', 'warning', 'success']), - title: PropTypes.string, - msg: PropTypes.node, - validationResponse: PropTypes.object, - timeout: PropTypes.number - }; - - static defaultProps = { - show: false, - type: 'default', - title: '', - msg: '', - timeout: 0 - }; - - state = {type: undefined}; - - componentWillReceiveProps(nextProps) { - if (this.props.show !== nextProps.show && nextProps.show === false) { - this.setState({type: this.props.type}); - } - else { - this.setState({type: undefined}); - } - } - - componentDidUpdate() { - if (this.props.timeout) { - setTimeout(this.props.onCloseClick, this.props.timeout); - } - } - - render() { - let {title, type, msg, show, validationResponse, onCloseClick} = this.props; - if (!show) { - type = this.state.type; - } - if (validationResponse) { - msg = (<SubmitErrorResponse validationResponse={validationResponse}/>); - } - return ( - <Modal show={show} className={`notification-modal ${typeClass[type]}`}> - <Modal.Header> - <Modal.Title>{title}</Modal.Title> - </Modal.Header> - <Modal.Body>{msg}</Modal.Body> - <Modal.Footer> - <Button bsStyle={typeClass[type]} onClick={onCloseClick}>{i18n('OK')}</Button> - </Modal.Footer> - </Modal> - ); - } -} - -export default connect(mapStateToProps, mapActionsToProps)(NotificationModal); diff --git a/openecomp-ui/src/nfvo-components/notifications/NotificationReducer.js b/openecomp-ui/src/nfvo-components/notifications/NotificationReducer.js deleted file mode 100644 index c8b30d6e50..0000000000 --- a/openecomp-ui/src/nfvo-components/notifications/NotificationReducer.js +++ /dev/null @@ -1,51 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. 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========================================================= - */ - -import NotificationConstants from './NotificationConstants.js'; - -export default (state = null, action) => { - switch (action.type) { - case NotificationConstants.NOTIFY_INFO: - return {type: 'default', title: action.data.title, msg: action.data.msg, timeout: action.data.timeout}; - - case NotificationConstants.NOTIFY_ERROR: - return { - type: 'error', - title: action.data.title, - msg: action.data.msg, - validationResponse: action.data.validationResponse, - timeout: action.data.timeout - }; - - case NotificationConstants.NOTIFY_WARNING: - return {type: 'warning', title: action.data.title, msg: action.data.msg, timeout: action.data.timeout}; - - case NotificationConstants.NOTIFY_SUCCESS: - return { - type: 'success', title: action.data.title, msg: action.data.msg, timeout: action.data.timeout - }; - case NotificationConstants.NOTIFY_CLOSE: - return null; - - default: - return state; - } - -}; diff --git a/openecomp-ui/src/nfvo-components/panel/NavigationSideBar.jsx b/openecomp-ui/src/nfvo-components/panel/NavigationSideBar.jsx index feb0f813ea..3b89137090 100644 --- a/openecomp-ui/src/nfvo-components/panel/NavigationSideBar.jsx +++ b/openecomp-ui/src/nfvo-components/panel/NavigationSideBar.jsx @@ -1,9 +1,23 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ import React from 'react'; import classnames from 'classnames'; import Collapse from 'react-bootstrap/lib/Collapse.js'; class NavigationSideBar extends React.Component { - static PropTypes = { activeItemId: React.PropTypes.string.isRequired, onSelect: React.PropTypes.func, @@ -11,51 +25,26 @@ class NavigationSideBar extends React.Component { groups: React.PropTypes.array }; + constructor(props) { + super(props); + this.state = { + activeItemId: null + }; + this.handleItemClicked = this.handleItemClicked.bind(this); + } + render() { let {groups, activeItemId} = this.props; return ( <div className='navigation-side-content'> {groups.map(group => ( - <div className='navigation-group' key={group.id}> - <div className='group-name'>{group.name}</div> - <div className='navigation-group-items'> - { - group.items && group.items.map(item => this.renderGroupItem(item, activeItemId)) - } - </div> - </div> + <NavigationMenu menu={group} activeItemId={activeItemId} onNavigationItemClick={this.handleItemClicked} key={'menu_' + group.id} /> ))} </div> ); } - renderGroupItem(item, activeItemId) { - let isGroup = item.items && item.items.length > 0; - return ( - <div className={classnames('navigation-group-item', {'selected-item': item.id === activeItemId})}> - <div - key={item.id} - className={classnames('navigation-group-item-name', { - 'selected': item.id === activeItemId, - 'disabled': item.disabled, - 'bold-name': item.expanded, - 'hidden': item.hidden - })} - onClick={(event) => this.handleItemClicked(event, item)}> - {item.name} - </div> - {isGroup && - <Collapse in={item.expanded}> - <div> - {item.items.map(item => this.renderGroupItem(item, activeItemId))} - </div> - </Collapse> - } - </div> - ); - } - handleItemClicked(event, item) { event.stopPropagation(); if(this.props.onToggle) { @@ -70,4 +59,70 @@ class NavigationSideBar extends React.Component { } } +class NavigationMenu extends React.Component { + static PropTypes = { + activeItemId: React.PropTypes.string.isRequired, + onNavigationItemClick: React.PropTypes.func, + menu: React.PropTypes.array + }; + + render() { + const {menu, activeItemId, onNavigationItemClick} = this.props; + return ( + <div className='navigation-group' key={menu.id}> + <NavigationMenuHeader title={menu.name} /> + <NavigationMenuItems items={menu.items} activeItemId={activeItemId} onNavigationItemClick={onNavigationItemClick} /> + </div>); + } +} + +function NavigationMenuHeader(props) { + return <div className='group-name' data-test-id='navbar-group-name'>{props.title}</div>; +} + +function NavigationMenuItems(props) { + const {items, activeItemId, onNavigationItemClick} = props; + return ( + <div className='navigation-group-items'> + { + items && items.map(item => (<NavigationMenuItem key={'menuItem_' + item.id} item={item} activeItemId={activeItemId} onNavigationItemClick={onNavigationItemClick} />)) + } + </div> + ); +} + +function NavigationMenuItem(props) { + const {onNavigationItemClick, item, activeItemId} = props; + const isGroup = item.items && item.items.length > 0; + return ( + <div className={classnames('navigation-group-item', {'selected-item': item.id === activeItemId})} key={'item_' + item.id}> + <NavigationLink item={item} activeItemId={activeItemId} onClick={onNavigationItemClick} /> + {isGroup && <Collapse in={item.expanded} data-test-id={'navigation-group-' + item.id}> + <div> + {item.items.map(subItem => (<NavigationMenuItem key={'menuItem_' + subItem.id} item={subItem} onNavigationItemClick={onNavigationItemClick} activeItemId={activeItemId} />)) } + </div> + </Collapse> + } + </div> + ); +} + +function NavigationLink(props) { + const {item, activeItemId, onClick} = props; + return ( + <div + key={'navAction_' + item.id} + className={classnames('navigation-group-item-name', { + 'selected': item.id === activeItemId, + 'disabled': item.disabled, + 'bold-name': item.expanded, + 'hidden': item.hidden + })} + onClick={(event) => onClick(event, item)} + data-test-id={'navbar-group-item-' + item.id}> + {item.name} + </div> + ); +} + export default NavigationSideBar; diff --git a/openecomp-ui/src/nfvo-components/panel/SlidePanel.jsx b/openecomp-ui/src/nfvo-components/panel/SlidePanel.jsx deleted file mode 100644 index 10c5326300..0000000000 --- a/openecomp-ui/src/nfvo-components/panel/SlidePanel.jsx +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react'; -import FontAwesome from 'react-fontawesome'; -import ReactDOM from 'react-dom'; - -class SlidePanel extends React.Component { - - static PropTypes = { - direction: React.PropTypes.string.isRequired, - className: React.PropTypes.string, - title: React.PropTypes.string, - isOpen: React.PropTypes.bool - }; - - static defaultProps = { - title: '', - className: '', - isOpen: true - }; - - state = { - isOpen: this.props.isOpen, - direction: this.props.direction, - width: 0, - arrowWidth: 0 - }; - - componentDidMount() { - this.setSliderPosition(); - } - - componentDidUpdate() { - this.setSliderPosition(); - } - - render() { - - let {children, className} = this.props; - let {isOpen} = this.state; - - return ( - <div className={ `slide-panel ${className}`}> - {this.renderHeader(isOpen)} - <div className={'slide-panel-content ' + (isOpen ? 'opened' : 'closed')}>{children}</div> - </div> - ); - } - - renderHeader(isOpen) { - let {direction: initialDirection, title} = this.props; - let {direction: currentDirection} = this.state; - - let iconName = currentDirection === 'right' ? 'angle-double-right collapse-double-icon' : 'angle-double-left collapse-double-icon'; - - let awestyle = {padding: '5px'}; - - if (!isOpen && initialDirection === 'right') { - awestyle.marginLeft = '-1px'; - } - return ( - <div className='slide-panel-header'> - { initialDirection === 'left' && <span className='slide-panel-header-title'>{title}</span>} - <FontAwesome - ref='arrowIcon' - style={awestyle} - onClick={this.handleClick} - className='pull-right' - name={iconName} - size='2x'/> - { initialDirection === 'right' && <span className='slide-panel-header-title'>{title}</span>} - </div> - ); - } - - handleClick = () => { - this.setState({ - isOpen: !this.state.isOpen, - direction: this.state.direction === 'left' ? 'right' : 'left' - }); - } - - setSliderPosition = () => { - - let el = ReactDOM.findDOMNode(this); - let {style} = el; - - let {direction: initialDirection} = this.props; - let arrowIconSize = Math.floor(ReactDOM.findDOMNode(this.refs.arrowIcon).getBoundingClientRect().width) * 2; - if (!this.state.isOpen) { - if (this.props.direction === 'left') { - style.left = arrowIconSize - el.getBoundingClientRect().width + 'px'; - } - if (initialDirection === 'right') { - style.right = arrowIconSize - el.getBoundingClientRect().width + 'px'; - } - } - else { - if (initialDirection === 'left') { - style.left = '0px'; - } - - if (this.props.direction === 'right') { - style.right = '0px'; - } - } - } - -} - -export default SlidePanel;
\ No newline at end of file diff --git a/openecomp-ui/src/nfvo-components/panel/versionController/VersionController.jsx b/openecomp-ui/src/nfvo-components/panel/versionController/VersionController.jsx index 78525f84c6..6d900dd0bb 100644 --- a/openecomp-ui/src/nfvo-components/panel/versionController/VersionController.jsx +++ b/openecomp-ui/src/nfvo-components/panel/versionController/VersionController.jsx @@ -1,17 +1,31 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ import React from 'react'; -import classnames from 'classnames'; import i18n from 'nfvo-utils/i18n/i18n.js'; -import Navbar from 'react-bootstrap/lib/Navbar.js'; -import Nav from 'react-bootstrap/lib/Nav.js'; -import ValidationInput from 'nfvo-components/input/validation/ValidationInput.jsx'; -import {actionsEnum, statusEnum} from './VersionControllerConstants.js'; +import {actionsEnum, statusEnum, statusBarTextMap } from './VersionControllerConstants.js'; +import SVGIcon from 'nfvo-components/icon/SVGIcon.jsx'; +import Tooltip from 'react-bootstrap/lib/Tooltip.js'; +import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger.js'; class VersionController extends React.Component { static propTypes = { - version: React.PropTypes.string, + version: React.PropTypes.object, viewableVersions: React.PropTypes.array, onVersionSwitching: React.PropTypes.func, isCheckedOut: React.PropTypes.bool.isRequired, @@ -23,143 +37,146 @@ class VersionController extends React.Component { }; render() { - let {status, isCheckedOut, version = '', viewableVersions = [], onVersionSwitching, callVCAction, onSave, isFormDataValid, onClose} = this.props; + let {status, isCheckedOut, version = {}, viewableVersions = [], onVersionSwitching, callVCAction, onSave, isFormDataValid, onClose} = this.props; let isCheckedIn = Boolean(status === statusEnum.CHECK_IN_STATUS); - let isLatestVersion = Boolean(version === viewableVersions[viewableVersions.length - 1]); + let isLatestVersion = Boolean(version.id === viewableVersions[viewableVersions.length - 1].id); if (!isLatestVersion) { status = statusEnum.PREVIOUS_VERSION; } - return ( <div className='version-controller-bar'> - <Navbar inverse className='navbar'> - <Navbar.Collapse> - <Nav className='items-in-left'> - <div className='version-section'> - <ValidationInput - type='select' - selectedEnum={version} - onEnumChange={value => onVersionSwitching && onVersionSwitching(value)}> - {viewableVersions && viewableVersions.map(viewVersion => { - return ( - <option key={viewVersion} value={viewVersion}>{`V ${viewVersion}`}</option> - ); - }) - } - {!viewableVersions.includes(version) && - <option key={version} value={version}>{`V ${version}`}</option>} - </ValidationInput> - </div> - <div className='vc-status'> - <div className='onboarding-status-icon'></div> - <div className='status-text'> {i18n('ONBOARDING')} - <div className='status-text-dash'> -</div> - </div> - {this.renderStatus(status)} - </div> - </Nav> - <Nav pullRight> - <div className='items-in-right'> - <div className='action-buttons'> - {callVCAction && - <div className='version-control-buttons'> - <div - className={classnames('vc-nav-item-button button-submit', {'disabled': !isCheckedIn || !isLatestVersion})} - onClick={() => this.submit(callVCAction)}> - {i18n('Submit')} - </div> - <div - className={classnames('vc-nav-item-button button-checkin-checkout', {'disabled': status === statusEnum.LOCK_STATUS || !isLatestVersion})} - onClick={() => this.checkinCheckoutVersion(callVCAction)}> - {`${isCheckedOut ? i18n('Check In') : i18n('Check Out')}`} - </div> - <div - className={classnames('sprite-new revert-btn ng-scope ng-isolate-scope', {'disabled': !isCheckedOut || version === '0.1' || !isLatestVersion})} - onClick={() => this.revertCheckout(callVCAction)}> - </div> - </div> - } - {onSave && - <div - className={classnames('sprite-new save-btn ng-scope ng-isolate-scope', {'disabled': !isCheckedOut || !isFormDataValid || !isLatestVersion})} - onClick={() => onSave()}> - </div> - } - </div> - <div className='vc-nav-item-close' onClick={() => onClose && onClose()}> X</div> - </div> - </Nav> - </Navbar.Collapse> - </Navbar> + <div className='vc-container'> + <div className='version-status-container'> + <VersionSelector viewableVersions={viewableVersions} version={version} onVersionSwitching={onVersionSwitching} /> + <StatusBarUpdates status={status}/> + </div> + <div className='save-submit-cancel-container'> + <ActionButtons onSubmit={callVCAction ? () => this.submit(callVCAction, version) : undefined} + onRevert={callVCAction ? () => this.revertCheckout(callVCAction, version) : undefined} + status={status} + onCheckinCheckout={callVCAction ? () => this.checkinCheckoutVersion(callVCAction, version) : undefined} + onSave={onSave ? () => onSave() : undefined} + isLatestVersion={isLatestVersion} + isCheckedOut={isCheckedOut} + isCheckedIn={isCheckedIn} isFormDataValid={isFormDataValid} version={version}/> + {onClose && <div className='vc-nav-item-close' onClick={() => onClose()} data-test-id='vc-cancel-btn'> X</div>} + </div> + </div> </div> ); } - renderStatus(status) { - switch (status) { - case statusEnum.CHECK_OUT_STATUS: - return ( - <div className='checkout-status-icon'> - <div className='catalog-tile-check-in-status sprite-new checkout-editable-status-icon'></div> - <div className='status-text'> {i18n('CHECKED OUT')} </div> - </div> - ); - case statusEnum.LOCK_STATUS: - return ( - <div className='status-text'> {i18n('LOCKED')} </div> - ); - case statusEnum.CHECK_IN_STATUS: - return ( - <div className='status-text'> {i18n('CHECKED IN')} </div> - ); - case statusEnum.SUBMIT_STATUS: - return ( - <div className='status-text'> {i18n('SUBMITTED')} </div> - ); - default: - return ( - <div className='status-text'> {i18n(status)} </div> - ); - } + submit(callVCAction, version) { + const action = actionsEnum.SUBMIT; + callVCAction(action, version); + } + + revertCheckout(callVCAction, version) { + const action = actionsEnum.UNDO_CHECK_OUT; + callVCAction(action, version); } - checkinCheckoutVersion(callVCAction) { + checkinCheckoutVersion(callVCAction, version) { if (this.props.isCheckedOut) { - this.checkin(callVCAction); + this.checkin(callVCAction, version); } else { - this.checkout(callVCAction); + this.checkout(callVCAction, version); } } - - checkin(callVCAction) { - + checkin(callVCAction, version) { const action = actionsEnum.CHECK_IN; - if (this.props.onSave) { this.props.onSave().then(()=>{ - callVCAction(action); - }); + callVCAction(action, version); + }); }else{ - callVCAction(action); + callVCAction(action, version); } } - - checkout(callVCAction) { + checkout(callVCAction, version) { const action = actionsEnum.CHECK_OUT; - callVCAction(action); + callVCAction(action, version); } +} - submit(callVCAction) { - const action = actionsEnum.SUBMIT; - callVCAction(action); +class ActionButtons extends React.Component { + static propTypes = { + version: React.PropTypes.object, + onSubmit: React.PropTypes.func, + onRevert: React.PropTypes.func, + onSave: React.PropTypes.func, + isLatestVersion: React.PropTypes.bool, + isCheckedIn: React.PropTypes.bool, + isCheckedOut: React.PropTypes.bool, + isFormDataValid: React.PropTypes.bool + }; + render() { + const {onSubmit, onRevert, onSave, isLatestVersion, isCheckedIn, isCheckedOut, isFormDataValid, version, status, onCheckinCheckout} = this.props; + const [checkinBtnIconSvg, checkinCheckoutBtnTitle] = status === statusEnum.CHECK_OUT_STATUS ? + ['version-controller-lock-open', i18n('Check In')] : + ['version-controller-lock-closed', i18n('Check Out')]; + const disabled = (isLatestVersion && onCheckinCheckout && status !== statusEnum.LOCK_STATUS) ? false : true; + return ( + <div className='action-buttons'> + <VCButton dataTestId='vc-checkout-btn' onClick={onCheckinCheckout} isDisabled={disabled} + name={checkinBtnIconSvg} tooltipText={checkinCheckoutBtnTitle}/> + {onSubmit && onRevert && + <div className='version-control-buttons'> + <VCButton dataTestId='vc-submit-btn' onClick={onSubmit} isDisabled={!isCheckedIn || !isLatestVersion} + name='version-controller-submit' tooltipText={i18n('Submit')}/> + <VCButton dataTestId='vc-revert-btn' onClick={onRevert} isDisabled={!isCheckedOut || version.label === '0.1' || !isLatestVersion} + name='version-controller-revert' tooltipText={i18n('Revert')}/> + </div> + } + {onSave && + <VCButton dataTestId='vc-save-btn' onClick={() => onSave()} isDisabled={!isCheckedOut || !isFormDataValid || !isLatestVersion} + name='version-controller-save' tooltipText={i18n('Save')}/> + } + </div> + ); } +} - revertCheckout(callVCAction) { - const action = actionsEnum.UNDO_CHECK_OUT; - callVCAction(action); - } +function StatusBarUpdates({status}) { + return ( + <div className='vc-status'> + <span className='status-text'>{i18n(statusBarTextMap[status])}</span> + </div> + ); +} + +function VCButton({name, tooltipText, isDisabled, onClick, dataTestId}) { + let onClickAction = isDisabled ? ()=>{} : onClick; + let disabled = isDisabled ? 'disabled' : ''; + + return ( + <OverlayTrigger placement='top' overlay={<Tooltip id='vc-tooltip'>{tooltipText}</Tooltip>}> + <div disabled={disabled} className='action-buttons-svg'> + <SVGIcon data-test-id={dataTestId} iconClassName={disabled} onClick={onClickAction ? onClickAction : undefined} name={name}/> + </div> + </OverlayTrigger> + ); +} + +function VersionSelector(props) { + let {version = {}, viewableVersions = [], onVersionSwitching} = props; + const includedVersions = viewableVersions.filter(ver => {return ver.id === version.id;}); + return (<div className='version-section-wrapper'> + <select className='version-selector' + onChange={ev => onVersionSwitching && onVersionSwitching({id: ev.target.value, label: ev.target.value})} + value={version.label}> + {viewableVersions && viewableVersions.map(viewVersion => { + return ( + <option key={viewVersion.id} value={viewVersion.id} data-test-id='vc-version-option'>{`V ${viewVersion.label}`}</option> + ); + }) + } + {!includedVersions.length && + <option key={version.id} value={version.id}>{`V ${version.label}`}</option>} + </select> + </div>); } export default VersionController; diff --git a/openecomp-ui/src/nfvo-components/panel/versionController/VersionControllerConstants.js b/openecomp-ui/src/nfvo-components/panel/versionController/VersionControllerConstants.js index 9251fd12c4..9af142433c 100644 --- a/openecomp-ui/src/nfvo-components/panel/versionController/VersionControllerConstants.js +++ b/openecomp-ui/src/nfvo-components/panel/versionController/VersionControllerConstants.js @@ -1,23 +1,18 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ +/*! * Copyright (C) 2017 AT&T Intellectual Property. 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 - * + * + * 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========================================================= + * 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. */ - import keyMirror from 'nfvo-utils/KeyMirror.js'; export const actionsEnum = keyMirror({ @@ -36,3 +31,11 @@ export const statusEnum = keyMirror({ PREVIOUS_VERSION: 'READ ONLY' }); +export const statusBarTextMap = keyMirror({ + 'Locked': 'Checked Out', + 'LockedByUser': '', + 'Available': 'Checked In', + 'Final': 'Submitted', + 'READ ONLY': 'Locked' +}); + diff --git a/openecomp-ui/src/nfvo-components/panel/versionController/VersionControllerUtils.js b/openecomp-ui/src/nfvo-components/panel/versionController/VersionControllerUtils.js index de9914454c..e8c12abec3 100644 --- a/openecomp-ui/src/nfvo-components/panel/versionController/VersionControllerUtils.js +++ b/openecomp-ui/src/nfvo-components/panel/versionController/VersionControllerUtils.js @@ -1,23 +1,18 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ +/*! * Copyright (C) 2017 AT&T Intellectual Property. 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 - * + * + * 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========================================================= + * 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. */ - import Configuration from 'sdc-app/config/Configuration.js'; import {statusEnum} from './VersionControllerConstants.js'; @@ -25,24 +20,32 @@ import {statusEnum} from './VersionControllerConstants.js'; const VersionControllerUtils = { getCheckOutStatusKindByUserID(status, lockingUser) { - let currentLoginUserID = Configuration.get('ATTUserID'); - let isCheckedOut = currentLoginUserID === lockingUser; + let returnStatus; + let isCheckedOut; + let currentLoginUserID = Configuration.get('UserID'); + if (lockingUser) { + isCheckedOut = currentLoginUserID === lockingUser; + returnStatus = isCheckedOut ? status : statusEnum.LOCK_STATUS; + } else { + isCheckedOut = false; + returnStatus = status; + } return { - status: isCheckedOut ? status : statusEnum.LOCK_STATUS, + status: returnStatus, isCheckedOut }; }, isCheckedOutByCurrentUser(resource) { - let currentLoginUserID = Configuration.get('ATTUserID'); + let currentLoginUserID = Configuration.get('UserID'); return resource.lockingUser !== undefined && resource.lockingUser === currentLoginUserID; }, isReadOnly(resource) { const {version, viewableVersions = []} = resource; const latestVersion = viewableVersions[viewableVersions.length - 1]; - return version !== latestVersion || !VersionControllerUtils.isCheckedOutByCurrentUser(resource); + return version.id !== latestVersion.id || !VersionControllerUtils.isCheckedOutByCurrentUser(resource); } }; diff --git a/openecomp-ui/src/nfvo-components/progressBar/ProgressBar.jsx b/openecomp-ui/src/nfvo-components/progressBar/ProgressBar.jsx index d786aeef8b..40720c39f4 100644 --- a/openecomp-ui/src/nfvo-components/progressBar/ProgressBar.jsx +++ b/openecomp-ui/src/nfvo-components/progressBar/ProgressBar.jsx @@ -1,3 +1,18 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. 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. + */ import React from 'react'; class ProgressBar extends React.Component { diff --git a/openecomp-ui/src/nfvo-components/table/SelectActionTable.jsx b/openecomp-ui/src/nfvo-components/table/SelectActionTable.jsx new file mode 100644 index 0000000000..06cb98bbe8 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/table/SelectActionTable.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import i18n from 'nfvo-utils/i18n/i18n.js'; +import SVGIcon from 'nfvo-components/icon/SVGIcon.jsx'; +import uuid from 'uuid-js'; + +export default class SelectActionTable extends React.Component { + + render() { + let {columns, onAdd, isReadOnlyMode, children, onAddItem} = this.props; + return ( + <div className={`select-action-table-view ${isReadOnlyMode ? 'disabled' : ''}`}> + <div className='select-action-table-controllers'> + {onAdd && onAddItem && <div data-test-id='select-action-table-add' onClick={onAdd}>{onAddItem}</div>} + <SVGIcon name='trash-o' className='dummy-icon' /> + </div> + <div className='select-action-table'> + <div className='select-action-table-headers'> + {columns.map(column => <div key={uuid.create()} className='select-action-table-header'>{i18n(column)}</div>)} + <SVGIcon name='trash-o' className='dummy-icon' /> + <SVGIcon name='trash-o' className='dummy-icon' /> + </div> + <div className='select-action-table-body'> + {children} + </div> + </div> + </div> + ); + } +} diff --git a/openecomp-ui/src/nfvo-components/table/SelectActionTableCell.jsx b/openecomp-ui/src/nfvo-components/table/SelectActionTableCell.jsx new file mode 100644 index 0000000000..2664c8e944 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/table/SelectActionTableCell.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import SelectInput from 'nfvo-components/input/SelectInput.jsx'; + +const SelectActionTableCell = ({options, selected, disabled, onChange, clearable = true, placeholder}) => { + return ( + <div className='select-action-table-cell'> + <SelectInput + placeholder={placeholder} + type='select' + value={selected} + data-test-id='select-action-table-dropdown' + disabled={disabled} + onChange={option => onChange(option ? option.value : null)} + clearable={clearable} + options={options} /> + </div> + ); +}; + +export default SelectActionTableCell; diff --git a/openecomp-ui/src/nfvo-components/table/SelectActionTableRow.jsx b/openecomp-ui/src/nfvo-components/table/SelectActionTableRow.jsx new file mode 100644 index 0000000000..17d8a17c09 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/table/SelectActionTableRow.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import SVGIcon from '../icon/SVGIcon.jsx'; +import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger.js'; +import Tooltip from 'react-bootstrap/lib/Tooltip.js'; + +function tooltip (msg) { + return ( + <Tooltip className='select-action-table-error-tooltip' id='error-tooltip'>{msg}</Tooltip> + ); +}; + +const IconWithOverlay = ({overlayMsg}) => ( + <OverlayTrigger placement='bottom' overlay={tooltip(overlayMsg)}> + <SVGIcon name='error-circle'/> + </OverlayTrigger> +); + +const SelectActionTableRow = ({children, onDelete, hasError, overlayMsg}) => ( + <div className='select-action-table-row-wrapper'> + <div className={`select-action-table-row ${hasError ? 'has-error' : ''}`}> + {children} + </div> + {onDelete ? <SVGIcon name='trash-o' data-test-id='select-action-table-delete' onClick={onDelete} /> : <SVGIcon name='angle-left' className='dummy-icon' />} + {hasError ? overlayMsg ? <IconWithOverlay overlayMsg={overlayMsg}/> : <SVGIcon name='error-circle'/> + : hasError === undefined ? <SVGIcon name='angle-left' className='dummy-icon'/> : <SVGIcon name='check-circle'/>} + + </div> +); + +export default SelectActionTableRow; |