diff options
Diffstat (limited to 'openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments')
14 files changed, 1244 insertions, 264 deletions
diff --git a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachments.js b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachments.js index a4b95a4b7e..8f2506abdd 100644 --- a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachments.js +++ b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachments.js @@ -1,42 +1,89 @@ -/*- - * ============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 {connect} from 'react-redux'; +import i18n from 'nfvo-utils/i18n/i18n.js'; +import {actionTypes as modalActionTypes} from 'nfvo-components/modal/GlobalModalConstants.js'; +import SoftwareProductActionHelper from 'sdc-app/onboarding/softwareProduct/SoftwareProductActionHelper.js'; +import HeatSetupActionHelper from './setup/HeatSetupActionHelper.js'; import SoftwareProductAttachmentsView from './SoftwareProductAttachmentsView.jsx'; -import SoftwareProductAttachmentsActionHelper from './SoftwareProductAttachmentsActionHelper.js'; +import {errorLevels} from 'sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationConstants.js'; +import OnboardingActionHelper from 'sdc-app/onboarding/OnboardingActionHelper.js'; +import HeatSetup from './setup/HeatSetup.js'; +import {doesHeatDataExist} from './SoftwareProductAttachmentsUtils.js'; + +import VersionControllerUtils from 'nfvo-components/panel/versionController/VersionControllerUtils.js'; + +export const mapStateToProps = (state) => { + let { + softwareProduct: { + softwareProductEditor:{data: currentSoftwareProduct = {}}, + softwareProductAttachments: {heatSetup, heatSetupCache, heatValidation : {errorList}} + } + } = state; + + let {unassigned = [], modules = []} = heatSetup; + let goToOverview = true; + if (errorList) { + for (let i = 0 ; i < errorList.length ; i++) { + if (errorList[i].level === errorLevels.ERROR) { + goToOverview = false; + } + } + } + let heatDataExist = doesHeatDataExist(heatSetup); -export const mapStateToProps = ({softwareProduct: {softwareProductAttachments}}) => { - let {attachmentsTree, hoveredNode, selectedNode, errorList} = softwareProductAttachments; + let isReadOnlyMode = currentSoftwareProduct && currentSoftwareProduct.version ? + VersionControllerUtils.isReadOnly(currentSoftwareProduct) : false; + let {version} = currentSoftwareProduct; return { - attachmentsTree, - hoveredNode, - selectedNode, - errorList + isValidationAvailable: unassigned.length === 0 && modules.length > 0, + heatSetup, + heatSetupCache, + heatDataExist, + goToOverview, + HeatSetupComponent: HeatSetup, + isReadOnlyMode, + version }; }; -const mapActionsToProps = (dispatch) => { +export const mapActionsToProps = (dispatch, {softwareProductId}) => { return { - toggleExpanded: (path) => SoftwareProductAttachmentsActionHelper.toggleExpanded(dispatch, {path}), - onSelectNode: (nodeName) => SoftwareProductAttachmentsActionHelper.onSelectNode(dispatch, {nodeName}), - onUnselectNode: () => SoftwareProductAttachmentsActionHelper.onUnselectNode(dispatch) + onDownload: ({heatCandidate, isReadOnlyMode, version}) => SoftwareProductActionHelper.downloadHeatFile(dispatch, {softwareProductId, heatCandidate, isReadOnlyMode, version}), + onUpload: (formData, version) => dispatch({ + type: modalActionTypes.GLOBAL_MODAL_WARNING, + data:{ + msg: i18n('Upload will erase existing data. Do you want to continue?'), + confirmationButtonText: i18n('Continue'), + onConfirmed: ()=>SoftwareProductActionHelper.uploadFile(dispatch, { + softwareProductId, + formData, + failedNotificationTitle: i18n('Upload validation failed'), + version + }) + } + }), + onSave: (heatCandidate, version) => SoftwareProductActionHelper.updateSoftwareProductHeatCandidate(dispatch, {softwareProductId, heatCandidate, version}), + onGoToOverview: () => { + OnboardingActionHelper.navigateToSoftwareProductLandingPage(dispatch, {softwareProductId}); + }, + onProcessAndValidate: ({heatData, heatDataCache, isReadOnlyMode, version}) => { + return HeatSetupActionHelper.processAndValidateHeat(dispatch, + {softwareProductId, heatData, heatDataCache, isReadOnlyMode, version}); + } }; }; diff --git a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsConstants.js b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsConstants.js index 33af476d9c..b0410d1566 100644 --- a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsConstants.js +++ b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsConstants.js @@ -1,55 +1,19 @@ -/*- - * ============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'; -import i18n from 'nfvo-utils/i18n/i18n.js'; - -export const actionTypes = keyMirror({ - TOGGLE_EXPANDED: null, - SELECTED_NODE: null, - UNSELECTED_NODE: null -}); - -export const errorTypes = keyMirror({ - MISSING_FILE_IN_ZIP: i18n('missing file in zip'), - MISSING_FILE_IN_MANIFEST: i18n('missing file in manifest'), - MISSING_OR_ILLEGAL_FILE_TYPE_IN_MANIFEST: i18n('missing or illegal file type in manifest'), - FILE_IS_YML_WITHOUT_YML_EXTENSION: i18n('file is defined as a heat file but it doesn\'t have .yml or .yaml extension'), - FILE_IS_ENV_WITHOUT_ENV_EXTENSION: i18n('file is defined as an env file but it doesn\'t have .env extension'), - ILLEGAL_YAML_FILE_CONTENT: i18n('illegal yaml file content'), - ILLEGAL_HEAT_YAML_FILE_CONTENT: i18n('illegal HEAT yaml file content'), - MISSING_FILE_NAME_IN_MANIFEST: i18n('a file is written in manifest without file name'), - MISSING_ENV_FILE_IN_ZIP: i18n('missing env file in zip'), - ARTIFACT_NOT_IN_USE: i18n('artifact not in use') -}); - -export const nodeTypes = keyMirror({ - heat: i18n('Heat'), - volume: i18n('Volume'), - network: i18n('Network'), - artifact: i18n('Artifact'), - env: i18n('Environment'), - other: i18n('') -}); - -export const mouseActions = keyMirror({ - MOUSE_BUTTON_CLICK: 0 -}); - +export const tabsMapping = { + SETUP: 1, + VALIDATION: 2 +}; diff --git a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsUtils.js b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsUtils.js new file mode 100644 index 0000000000..2e76b11630 --- /dev/null +++ b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsUtils.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. + */ + +export function doesHeatDataExist(heatData) { + let result = false; + for (let key of Object.keys(heatData)) { + if(heatData[key].length > 0) { + result = true; + } + } + return result; +} diff --git a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsView.jsx b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsView.jsx index c52999ca46..66fb2f8356 100644 --- a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsView.jsx +++ b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsView.jsx @@ -1,182 +1,119 @@ -import React from 'react'; -import FontAwesome from 'react-fontawesome'; -import classNames from 'classnames'; -import Collapse from 'react-bootstrap/lib/Collapse.js'; - +/*! + * 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'; +import Tabs from 'react-bootstrap/lib/Tabs.js'; +import Tab from 'react-bootstrap/lib/Tab.js'; +import {tabsMapping} from './SoftwareProductAttachmentsConstants.js'; import i18n from 'nfvo-utils/i18n/i18n.js'; -import {nodeTypes, mouseActions} from './SoftwareProductAttachmentsConstants'; - -const typeToIcon = Object.freeze({ - heat: 'building-o', - volume: 'database', - network: 'cloud', - artifact: 'gear', - env: 'server', - other: 'cube' -}); +import Icon from 'nfvo-components/icon/Icon.jsx'; +import HeatValidation from './validation/HeatValidation.js'; -const leftPanelWidth = 250; - -class SoftwareProductAttachmentsView extends React.Component { +class HeatScreenView extends Component { static propTypes = { - attachmentsTree: React.PropTypes.object.isRequired + isValidationAvailable: PropTypes.bool, + goToOverview: PropTypes.bool }; + state = { - treeWidth: '400' + activeTab: tabsMapping.SETUP }; render() { - let {attachmentsTree, errorList} = this.props; - let {treeWidth} = this.state; + let {isValidationAvailable, isReadOnlyMode, heatDataExist, onDownload, softwareProductId, onProcessAndValidate, heatSetup, HeatSetupComponent, onGoToOverview, version, ...other} = this.props; return ( - <div className='software-product-attachments'> - <div className='software-product-attachments-tree' style={{'width' : treeWidth + 'px'}}> - <div className='tree-wrapper'> - { - attachmentsTree && attachmentsTree.children && attachmentsTree.children.map((child, ind) => this.renderNode(child, [ind])) - } - </div> + <div className='vsp-attachments-view'> + <div className='attachments-view-controllers'> + {(this.state.activeTab === tabsMapping.SETUP) && + <Icon + iconClassName={heatDataExist ? '' : 'disabled'} + className={heatDataExist ? '' : 'disabled'} + image='download' + label={i18n('Download HEAT')} + onClick={heatDataExist ? () => onDownload({heatCandidate: heatSetup, isReadOnlyMode, version}) : undefined} + data-test-id='download-heat'/>} + {(this.state.activeTab === tabsMapping.VALIDATION && softwareProductId) && + <Icon + iconClassName={this.props.goToOverview ? '' : 'disabled'} + className={`go-to-overview-icon ${this.props.goToOverview ? '' : 'disabled'}`} + labelClassName='go-to-overview-label' + onClick={this.props.goToOverview ? onGoToOverview : undefined} + image='go-to-overview' + label={i18n('Go to Overview')} + data-test-id='go-to-overview'/>} + <Icon + image='upload' + label={i18n('Upload New HEAT')} + className={isReadOnlyMode ? 'disabled' : ''} + iconClassName={isReadOnlyMode ? 'disabled' : ''} + onClick={evt => {this.refs.hiddenImportFileInput.click(evt);}} + data-test-id='upload-heat'/> + <input + ref='hiddenImportFileInput' + type='file' + name='fileInput' + accept='.zip' + onChange={evt => this.handleImport(evt)}/> </div> - <div onMouseDown={(e) => this.onChangeTreeWidth(e)} className='software-product-attachments-separator'/> - - <div className='software-product-attachments-error-list'> - {errorList.length ? this.renderErrorList(errorList) : <div className='no-errors'>{attachmentsTree.children ? - i18n('VALIDATION SUCCESS') : i18n('THERE IS NO HEAT DATA TO PRESENT') }</div>} - </div> - </div> - ); - } - - renderNode(node, path) { - let isFolder = node.children && node.children.length > 0; - let {onSelectNode} = this.props; - return ( - <div key={node.name} className='tree-block-inside'> - { - <div onDoubleClick={() => this.props.toggleExpanded(path)} className={this.getTreeRowClassName(node.name)}> - { - isFolder && - <div onClick={() => this.props.toggleExpanded(path)} className={classNames('tree-node-expander', {'tree-node-expander-collapsed': !node.expanded})}> - <FontAwesome name='caret-down'/> - </div> - } - { - - <span className='tree-node-icon'> - <FontAwesome name={typeToIcon[node.type]}/> - </span> - } - { - - <span onClick={() => onSelectNode(node.name)} className={this.getTreeTextClassName(node)}> - {node.name} - </span> - } - </div> - } - { - isFolder && - <Collapse in={node.expanded}> - <div className='tree-node-children'> - { - node.children.map((child, ind) => this.renderNode(child, [...path, ind])) - } - </div> - </Collapse> - } + <Tabs id='attachments-tabs' activeKey={this.state.activeTab} onSelect={key => this.handleTabPress(key)}> + <Tab eventKey={tabsMapping.SETUP} title='HEAT Setup'> + <HeatSetupComponent + heatDataExist={heatDataExist} + changeAttachmentsTab={tab => this.setState({activeTab: tab})} + onProcessAndValidate={onProcessAndValidate} + softwareProductId={softwareProductId} + isReadOnlyMode={isReadOnlyMode} + version={version}/> + </Tab> + <Tab eventKey={tabsMapping.VALIDATION} title='Heat Validation' disabled={!isValidationAvailable}> + <HeatValidation {...other}/> + </Tab> + </Tabs> </div> ); } - createErrorList(errorList, node, parent) { - if (node.errors) { - node.errors.forEach(error => errorList.push({ - error, - name: node.name, - parentName: parent.name, - type: node.type - })); + handleTabPress(key) { + let {heatSetup, heatSetupCache, onProcessAndValidate, isReadOnlyMode, version} = this.props; + switch (key) { + case tabsMapping.VALIDATION: + onProcessAndValidate({heatData: heatSetup, heatDataCache: heatSetupCache, isReadOnlyMode, version}).then( + () => this.setState({activeTab: tabsMapping.VALIDATION}) + ); + return; + case tabsMapping.SETUP: + this.setState({activeTab: tabsMapping.SETUP}); + return; } - if (node.children && node.children.length) { - node.children.map((child) => this.createErrorList(errorList, child, node)); - } - } - - renderErrorList(errors) { - let prevError = {}; - let {selectedNode} = this.props; - return errors.map(error => { - let isSameNodeError = error.name === prevError.name && error.parentName === prevError.parentName; - prevError = error; - - return ( - <div - key={error.name + error.errorMessage + error.parentName} - - onClick={() => this.selectNode(error.name)} - className={classNames('error-item', {'clicked': selectedNode === error.name, 'shifted': !isSameNodeError})}> - <span className={classNames('error-item-file-type', {'strong': !isSameNodeError})}> - { - error.hasParent ? - i18n('{type} {name} in {parentName}: ', { - type: nodeTypes[error.type], - name: error.name, - parentName: error.parentName - }) : - i18n('{type} {name}: ', { - type: nodeTypes[error.type], - name: error.name - }) - } - </span> - <span className={`error-item-file-type ${error.errorLevel}`}> {error.errorMessage} </span> - </div> - ); - }); } - selectNode(currentSelectedNode) { - let {onUnselectNode, onSelectNode, selectedNode} = this.props; - if (currentSelectedNode !== selectedNode) { - onSelectNode(currentSelectedNode); - }else{ - onUnselectNode(); - } - - } - - getTreeRowClassName(name) { - let {hoveredNode, selectedNode} = this.props; - return classNames({ - 'tree-node-row': true, - 'tree-node-selected': name === hoveredNode, - 'tree-node-clicked': name === selectedNode - }); + handleImport(evt) { + evt.preventDefault(); + let {version} = this.props; + let formData = new FormData(); + formData.append('upload', this.refs.hiddenImportFileInput.files[0]); + this.refs.hiddenImportFileInput.value = ''; + this.props.onUpload(formData, version); + this.setState({activeTab: tabsMapping.SETUP}); } - getTreeTextClassName(node) { - let {selectedNode} = this.props; - return classNames({ - 'tree-element-text': true, - 'error-status': node.errors, - 'error-status-selected': node.name === selectedNode - }); + save() { + return this.props.onSave(this.props.heatSetup, this.props.version); } - onChangeTreeWidth(e) { - if (e.button === mouseActions.MOUSE_BUTTON_CLICK) { - let onMouseMove = (e) => { - this.setState({treeWidth: e.clientX - leftPanelWidth}); - }; - let onMouseUp = () => { - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - }; - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - } - } } -export default SoftwareProductAttachmentsView; +export default HeatScreenView; diff --git a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetup.js b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetup.js new file mode 100644 index 0000000000..4c3adc6a7d --- /dev/null +++ b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetup.js @@ -0,0 +1,62 @@ +/*! + * 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 HeatSetupView from './HeatSetupView.jsx'; +import HeatSetupActionHelper from './HeatSetupActionHelper.js'; + + +const BASE = true; + +function baseExists(modules) { + for (let i in modules) { + if (modules[i].isBase) { + return true; + } + } + return false; +} + +export const mapStateToProps = ({softwareProduct: {softwareProductAttachments: {heatSetup, heatSetupCache}}}) => { + let {modules = [], unassigned = [], artifacts = [], nested = []} = heatSetup; + let isBaseExist = baseExists(modules); + + return { + heatSetupCache, + modules, + unassigned, + artifacts, + nested, + isBaseExist + }; +}; + +export const mapActionsToProps = (dispatch, {}) => { + return { + onModuleRename: (oldName, newName) => HeatSetupActionHelper.renameModule(dispatch, {oldName, newName}), + onModuleAdd: () => HeatSetupActionHelper.addModule(dispatch, !BASE), + onBaseAdd: () => HeatSetupActionHelper.addModule(dispatch, BASE), + onModuleDelete: moduleName => HeatSetupActionHelper.deleteModule(dispatch, moduleName), + onModuleFileTypeChange: ({module, value, type}) => HeatSetupActionHelper.changeModuleFileType(dispatch, { + module, + value, + type + }), + onArtifactListChange: artifacts => HeatSetupActionHelper.changeArtifactList(dispatch, artifacts), + onAddAllUnassigned: () => HeatSetupActionHelper.addAllUnassignedFilesToArtifacts(dispatch) + }; +}; + +export default connect(mapStateToProps, mapActionsToProps, null, {withRef: true})(HeatSetupView); diff --git a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupActionHelper.js b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupActionHelper.js new file mode 100644 index 0000000000..53143647a3 --- /dev/null +++ b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupActionHelper.js @@ -0,0 +1,77 @@ +/*! + * 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} from './HeatSetupConstants.js'; +import isEqual from 'lodash/isEqual.js'; +import cloneDeep from 'lodash/cloneDeep.js'; +import SoftwareProductActionHelper from 'sdc-app/onboarding/softwareProduct/SoftwareProductActionHelper.js'; +import i18n from 'nfvo-utils/i18n/i18n.js'; +import {actionTypes as modalActionTypes} from 'nfvo-components/modal/GlobalModalConstants.js'; + +export default { + + addModule(dispatch, isBase){ + dispatch({type: actionTypes.ADD_MODULE, data: {isBase}}); + }, + + deleteModule(dispatch, moduleName){ + dispatch({type: actionTypes.REMOVE_MODULE, data: {moduleName}}); + }, + + renameModule(dispatch, {oldName, newName}){ + dispatch({type: actionTypes.RENAME_MODULE, data: {oldName, newName}}); + }, + + changeModuleFileType(dispatch, {module, value, type}){ + if (!value) { + value = {value: ''}; + } + dispatch({type: actionTypes.FILE_ASSIGN_CHANGED, data: {module, value, type}}); + }, + + changeArtifactList(dispatch, artifacts){ + dispatch({type: actionTypes.ARTIFACT_LIST_CHANGE, data: {artifacts: artifacts.map(artifact => artifact.value)}}); + }, + + processAndValidateHeat(dispatch, {softwareProductId, heatData, heatDataCache, isReadOnlyMode, version}){ + return (isEqual({...heatData, softwareProductId}, heatDataCache) || isReadOnlyMode) ? Promise.resolve() : + SoftwareProductActionHelper.updateSoftwareProductHeatCandidate(dispatch, {softwareProductId, heatCandidate: heatData, version}) + .then(() => SoftwareProductActionHelper.processAndValidateHeatCandidate(dispatch, {softwareProductId, version})) + .then(() => dispatch({type: actionTypes.FILL_HEAT_SETUP_CACHE, payload: {...cloneDeep(heatData), softwareProductId}})); + }, + + addAllUnassignedFilesToArtifacts(dispatch){ + dispatch({type: actionTypes.ADD_ALL_UNASSIGNED_TO_ARTIFACTS}); + }, + + heatSetupLeaveConfirmation(dispatch, {softwareProductId, heatSetup, heatSetupCache}) { + return new Promise((resolve, reject) => { + if (isEqual({...heatSetup, softwareProductId}, heatSetupCache)) { + resolve(); + } else { + dispatch({ + type: modalActionTypes.GLOBAL_MODAL_WARNING, + data:{ + msg: i18n(`You have uploaded a new HEAT. If you navigate away or Check-in without proceeding to validation, + Old HEAT zip file will be in use. new HEAT will be ignored. Do you want to continue?`), + confirmationButtonText: i18n('Continue'), + onConfirmed: () => resolve(), + onDeclined: () => reject() + } + }); + } + }); + } +}; diff --git a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupConstants.js b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupConstants.js new file mode 100644 index 0000000000..2d6bd574a7 --- /dev/null +++ b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupConstants.js @@ -0,0 +1,42 @@ +/*! + * 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({ + + ARTIFACT_LIST_CHANGE: null, + ADD_ALL_UNASSIGNED_TO_ARTIFACTS: null, + ADD_ALL_ARTIFACTS_TO_UNASSIGNED: null, + + ADD_MODULE: null, + REMOVE_MODULE: null, + RENAME_MODULE: null, + FILL_HEAT_SETUP_CACHE: null, + FILE_ASSIGN_CHANGED: null, + + MANIFEST_LOADED: null, + + GO_TO_VALIDATION: null, + IN_VALIDATION: null + +}); + +export const fileTypes = { + YAML: {label: 'yaml', regex: /(yaml|yml)/g}, + ENV: {label: 'env', regex: /env/g}, + VOL: {label: 'vol', regex: /(yaml|yml)/g}, + VOL_ENV: {label: 'volEnv', regex: /env/g} +}; diff --git a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupReducer.js b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupReducer.js new file mode 100644 index 0000000000..f49339ce35 --- /dev/null +++ b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupReducer.js @@ -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 {actionTypes} from './HeatSetupConstants.js'; +import differenceWith from 'lodash/differenceWith.js'; + + +const emptyModule = (isBase, currentLength) => ({ + name: `${isBase ? 'base_' : 'module_'}${currentLength + 1}`, + isBase: isBase +}); + +function syncUnassignedFilesWithArtifactsChanges(unassigned, artifacts, oldArtifacts) { + if (artifacts.length > oldArtifacts.length) { + return differenceWith(unassigned, artifacts, (unassignedFile, artifact) => unassignedFile === artifact); + } + else { + const removedArtifact = differenceWith(oldArtifacts, artifacts, (oldArtifact, artifact) => artifact === oldArtifact); + return [...unassigned, removedArtifact[0]]; + } +} + +function findModuleIndexByName(modules, name) { + return modules.findIndex(module => module.name === name); +} + +function addDeletedModuleFilesToUnassigned(unassigned, deletedModule){ + let files = []; + for(let i in deletedModule){ + if (deletedModule.hasOwnProperty(i)) { + if (typeof deletedModule[i] === 'string' && deletedModule[i] && i !== 'name') { + files.push(deletedModule[i]); + } + } + } + + return unassigned.concat(files); +} + +export default (state = {}, action) => { + switch (action.type) { + case actionTypes.MANIFEST_LOADED: + return { + ...state, + ...action.response, + modules: action.response.modules.map(module => ({...module, name: module.name || module.yaml.substring(0, module.yaml.lastIndexOf('.'))})) + }; + case actionTypes.ARTIFACT_LIST_CHANGE: + return { + ...state, + artifacts: action.data.artifacts, + unassigned: syncUnassignedFilesWithArtifactsChanges(state.unassigned, action.data.artifacts, state.artifacts) + }; + case actionTypes.ADD_ALL_UNASSIGNED_TO_ARTIFACTS: + return { + ...state, + artifacts: [...state.artifacts,...state.unassigned], + unassigned: [] + }; + case actionTypes.ADD_ALL_ARTIFACTS_TO_UNASSIGNED: + return { + ...state, + artifacts: [], + unassigned: [...state.unassigned, ...state.artifacts] + }; + case actionTypes.ADD_MODULE: + return { + ...state, + modules: state.modules.concat({...emptyModule(action.data.isBase, state.modules.length)}) + }; + case actionTypes.REMOVE_MODULE: + const moduleIndexToDelete = findModuleIndexByName(state.modules, action.data.moduleName); + let unassigned = addDeletedModuleFilesToUnassigned(state.unassigned, state.modules[moduleIndexToDelete]); + return { + ...state, + unassigned, + modules: [...state.modules.slice(0, moduleIndexToDelete), ...state.modules.slice(moduleIndexToDelete + 1)] + }; + case actionTypes.RENAME_MODULE: + const indexToRename = findModuleIndexByName(state.modules, action.data.oldName); + let moduleToRename = state.modules[indexToRename]; + moduleToRename.name = action.data.newName; + return { + ...state, + modules: [...state.modules.slice(0, indexToRename), moduleToRename, ...state.modules.slice(indexToRename + 1)] + }; + case actionTypes.FILE_ASSIGN_CHANGED: + let {module, value:{value}, type} = action.data; + const moduleIndexToModify = findModuleIndexByName(state.modules, module.name); + let moduleToModify = state.modules[moduleIndexToModify]; + let dumpedFile = moduleToModify[type]; + if (dumpedFile !== value) { + if(value) { + moduleToModify[type] = value; + } + else { + delete moduleToModify[type]; + } + const newUnassignedList = dumpedFile ? [...state.unassigned.filter(file => file !== value), dumpedFile] : state.unassigned.filter(file => file !== value); + return { + ...state, + modules: [...state.modules.slice(0, moduleIndexToModify), moduleToModify, ...state.modules.slice(moduleIndexToModify + 1)], + unassigned: newUnassignedList + }; + } + else { + return state; + } + default: + return state; + } +}; diff --git a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupView.jsx b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupView.jsx new file mode 100644 index 0000000000..0d8bc58361 --- /dev/null +++ b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupView.jsx @@ -0,0 +1,328 @@ +/*! + * 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 Button from 'react-bootstrap/lib/Button.js'; +import Tooltip from 'react-bootstrap/lib/Tooltip.js'; +import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger.js'; +import FormControl from 'react-bootstrap/lib/FormControl.js'; +import i18n from 'nfvo-utils/i18n/i18n.js'; +import SelectInput from 'nfvo-components/input/SelectInput.jsx'; +import Icon from 'nfvo-components/icon/Icon.jsx'; +import SVGIcon from 'nfvo-components/icon/SVGIcon.jsx'; +import {fileTypes} from './HeatSetupConstants.js'; +import {tabsMapping} from '../SoftwareProductAttachmentsConstants.js'; +import {sortable} from 'react-sortable'; + +class ListItem extends Component { + + render() { + return ( + <li {...this.props}>{this.props.children}</li> + ); + } +} + + +const SortableListItem = sortable(ListItem); + +class SortableModuleFileList extends Component { + + state = { + draggingIndex: null, + data: this.props.modules + }; + + + componentWillReceiveProps(nextProps) { + this.setState({data: nextProps.modules}); + } + + render() { + + let {unassigned, onModuleRename, onModuleDelete, onModuleAdd, onBaseAdd, onModuleFileTypeChange, isBaseExist} = this.props; + const childProps = module => ({ + module, + onModuleRename, + onModuleDelete, + onModuleFileTypeChange: (value, type) => onModuleFileTypeChange({module, value, type}), + files: unassigned + }); + let listItems = this.state.data.map(function (item, i) { + return ( + <SortableListItem + key={i} + updateState={data => this.setState(data)} + items={this.state.data} + draggingIndex={this.state.draggingIndex} + sortId={i} + outline='list'><ModuleFile {...childProps(item)} /></SortableListItem> + ); + }, this); + + return ( + <div className='modules-list-wrapper'> + <div className='modules-list-header'> + <div className='modules-list-controllers'> + {!isBaseExist && <Button bsStyle='link' onClick={onBaseAdd} disabled={unassigned.length === 0}>{i18n('Add Base')}</Button>} + <Button bsStyle='link' onClick={onModuleAdd} disabled={unassigned.length === 0}>{i18n('Add Module')}</Button> + </div> + </div> + <ul>{listItems}</ul> + </div> + ); + } +} + +const tooltip = (name) => <Tooltip id='tooltip-bottom'>{name}</Tooltip>; +const UnassignedFileList = (props) => { + return ( + <div className='unassigned-files'> + <div className='unassigned-files-title'>{i18n('UNASSIGNED FILES')}</div> + <div className='unassigned-files-list'>{props.children}</div> + </div> + ); +}; + +const EmptyListContent = props => { + let {onClick, heatDataExist} = props; + let displayText = heatDataExist ? 'All Files Are Assigned' : ''; + return ( + <div className='go-to-validation-button-wrapper'> + <div className='all-files-assigned'>{i18n(displayText)}</div> + {heatDataExist && <div className={'link'} onClick={onClick} data-test-id='go-to-validation'>{i18n('Proceed To Validation')}<SVGIcon name='angle-right'/></div>} + </div> + ); +}; +const UnassignedFile = (props) => ( + <OverlayTrigger placement='bottom' overlay={tooltip(props.name)} delayShow={1000}> + <li data-test-id='unassigned-files' className='unassigned-files-list-item'>{props.name}</li> + </OverlayTrigger> +); + +const AddOrDeleteVolumeFiels = ({add = true, onAdd, onDelete}) => { + const displayText = add ? 'Add Volume Files' : 'Delete Volume Files'; + const action = add ? onAdd : onDelete; + return ( + <div className='add-or-delete-volumes' onClick={action}> + <SVGIcon name={add ? 'plus' : 'close'} /> + <span>{i18n(displayText)}</span> + </div> + ); +}; + +const SelectWithFileType = ({type, selected, files, onChange}) => { + + let filteredFiledAccordingToType = files.filter(file => file.label.search(type.regex) > -1); + if (selected) { + filteredFiledAccordingToType = filteredFiledAccordingToType.concat({label: selected, value: selected}); + } + + return ( + <SelectInput + data-test-id={`${type.label}-list`} + label={type.label} + value={selected} + onChange={value => value !== selected && onChange(value, type.label)} + disabled={filteredFiledAccordingToType.length === 0} + placeholder={filteredFiledAccordingToType.length === 0 ? '' : undefined} + clearable={true} + options={filteredFiledAccordingToType} /> + ); +}; + +class NameEditInput extends Component { + componentDidMount() { + this.input.focus(); + } + + render() { + return ( + <FormControl {...this.props} className='name-edit' inputRef={input => this.input = input}/> + ); + } +} + +class ModuleFile extends Component { + constructor(props) { + super(props); + this.state = { + isInNameEdit: false, + displayVolumes: Boolean(props.module.vol || props.module.volEnv) + }; + } + + handleSubmit(event, name) { + if (event.keyCode === 13) { + this.handleModuleRename(event, name); + } + } + + componentWillReceiveProps(nextProps) { + this.setState({displayVolumes: Boolean(nextProps.module.vol || nextProps.module.volEnv)}); + } + + handleModuleRename(event, name) { + this.setState({isInNameEdit: false}); + this.props.onModuleRename(name, event.target.value); + } + + deleteVolumeFiles() { + const { onModuleFileTypeChange} = this.props; + onModuleFileTypeChange(null, fileTypes.VOL.label); + onModuleFileTypeChange(null, fileTypes.VOL_ENV.label); + this.setState({displayVolumes: false}); + } + + renderNameAccordingToEditState() { + const {module: {name}} = this.props; + if (this.state.isInNameEdit) { + return (<NameEditInput defaultValue={name} onBlur={evt => this.handleModuleRename(evt, name)} onKeyDown={evt => this.handleSubmit(evt, name)}/>); + } + return (<span className='filename-text'>{name}</span>); + } + + render() { + const {module: {name, isBase, yaml, env, vol, volEnv}, onModuleDelete, files, onModuleFileTypeChange} = this.props; + const {displayVolumes} = this.state; + const moduleType = isBase ? 'BASE' : 'MODULE'; + return ( + <div className='modules-list-item' data-test-id='module-item'> + <div className='modules-list-item-controllers'> + <div className='modules-list-item-filename'> + <Icon image={isBase ? 'base' : 'module'} iconClassName='heat-setup-module-icon' /> + <span className='module-title-by-type'>{`${moduleType}: `}</span> + <div className={`text-and-icon ${this.state.isInNameEdit ? 'in-edit' : ''}`}> + {this.renderNameAccordingToEditState()} + {!this.state.isInNameEdit && <SVGIcon + name='pencil' + onClick={() => this.setState({isInNameEdit: true})} + data-test-id={isBase ? 'base-name' : 'module-name'}/>} + </div> + </div> + <SVGIcon name='trash-o' onClick={() => onModuleDelete(name)} data-test-id='module-delete'/> + </div> + <div className='modules-list-item-selectors'> + <SelectWithFileType + type={fileTypes.YAML} + files={files} + selected={yaml} + onChange={onModuleFileTypeChange}/> + <SelectWithFileType + type={fileTypes.ENV} + files={files} + selected={env} + onChange={onModuleFileTypeChange}/> + {displayVolumes && <SelectWithFileType + type={fileTypes.VOL} + files={files} + selected={vol} + onChange={onModuleFileTypeChange}/>} + {displayVolumes && <SelectWithFileType + type={fileTypes.VOL_ENV} + files={files} + selected={volEnv} + onChange={onModuleFileTypeChange}/>} + <AddOrDeleteVolumeFiels onAdd={() => this.setState({displayVolumes: true})} onDelete={() => this.deleteVolumeFiles()} add={!displayVolumes}/> + </div> + </div> + ); + } +} + +class ArtifactOrNestedFileList extends Component { + + render() { + let {type, title, selected, options, onSelectChanged, onAddAllUnassigned} = this.props; + return ( + <div className={`artifact-files ${type === 'nested' ? 'nested' : ''}`}> + <div className='artifact-files-header'> + <span> + {type === 'artifact' && (<Icon image='artifacts' iconClassName='heat-setup-module-icon' />)} + {`${title}`} + </span> + {type === 'artifact' && <span className='add-all-unassigned' onClick={onAddAllUnassigned}>{i18n('Add All Unassigned Files')}</span>} + </div> + {type === 'nested' ? ( + <ul className='nested-list'>{selected.map(nested => + <li key={nested} className='nested-list-item'>{nested}</li> + )}</ul>) : + (<SelectInput + options={options} + onMultiSelectChanged={onSelectChanged || (() => { + })} + value={selected} + clearable={false} + placeholder={i18n('Add Artifact')} + multi/>) + } + </div> + ); + } +} + +const buildLabelValueObject = str => (typeof str === 'string' ? {value: str, label: str} : str); + +class SoftwareProductHeatSetupView extends Component { + + processAndValidateHeat(heatData, heatDataCache){ + let {onProcessAndValidate, changeAttachmentsTab, version} = this.props; + onProcessAndValidate({heatData, heatDataCache, version}).then( + () => changeAttachmentsTab(tabsMapping.VALIDATION) + ); + } + + render() { + let {modules, heatSetupCache, isReadOnlyMode, heatDataExist, unassigned, artifacts, nested, onArtifactListChange, onAddAllUnassigned} = this.props; + + const formattedUnassigned = unassigned.map(buildLabelValueObject); + const formattedArtifacts = artifacts.map(buildLabelValueObject); + return ( + <div className={`heat-setup-view ${isReadOnlyMode ? 'disabled' : ''}`}> + <div className='heat-setup-view-modules-and-artifacts'> + <SortableModuleFileList + {...this.props} + artifacts={formattedArtifacts} + unassigned={formattedUnassigned}/> + <ArtifactOrNestedFileList + type={'artifact'} + title={i18n('ARTIFACTS')} + options={formattedUnassigned} + selected={formattedArtifacts} + onSelectChanged={onArtifactListChange} + onAddAllUnassigned={onAddAllUnassigned}/> + <ArtifactOrNestedFileList + type={'nested'} + title={i18n('NESTED HEAT FILES')} + options={[]} + selected={nested}/> + </div> + <UnassignedFileList> + { + formattedUnassigned.length > 0 ? + (<ul>{formattedUnassigned.map(file => <UnassignedFile key={file.label} name={file.label}/>)}</ul>) + : + (<EmptyListContent + heatDataExist={heatDataExist} + onClick={() => this.processAndValidateHeat({modules, unassigned, artifacts, nested}, heatSetupCache)}/>) + } + </UnassignedFileList> + </div> + ); + } + +} + +export default SoftwareProductHeatSetupView; diff --git a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidation.js b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidation.js new file mode 100644 index 0000000000..21f6e6c77f --- /dev/null +++ b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidation.js @@ -0,0 +1,51 @@ +/*! + * 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 HeatValidationView from './HeatValidationView.jsx'; +import HeatValidationActionHelper from './HeatValidationActionHelper.js'; +import {errorLevels, nodeFilters} from './HeatValidationConstants.js'; + +export const mapStateToProps = ({softwareProduct: {softwareProductAttachments: {heatValidation}}}) => { + let {attachmentsTree, selectedNode, errorList} = heatValidation; + let currentErrors = [], currentWarnings = []; + if (errorList) { + for (let i = 0 ; i < errorList.length ; i++) { + if (errorList[i].level === errorLevels.ERROR && (errorList[i].name === selectedNode || selectedNode === nodeFilters.ALL)) { + currentErrors[currentErrors.length] = errorList[i]; + } + if (errorList[i].level === errorLevels.WARNING && (errorList[i].name === selectedNode || selectedNode === nodeFilters.ALL)) { + currentWarnings[currentWarnings.length] = errorList[i]; + } + } + } + return { + attachmentsTree, + selectedNode, + errorList, + currentErrors, + currentWarnings + }; +}; + +const mapActionsToProps = (dispatch) => { + return { + toggleExpanded: (path) => HeatValidationActionHelper.toggleExpanded(dispatch, {path}), + onSelectNode: (nodeName) => HeatValidationActionHelper.onSelectNode(dispatch, {nodeName}), + onDeselectNode: () => HeatValidationActionHelper.onDeselectNode(dispatch) + }; +}; + +export default connect(mapStateToProps, mapActionsToProps, null, {withRef: true})(HeatValidationView); diff --git a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsActionHelper.js b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationActionHelper.js index a7f7a5173b..73366c20cc 100644 --- a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsActionHelper.js +++ b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationActionHelper.js @@ -1,24 +1,19 @@ -/*- - * ============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 './SoftwareProductAttachmentsConstants.js'; +import {actionTypes} from './HeatValidationConstants.js'; export default { @@ -36,7 +31,7 @@ export default { }); }, - onUnselectNode(dispatch) { + onDeselectNode(dispatch) { dispatch({ type: actionTypes.UNSELECTED_NODE }); diff --git a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationConstants.js b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationConstants.js new file mode 100644 index 0000000000..f783fe6482 --- /dev/null +++ b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationConstants.js @@ -0,0 +1,57 @@ +/*! + * 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'; +import i18n from 'nfvo-utils/i18n/i18n.js'; + +export const actionTypes = keyMirror({ + TOGGLE_EXPANDED: null, + SELECTED_NODE: null, + UNSELECTED_NODE: null +}); + +export const errorTypes = keyMirror({ + MISSING_FILE_IN_ZIP: i18n('missing file in zip'), + MISSING_FILE_IN_MANIFEST: i18n('missing file in manifest'), + MISSING_OR_ILLEGAL_FILE_TYPE_IN_MANIFEST: i18n('missing or illegal file type in manifest'), + FILE_IS_YML_WITHOUT_YML_EXTENSION: i18n('file is defined as a heat file but it doesn\'t have .yml or .yaml extension'), + FILE_IS_ENV_WITHOUT_ENV_EXTENSION: i18n('file is defined as an env file but it doesn\'t have .env extension'), + ILLEGAL_YAML_FILE_CONTENT: i18n('illegal yaml file content'), + ILLEGAL_HEAT_YAML_FILE_CONTENT: i18n('illegal HEAT yaml file content'), + MISSING_FILE_NAME_IN_MANIFEST: i18n('a file is written in manifest without file name'), + MISSING_ENV_FILE_IN_ZIP: i18n('missing env file in zip'), + ARTIFACT_NOT_IN_USE: i18n('artifact not in use') +}); + +export const errorLevels = keyMirror({ + WARNING: 'WARNING', + ERROR: 'ERROR' +}); +export const nodeFilters = keyMirror({ + ALL: 'All' +}); +export const nodeTypes = keyMirror({ + heat: i18n('Heat'), + volume: i18n('Volume'), + network: i18n('Network'), + artifact: i18n('Artifact'), + env: i18n('Environment'), + other: i18n('') +}); + +export const mouseActions = keyMirror({ + MOUSE_BUTTON_CLICK: 0 +}); + diff --git a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsReducer.js b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationReducer.js index 5c5567b032..f0c10ed457 100644 --- a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsReducer.js +++ b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationReducer.js @@ -1,25 +1,20 @@ -/*- - * ============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 as softwareProductsActionTypes} from 'sdc-app/onboarding/softwareProduct/SoftwareProductConstants.js'; -import {actionTypes} from './SoftwareProductAttachmentsConstants.js'; +import {actionTypes, nodeFilters} from './HeatValidationConstants.js'; const mapVolumeData = ({fileName, env, errors}) => ({ name: fileName, @@ -80,7 +75,7 @@ const mapHeatData = ({fileName, env, nested, volume, network, artifacts, errors, function createErrorList(node, parent, deep = 0, errorList = []) { if (node.errors) { errorList.push(...node.errors.map((error) => ({ - errorLevel: error.level, + level: error.level, errorMessage: error.message, name: node.name, hasParent: deep > 2, @@ -95,14 +90,15 @@ function createErrorList(node, parent, deep = 0, errorList = []) { } const mapValidationDataToTree = validationData => { - let {HEAT, volume, network, artifacts, other} = validationData.importStructure || {}; + let {heat, volume, network, artifacts, other} = validationData.importStructure || {}; return { children: [ { name: 'HEAT', expanded: true, type: 'heat', - children: (HEAT ? HEAT.map(mapHeatData) : []) + header: true, + children: (heat ? heat.map(mapHeatData) : []) }, ...(artifacts ? [{ name: 'artifacts', @@ -174,7 +170,8 @@ export default (state = {attachmentsTree: {}}, action) => { return { ...state, attachmentsTree, - errorList + errorList, + selectedNode: nodeFilters.ALL }; case actionTypes.TOGGLE_EXPANDED: return { @@ -191,7 +188,7 @@ export default (state = {attachmentsTree: {}}, action) => { case actionTypes.UNSELECTED_NODE: return { ...state, - selectedNode: undefined + selectedNode: nodeFilters.ALL }; default: return state; diff --git a/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationView.jsx b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationView.jsx new file mode 100644 index 0000000000..25ad90f351 --- /dev/null +++ b/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationView.jsx @@ -0,0 +1,274 @@ +/*! + * 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'; +import classNames from 'classnames'; +import Collapse from 'react-bootstrap/lib/Collapse.js'; +import Icon from 'nfvo-components/icon/Icon.jsx'; +import SVGIcon from 'nfvo-components/icon/SVGIcon.jsx'; +import i18n from 'nfvo-utils/i18n/i18n.js'; +import {mouseActions, errorLevels, nodeFilters} from './HeatValidationConstants.js'; + +const leftPanelWidth = 250; +const typeToIcon = Object.freeze({ + heat: 'heat', + volume: 'volume', + network: 'network', + artifact: 'validation-artifacts', + env: 'env', + other: 'validation-other' +}); + + +class HeatValidationView extends Component { + + static propTypes = { + attachmentsTree: PropTypes.object.isRequired, + errorList: PropTypes.array.isRequired, + currentErrors: PropTypes.array.isRequired, + currentWarnings: PropTypes.array.isRequired, + onSelectNode: PropTypes.func.isRequired, + onDeselectNode: PropTypes.func.isRequired, + toggleExpanded: PropTypes.func.isRequired, + selectedNode: PropTypes.string + }; + + render() { + return (<div className='vsp-attachments-heat-validation' data-test-id='heat-validation-editor'> + <HeatFileTree errorList={this.props.errorList} attachmentsTree={this.props.attachmentsTree} + onSelectNode={this.props.onSelectNode} toggleExpanded={this.props.toggleExpanded} + selectedNode={this.props.selectedNode} onDeselectNode={this.props.onDeselectNode} /> + <HeatMessageBoard errors={this.props.currentErrors} warnings={this.props.currentWarnings} selectedNode={this.props.selectedNode} /> + </div> ); + } +} + +function HeatFileTreeRow(props) { + let {node, path, toggleExpanded, selectedNode, selectNode} = props; + let isFolder = node.children && node.children.length > 0; + return ( + <div onDoubleClick={() => toggleExpanded(path)} className={classNames({ + 'tree-node-row': true, + 'tree-node-clicked': node.name === props.selectedNode + })} data-test-id='validation-tree-node'> + <div className='name-section'> + { + isFolder && + <div onClick={() => toggleExpanded(path)} + className='tree-node-expander'> + <SVGIcon name={!node.expanded ? 'chevron-up' : 'chevron-down'} data-test-id='validation-tree-block-toggle'/> + </div> + } + { + + <span className='tree-node-icon'> + <Icon image={typeToIcon[node.type]} iconClassName={selectedNode === node.name ? 'selected' : ''}/> + </span> + } + { + + <span className='tree-node-name' onClick={() => selectNode(node.name)} data-test-id='validation-tree-node-name'> + {node.name ? node.name : 'UNKNOWN'} + </span> + } + </div> + <ErrorsAndWarningsCount errorList={node.errors} onClick={() => selectNode(node.name)} /> + </div>); +} + +function HeatFileTreeHeader(props) { + let hasErrors = props.errorList.filter(error => error.level === errorLevels.ERROR).length > 0; + return ( + <div onClick={() => props.selectNode(nodeFilters.ALL)} className={classNames({'attachments-tree-header': true, + 'header-selected' : props.selectedNode === nodeFilters.ALL})} data-test-id='validation-tree-header'> + <div className='tree-header-title' > + <Icon image='zip' iconClassName={classNames(props.selectedNode === nodeFilters.ALL ? 'selected' : '', 'header-icon')} /> + <span className={classNames({'tree-header-title-text' : true, + 'tree-header-title-selected' : props.selectedNode === nodeFilters.ALL})}>{i18n(`HEAT${hasErrors ? ' (Draft)' : ''}`)}</span> + </div> + <ErrorsAndWarningsCount errorList={props.errorList} size='large' /> + </div>); +} + +class HeatFileTree extends React.Component { + static propTypes = { + attachmentsTree: PropTypes.object.isRequired, + errorList: PropTypes.array.isRequired, + onSelectNode: PropTypes.func.isRequired, + onDeselectNode: PropTypes.func.isRequired, + toggleExpanded: PropTypes.func.isRequired, + selectedNode: PropTypes.string + }; + state = { + treeWidth: '400' + }; + render() { + let {attachmentsTree} = this.props; + return ( + <div className='validation-tree-section' style={{'width' : this.state.treeWidth + 'px'}}> + <div className='vsp-attachments-heat-validation-tree'> + <div className='tree-wrapper'> + {attachmentsTree && attachmentsTree.children && attachmentsTree.children.map((child, ind) => this.renderNode(child, [ind]))} + </div> + </div> + <div onMouseDown={(e) => this.onChangeTreeWidth(e)} + className='vsp-attachments-heat-validation-separator' data-test-id='validation-tree-separator'></div> + </div>); + } + renderNode(node, path) { + let rand = Math.random() * (3000 - 1) + 1; + let isFolder = node.children && node.children.length > 0; + let {selectedNode} = this.props; + return ( + <div key={node.name + rand} className={classNames({'tree-block-inside' : !node.header})}> + { + node.header ? + <HeatFileTreeHeader selectedNode={selectedNode} errorList={this.props.errorList} selectNode={(nodeName) => this.selectNode(nodeName)} /> : + <HeatFileTreeRow toggleExpanded={this.props.toggleExpanded} node={node} path={path} selectedNode={selectedNode} selectNode={() => this.selectNode(node.name)} /> + } + { + isFolder && + <Collapse in={node.expanded}> + <div className='tree-node-children'> + { + node.children.map((child, ind) => this.renderNode(child, [...path, ind])) + } + </div> + </Collapse> + } + </div> + ); + } + + + + + + selectNode(currentSelectedNode) { + let {onDeselectNode, onSelectNode, selectedNode} = this.props; + if (currentSelectedNode !== selectedNode) { + onSelectNode(currentSelectedNode); + } else { + onDeselectNode(); + } + + + + } + + onChangeTreeWidth(e) { + if (e.button === mouseActions.MOUSE_BUTTON_CLICK) { + let onMouseMove = (e) => { + this.setState({treeWidth: e.clientX - leftPanelWidth}); + }; + let onMouseUp = () => { + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + }; + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + } + } +} + +class HeatMessageBoard extends Component { + static propTypes = { + currentErrors: PropTypes.array, + currentWarnings: PropTypes.array, + selectedNode: PropTypes.string + }; + render() { + let {errors, warnings} = this.props; + let allItems = [...errors, ...warnings]; + return ( + <div className='message-board-section'> + { allItems.map(error => this.renderError(error)) } + </div> + ); + } + renderError(error) { + let rand = Math.random() * (3000 - 1) + 1; + return ( + <div + key={error.name + error.errorMessage + error.parentName + rand} + className='error-item' data-test-id='validation-error'> + {error.level === errorLevels.WARNING ? + <SVGIcon name='exclamation-triangle-line' iconClassName='large' /> : <Icon image='error-lg' /> } + <span className='error-item-file-type'> + { + (this.props.selectedNode === nodeFilters.ALL) ? + <span> + <span className='error-file-name'> + {i18n('{errorName}:', { + errorName: error.name + })} + </span> + <span> + {i18n('{message}', {message: error.errorMessage})} + </span> + </span> : + i18n('{errorMsg}', { + errorMsg: error.errorMessage + }) + } + </span> + </div> + ); + } +} +class ErrorsAndWarningsCount extends Component { + static propTypes = { + errorList: PropTypes.array, + size: PropTypes.string + }; + render() { + let errors = this.getErrorsAndWarningsCount(this.props.errorList); + if (!errors) { + return null; + } + let errIcon = 'error'; + let {size} = this.props; + if (size && size === 'large') { + errIcon += '-lg'; + } + return (<div className='counters'> + {(errors.errorCount > 0) && <div className='counter'> + <Icon image={errIcon} iconClassName='counter-icon'/> + <div className={'error-text ' + (size ? size : '')} data-test-id='validation-error-count'>{errors.errorCount}</div> + </div>} + {(errors.warningCount > 0) && <div className='counter'> + <SVGIcon name='exclamation-triangle-line' iconClassName={size} /> + <div className={'warning-text ' + (size ? size : '')} data-test-id='validation-warning-count'>{errors.warningCount}</div> + </div>} + </div>); + } + getErrorsAndWarningsCount(errorList) { + let errorCount = 0, warningCount = 0; + if (errorList && errorList.length > 0) { + for (let i = 0; i < errorList.length; i++) { + if (errorList[i].level === errorLevels.ERROR) { + errorCount++; + } else if (errorList[i].level === errorLevels.WARNING) { + warningCount++; + } + } + } + if (errorCount === 0 && warningCount === 0) { + return null; + } + return {errorCount, warningCount}; + } +} +export default HeatValidationView; |