2 * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13 * or implied. See the License for the specific language governing
14 * permissions and limitations under the License.
16 import React, {Component} from 'react';
17 import Button from 'react-bootstrap/lib/Button.js';
18 import Tooltip from 'react-bootstrap/lib/Tooltip.js';
19 import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger.js';
20 import FormControl from 'react-bootstrap/lib/FormControl.js';
21 import i18n from 'nfvo-utils/i18n/i18n.js';
22 import SelectInput from 'nfvo-components/input/SelectInput.jsx';
23 import Icon from 'nfvo-components/icon/Icon.jsx';
24 import SVGIcon from 'nfvo-components/icon/SVGIcon.jsx';
25 import {fileTypes} from './HeatSetupConstants.js';
26 import {tabsMapping} from '../SoftwareProductAttachmentsConstants.js';
27 import {sortable} from 'react-sortable';
29 class ListItem extends Component {
33 <li {...this.props}>{this.props.children}</li>
39 const SortableListItem = sortable(ListItem);
41 class SortableModuleFileList extends Component {
45 data: this.props.modules
49 componentWillReceiveProps(nextProps) {
50 this.setState({data: nextProps.modules});
55 let {unassigned, onModuleRename, onModuleDelete, onModuleAdd, onBaseAdd, onModuleFileTypeChange, isBaseExist} = this.props;
56 const childProps = module => ({
60 onModuleFileTypeChange: (value, type) => onModuleFileTypeChange({module, value, type}),
63 let listItems = this.state.data.map(function (item, i) {
67 updateState={data => this.setState(data)}
68 items={this.state.data}
69 draggingIndex={this.state.draggingIndex}
71 outline='list'><ModuleFile {...childProps(item)} /></SortableListItem>
76 <div className='modules-list-wrapper'>
77 <div className='modules-list-header'>
78 <div className='modules-list-controllers'>
79 {!isBaseExist && <Button bsStyle='link' onClick={onBaseAdd} disabled={unassigned.length === 0}>{i18n('Add Base')}</Button>}
80 <Button bsStyle='link' onClick={onModuleAdd} disabled={unassigned.length === 0}>{i18n('Add Module')}</Button>
89 const tooltip = (name) => <Tooltip id='tooltip-bottom'>{name}</Tooltip>;
90 const UnassignedFileList = (props) => {
92 <div className='unassigned-files'>
93 <div className='unassigned-files-title'>{i18n('UNASSIGNED FILES')}</div>
94 <div className='unassigned-files-list'>{props.children}</div>
99 const EmptyListContent = props => {
100 let {onClick, heatDataExist} = props;
101 let displayText = heatDataExist ? 'All Files Are Assigned' : '';
103 <div className='go-to-validation-button-wrapper'>
104 <div className='all-files-assigned'>{i18n(displayText)}</div>
105 {heatDataExist && <div className={'link'} onClick={onClick} data-test-id='go-to-validation'>{i18n('Proceed To Validation')}<SVGIcon name='angle-right'/></div>}
109 const UnassignedFile = (props) => (
110 <OverlayTrigger placement='bottom' overlay={tooltip(props.name)} delayShow={1000}>
111 <li data-test-id='unassigned-files' className='unassigned-files-list-item'>{props.name}</li>
115 const AddOrDeleteVolumeFiels = ({add = true, onAdd, onDelete}) => {
116 const displayText = add ? 'Add Volume Files' : 'Delete Volume Files';
117 const action = add ? onAdd : onDelete;
119 <div className='add-or-delete-volumes' onClick={action}>
120 <SVGIcon name={add ? 'plus' : 'close'} />
121 <span>{i18n(displayText)}</span>
126 const SelectWithFileType = ({type, selected, files, onChange}) => {
128 let filteredFiledAccordingToType = files.filter(file => file.label.search(type.regex) > -1);
130 filteredFiledAccordingToType = filteredFiledAccordingToType.concat({label: selected, value: selected});
135 data-test-id={`${type.label}-list`}
138 onChange={value => value !== selected && onChange(value, type.label)}
139 disabled={filteredFiledAccordingToType.length === 0}
140 placeholder={filteredFiledAccordingToType.length === 0 ? '' : undefined}
142 options={filteredFiledAccordingToType} />
146 class NameEditInput extends Component {
147 componentDidMount() {
153 <FormControl {...this.props} className='name-edit' inputRef={input => this.input = input}/>
158 class ModuleFile extends Component {
163 displayVolumes: Boolean(props.module.vol || props.module.volEnv)
167 handleSubmit(event, name) {
168 if (event.keyCode === 13) {
169 this.handleModuleRename(event, name);
173 componentWillReceiveProps(nextProps) {
174 this.setState({displayVolumes: Boolean(nextProps.module.vol || nextProps.module.volEnv)});
177 handleModuleRename(event, name) {
178 this.setState({isInNameEdit: false});
179 this.props.onModuleRename(name, event.target.value);
182 deleteVolumeFiles() {
183 const { onModuleFileTypeChange} = this.props;
184 onModuleFileTypeChange(null, fileTypes.VOL.label);
185 onModuleFileTypeChange(null, fileTypes.VOL_ENV.label);
186 this.setState({displayVolumes: false});
189 renderNameAccordingToEditState() {
190 const {module: {name}} = this.props;
191 if (this.state.isInNameEdit) {
192 return (<NameEditInput defaultValue={name} onBlur={evt => this.handleModuleRename(evt, name)} onKeyDown={evt => this.handleSubmit(evt, name)}/>);
194 return (<span className='filename-text'>{name}</span>);
198 const {module: {name, isBase, yaml, env, vol, volEnv}, onModuleDelete, files, onModuleFileTypeChange} = this.props;
199 const {displayVolumes} = this.state;
200 const moduleType = isBase ? 'BASE' : 'MODULE';
202 <div className='modules-list-item' data-test-id='module-item'>
203 <div className='modules-list-item-controllers'>
204 <div className='modules-list-item-filename'>
205 <Icon image={isBase ? 'base' : 'module'} iconClassName='heat-setup-module-icon' />
206 <span className='module-title-by-type'>{`${moduleType}: `}</span>
207 <div className={`text-and-icon ${this.state.isInNameEdit ? 'in-edit' : ''}`}>
208 {this.renderNameAccordingToEditState()}
209 {!this.state.isInNameEdit && <SVGIcon
211 onClick={() => this.setState({isInNameEdit: true})}
212 data-test-id={isBase ? 'base-name' : 'module-name'}/>}
215 <SVGIcon name='trash-o' onClick={() => onModuleDelete(name)} data-test-id='module-delete'/>
217 <div className='modules-list-item-selectors'>
219 type={fileTypes.YAML}
222 onChange={onModuleFileTypeChange}/>
227 onChange={onModuleFileTypeChange}/>
228 {displayVolumes && <SelectWithFileType
232 onChange={onModuleFileTypeChange}/>}
233 {displayVolumes && <SelectWithFileType
234 type={fileTypes.VOL_ENV}
237 onChange={onModuleFileTypeChange}/>}
238 <AddOrDeleteVolumeFiels onAdd={() => this.setState({displayVolumes: true})} onDelete={() => this.deleteVolumeFiles()} add={!displayVolumes}/>
245 class ArtifactOrNestedFileList extends Component {
248 let {type, title, selected, options, onSelectChanged, onAddAllUnassigned} = this.props;
250 <div className={`artifact-files ${type === 'nested' ? 'nested' : ''}`}>
251 <div className='artifact-files-header'>
253 {type === 'artifact' && (<Icon image='artifacts' iconClassName='heat-setup-module-icon' />)}
256 {type === 'artifact' && <span className='add-all-unassigned' onClick={onAddAllUnassigned}>{i18n('Add All Unassigned Files')}</span>}
258 {type === 'nested' ? (
259 <ul className='nested-list'>{selected.map(nested =>
260 <li key={nested} className='nested-list-item'>{nested}</li>
264 onMultiSelectChanged={onSelectChanged || (() => {
268 placeholder={i18n('Add Artifact')}
276 const buildLabelValueObject = str => (typeof str === 'string' ? {value: str, label: str} : str);
278 class SoftwareProductHeatSetupView extends Component {
280 processAndValidateHeat(heatData, heatDataCache){
281 let {onProcessAndValidate, changeAttachmentsTab, version} = this.props;
282 onProcessAndValidate({heatData, heatDataCache, version}).then(
283 () => changeAttachmentsTab(tabsMapping.VALIDATION)
288 let {modules, heatSetupCache, isReadOnlyMode, heatDataExist, unassigned, artifacts, nested, onArtifactListChange, onAddAllUnassigned} = this.props;
290 const formattedUnassigned = unassigned.map(buildLabelValueObject);
291 const formattedArtifacts = artifacts.map(buildLabelValueObject);
293 <div className={`heat-setup-view ${isReadOnlyMode ? 'disabled' : ''}`}>
294 <div className='heat-setup-view-modules-and-artifacts'>
295 <SortableModuleFileList
297 artifacts={formattedArtifacts}
298 unassigned={formattedUnassigned}/>
299 <ArtifactOrNestedFileList
301 title={i18n('ARTIFACTS')}
302 options={formattedUnassigned}
303 selected={formattedArtifacts}
304 onSelectChanged={onArtifactListChange}
305 onAddAllUnassigned={onAddAllUnassigned}/>
306 <ArtifactOrNestedFileList
308 title={i18n('NESTED HEAT FILES')}
314 formattedUnassigned.length > 0 ?
315 (<ul>{formattedUnassigned.map(file => <UnassignedFile key={file.label} name={file.label}/>)}</ul>)
318 heatDataExist={heatDataExist}
319 onClick={() => this.processAndValidateHeat({modules, unassigned, artifacts, nested}, heatSetupCache)}/>)
321 </UnassignedFileList>
328 export default SoftwareProductHeatSetupView;