[SDC] Onboarding 1710 rebase.
[sdc.git] / openecomp-ui / src / sdc-app / onboarding / softwareProduct / attachments / setup / HeatSetupView.jsx
1 /*!
2  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 import React, {Component} from 'react';
17 import Button from 'sdc-ui/lib/react/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 'sdc-ui/lib/react/SVGIcon.js';
25 import {fileTypes} from './HeatSetupConstants.js';
26 import {tabsMapping} from '../SoftwareProductAttachmentsConstants.js';
27 import {sortable} from 'react-sortable';
28
29 class ListItem extends Component {
30
31         render() {
32                 return (
33                         <li {...this.props}>{this.props.children}</li>
34                 );
35         }
36 }
37
38
39 const SortableListItem = sortable(ListItem);
40
41 class SortableModuleFileList extends Component {
42
43         state = {
44                 draggingIndex: null,
45                 data: this.props.modules
46         };
47
48
49         componentWillReceiveProps(nextProps) {
50                 this.setState({data: nextProps.modules});
51         }
52
53         render() {
54
55                 let {unassigned, onModuleRename, onModuleDelete, onModuleAdd, onBaseAdd, onModuleFileTypeChange, isBaseExist} = this.props;
56                 const childProps = module => ({
57                         module,
58                         onModuleRename,
59                         onModuleDelete,
60                         onModuleFileTypeChange: (value, type) => onModuleFileTypeChange({module, value, type}),
61                         files: unassigned
62                 });
63                 let listItems = this.state.data.map(function (item, i) {
64                         return (
65                                 <SortableListItem
66                                         key={i}
67                                         updateState={data => this.setState(data)}
68                                         items={this.state.data}
69                                         draggingIndex={this.state.draggingIndex}
70                                         sortId={i}
71                                         outline='list'><ModuleFile {...childProps(item)} /></SortableListItem>
72                         );
73                 }, this);
74
75                 return (
76                         <div className='modules-list-wrapper'>
77                                 <div className='modules-list-header'>
78                                         <div className='modules-list-controllers'>
79                                                 {!isBaseExist && <Button btnType='link' onClick={onBaseAdd} disabled={unassigned.length === 0}>{i18n('Add Base')}</Button>}
80                                                 <Button btnType='link' onClick={onModuleAdd} disabled={unassigned.length === 0}>{i18n('Add Module')}</Button>
81                                         </div>
82                                 </div>
83                                 <ul>{listItems}</ul>
84                         </div>
85                 );
86         }
87 }
88
89 const tooltip = (name) => <Tooltip id='tooltip-bottom'>{name}</Tooltip>;
90 const UnassignedFileList = (props) => {
91         return (
92                 <div className='unassigned-files'>
93                         <div className='unassigned-files-title'>{i18n('UNASSIGNED FILES')}</div>
94                         <div className='unassigned-files-list'>{props.children}</div>
95                 </div>
96         );
97 };
98
99 const EmptyListContent = props => {
100         let {onClick, heatDataExist} = props;
101         let displayText = heatDataExist ? 'All Files Are Assigned' : '';
102         return (
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='angleRight'/></div>}
106                 </div>
107         );
108 };
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>
112         </OverlayTrigger>
113 );
114
115 const AddOrDeleteVolumeFiels = ({add = true, onAdd, onDelete}) => {
116         const displayText = add ? 'Add Volume Files' : 'Delete Volume Files';
117         const action = add ? onAdd : onDelete;
118         return (
119                 <div className='add-or-delete-volumes' onClick={action}>
120                         <SVGIcon name={add ? 'plus' : 'close'} />
121                         <span>{i18n(displayText)}</span>
122                 </div>
123         );
124 };
125
126 const SelectWithFileType = ({type, selected, files, onChange}) => {
127
128         let filteredFiledAccordingToType = files.filter(file => file.label.search(type.regex) > -1);
129         if (selected) {
130                 filteredFiledAccordingToType = filteredFiledAccordingToType.concat({label: selected, value: selected});
131         }
132
133         return (
134                 <SelectInput
135                         data-test-id={`${type.label}-list`}
136                         label={type.label}
137                         value={selected}
138                         onChange={value => value !== selected && onChange(value, type.label)}
139                         disabled={filteredFiledAccordingToType.length === 0}
140                         placeholder={filteredFiledAccordingToType.length === 0 ? '' : undefined}
141                         clearable={true}
142                         options={filteredFiledAccordingToType} />
143         );
144 };
145
146 class NameEditInput extends Component {
147         componentDidMount() {
148                 this.input.focus();
149         }
150
151         render() {
152                 return (
153                         <FormControl {...this.props} className='name-edit' inputRef={input => this.input = input}/>
154                 );
155         }
156 }
157
158 class ModuleFile extends Component {
159         constructor(props) {
160                 super(props);
161                 this.state = {
162                         isInNameEdit: false,
163                         displayVolumes: Boolean(props.module.vol || props.module.volEnv)
164                 };
165         }
166
167         handleSubmit(event, name) {
168                 if (event.keyCode === 13) {
169                         this.handleModuleRename(event, name);
170                 }
171         }
172
173         componentWillReceiveProps(nextProps) {
174                 this.setState({displayVolumes: Boolean(nextProps.module.vol || nextProps.module.volEnv)});
175         }
176
177         handleModuleRename(event, name) {
178                 this.setState({isInNameEdit: false});
179                 this.props.onModuleRename(name, event.target.value);
180         }
181
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});
187         }
188
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)}/>);
193                 }
194                 return (<span className='filename-text'>{name}</span>);
195         }
196
197         render() {
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';
201                 return (
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
210                                                                 name='pencil'
211                                                                 onClick={() => this.setState({isInNameEdit: true})}
212                                                                 data-test-id={isBase ? 'base-name' : 'module-name'}/>}
213                                                 </div>
214                                         </div>
215                                         <SVGIcon name='trashO' onClick={() => onModuleDelete(name)} data-test-id='module-delete'/>
216                                 </div>
217                                 <div className='modules-list-item-selectors'>
218                                         <SelectWithFileType
219                                                 type={fileTypes.YAML}
220                                                 files={files}
221                                                 selected={yaml}
222                                                 onChange={onModuleFileTypeChange}/>
223                                         <SelectWithFileType
224                                                 type={fileTypes.ENV}
225                                                 files={files}
226                                                 selected={env}
227                                                 onChange={onModuleFileTypeChange}/>
228                                         {displayVolumes  && <SelectWithFileType
229                                                 type={fileTypes.VOL}
230                                                 files={files}
231                                                 selected={vol}
232                                                 onChange={onModuleFileTypeChange}/>}
233                                         {displayVolumes && <SelectWithFileType
234                                                 type={fileTypes.VOL_ENV}
235                                                 files={files}
236                                                 selected={volEnv}
237                                                 onChange={onModuleFileTypeChange}/>}
238                                         <AddOrDeleteVolumeFiels onAdd={() => this.setState({displayVolumes: true})} onDelete={() => this.deleteVolumeFiles()} add={!displayVolumes}/>
239                                 </div>
240                         </div>
241                 );
242         }
243 }
244
245 class ArtifactOrNestedFileList extends Component {
246
247         render() {
248                 let {type, title, selected, options, onSelectChanged, onAddAllUnassigned} = this.props;
249                 return (
250                         <div className={`artifact-files ${type === 'nested' ? 'nested' : ''}`}>
251                                 <div className='artifact-files-header'>
252                                         <span>
253                                                 {type === 'artifact' && (<Icon image='artifacts' iconClassName='heat-setup-module-icon' />)}
254                                                 {`${title}`}
255                                         </span>
256                                         {type === 'artifact' && <span className='add-all-unassigned' onClick={onAddAllUnassigned}>{i18n('Add All Unassigned Files')}</span>}
257                                 </div>
258                                 {type === 'nested' ? (
259                                         <ul className='nested-list'>{selected.map(nested =>
260                                                 <li key={nested} className='nested-list-item'>{nested}</li>
261                                         )}</ul>) :
262                                         (<SelectInput
263                                                 options={options}
264                                                 onMultiSelectChanged={onSelectChanged || (() => {
265                                                 })}
266                                                 value={selected}
267                                                 clearable={false}
268                                                 placeholder={i18n('Add Artifact')}
269                                                 multi/>)
270                                 }
271                         </div>
272                 );
273         }
274 }
275
276 const buildLabelValueObject = str => (typeof str === 'string' ? {value: str, label: str} : str);
277
278 class SoftwareProductHeatSetupView extends Component {
279
280         processAndValidateHeat(heatData, heatDataCache){
281                 let {onProcessAndValidate, changeAttachmentsTab, version} = this.props;
282                 onProcessAndValidate({heatData, heatDataCache, version}).then(
283                         () => changeAttachmentsTab(tabsMapping.VALIDATION)
284                 );
285         }
286
287         render() {
288                 let {modules, heatSetupCache, isReadOnlyMode, heatDataExist, unassigned, artifacts, nested, onArtifactListChange, onAddAllUnassigned} = this.props;
289
290                 const formattedUnassigned = unassigned.map(buildLabelValueObject);
291                 const formattedArtifacts = artifacts.map(buildLabelValueObject);
292                 return (
293                         <div className={`heat-setup-view ${isReadOnlyMode ? 'disabled' : ''}`}>
294                                 <div className='heat-setup-view-modules-and-artifacts'>
295                                         <SortableModuleFileList
296                                                 {...this.props}
297                                                 artifacts={formattedArtifacts}
298                                                 unassigned={formattedUnassigned}/>
299                                         <ArtifactOrNestedFileList
300                                                 type={'artifact'}
301                                                 title={i18n('ARTIFACTS')}
302                                                 options={formattedUnassigned}
303                                                 selected={formattedArtifacts}
304                                                 onSelectChanged={onArtifactListChange}
305                                                 onAddAllUnassigned={onAddAllUnassigned}/>
306                                         <ArtifactOrNestedFileList
307                                                 type={'nested'}
308                                                 title={i18n('NESTED HEAT FILES')}
309                                                 options={[]}
310                                                 selected={nested}/>
311                                 </div>
312                                 <UnassignedFileList>
313                                         {
314                                                 formattedUnassigned.length > 0 ?
315                                                 (<ul>{formattedUnassigned.map(file => <UnassignedFile key={file.label} name={file.label}/>)}</ul>)
316                                                 :
317                                                 (<EmptyListContent
318                                                         heatDataExist={heatDataExist}
319                                                         onClick={() => this.processAndValidateHeat({modules, unassigned, artifacts, nested}, heatSetupCache)}/>)
320                                         }
321                                 </UnassignedFileList>
322                         </div>
323                 );
324         }
325
326 }
327
328 export default SoftwareProductHeatSetupView;