summaryrefslogtreecommitdiffstats
path: root/openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments
diff options
context:
space:
mode:
Diffstat (limited to 'openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments')
-rw-r--r--openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachments.js95
-rw-r--r--openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsConstants.js60
-rw-r--r--openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsUtils.js25
-rw-r--r--openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsView.jsx253
-rw-r--r--openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetup.js62
-rw-r--r--openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupActionHelper.js77
-rw-r--r--openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupConstants.js42
-rw-r--r--openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupReducer.js124
-rw-r--r--openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/setup/HeatSetupView.jsx328
-rw-r--r--openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidation.js51
-rw-r--r--openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationActionHelper.js (renamed from openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsActionHelper.js)25
-rw-r--r--openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationConstants.js57
-rw-r--r--openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationReducer.js (renamed from openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/SoftwareProductAttachmentsReducer.js)35
-rw-r--r--openecomp-ui/src/sdc-app/onboarding/softwareProduct/attachments/validation/HeatValidationView.jsx274
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;