Obtain upload lock before uploading
[sdc.git] / openecomp-ui / src / sdc-app / onboarding / softwareProduct / landingPage / SoftwareProductLandingPageView.jsx
1 /*
2  * Copyright © 2016-2018 European Support Limited
3  * Modifications Copyright (C) 2021 Nordix Foundation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 import React from 'react';
18 import PropTypes from 'prop-types';
19 import classnames from 'classnames';
20 import Dropzone from 'react-dropzone';
21
22 import i18n from 'nfvo-utils/i18n/i18n.js';
23 import VnfRepositorySearchBox from 'nfvo-components/vnfMarketPlace/VnfRepositorySearchBox.jsx';
24
25 import { SVGIcon } from 'onap-ui-react';
26 import SoftwareProductComponentsList from 'sdc-app/onboarding/softwareProduct/components/SoftwareProductComponents.js';
27 import VspUploadStatus from 'sdc-app/onboarding/softwareProduct/landingPage/VspUploadStatus';
28 import ProgressBar from 'react-bootstrap/lib/ProgressBar';
29
30 const SoftwareProductPropType = PropTypes.shape({
31     name: PropTypes.string,
32     description: PropTypes.string,
33     version: PropTypes.string,
34     id: PropTypes.string,
35     categoryId: PropTypes.string,
36     vendorId: PropTypes.string,
37     licenseType: PropTypes.string,
38     status: PropTypes.string,
39     licensingData: PropTypes.object,
40     validationData: PropTypes.object,
41     selectedModelList: PropTypes.arrayOf(PropTypes.string)
42 });
43
44 const ComponentPropType = PropTypes.shape({
45     id: PropTypes.string,
46     name: PropTypes.string,
47     displayName: PropTypes.string,
48     description: PropTypes.string
49 });
50
51 class SoftwareProductLandingPageView extends React.Component {
52     state = {
53         fileName: '',
54         dragging: false,
55         uploadStatus: {},
56         files: [],
57         uploadProgress: 0,
58         showProgressBar: false
59     };
60
61     constructor(props) {
62         super(props);
63         this.getExternalLicenceFeatureState = this.getExternalLicenceFeatureState.bind(
64             this
65         );
66     }
67
68     static propTypes = {
69         currentSoftwareProduct: SoftwareProductPropType,
70         isReadOnlyMode: PropTypes.bool,
71         componentsList: PropTypes.arrayOf(ComponentPropType),
72         version: PropTypes.object,
73         onLicenseChange: PropTypes.func,
74         onUpload: PropTypes.func,
75         fetchUploadStatus: PropTypes.func,
76         onUploadConfirmation: PropTypes.func,
77         onInvalidFileSizeUpload: PropTypes.func,
78         onComponentSelect: PropTypes.func,
79         onAddComponent: PropTypes.func
80     };
81
82     componentDidMount() {
83         const {
84             onCandidateInProcess,
85             currentSoftwareProduct,
86             isCertified
87         } = this.props;
88         if (currentSoftwareProduct.candidateOnboardingOrigin && !isCertified) {
89             onCandidateInProcess(currentSoftwareProduct.id);
90         }
91         this.keepCheckingUploadStatus();
92     }
93
94     componentWillUnmount() {
95         this.stopUploadStatusChecking();
96     }
97
98     keepCheckingUploadStatus(initialDelayInMs = 0, updatePeriodInMs = 10000) {
99         this.stopUploadStatusChecking();
100         setTimeout(() => this.updateUploadStatus(), initialDelayInMs);
101         this.uploadStatusInterval = setInterval(
102             () => this.updateUploadStatus(),
103             updatePeriodInMs
104         );
105     }
106
107     stopUploadStatusChecking() {
108         clearInterval(this.uploadStatusInterval);
109     }
110
111     updateUploadStatus() {
112         const currentVspId = this.props.currentSoftwareProduct.id;
113         this.props
114             .fetchUploadStatus(currentVspId)
115             .then(uploadStatusResponse => {
116                 const vspUploadStatus = new VspUploadStatus(
117                     uploadStatusResponse
118                 );
119                 this.setState({
120                     uploadStatus: vspUploadStatus
121                 });
122             })
123             .catch(error =>
124                 console.error('Could not retrieve upload status', error)
125             );
126     }
127
128     licenceChange = (e, currentSoftwareProduct, onLicenseChange) => {
129         currentSoftwareProduct.licenseType = e.target.value
130             ? e.target.value
131             : 'INTERNAL';
132         onLicenseChange(currentSoftwareProduct);
133     };
134
135     getExternalLicenceFeatureState() {
136         const licenseFeature = this.props.features.find(
137             feature => feature.name === 'EXTERNAL_LICENSE'
138         );
139         return licenseFeature ? licenseFeature.active : true;
140     }
141
142     render() {
143         let {
144             currentSoftwareProduct,
145             isReadOnlyMode,
146             isManual,
147             onLicenseChange
148         } = this.props;
149         let licenceChange = this.licenceChange;
150         return (
151             <div className="software-product-landing-wrapper">
152                 <Dropzone
153                     className={classnames('software-product-landing-view', {
154                         'active-dragging': this.state.dragging
155                     })}
156                     onDrop={files =>
157                         this.handleImportSubmit(files, isReadOnlyMode, isManual)
158                     }
159                     onDragEnter={() =>
160                         this.handleOnDragEnter(isReadOnlyMode, isManual)
161                     }
162                     onDragLeave={() => this.setState({ dragging: false })}
163                     multiple={false}
164                     disableClick={true}
165                     ref="fileInput"
166                     name="fileInput"
167                     accept=".zip, .csar">
168                     <div className="draggable-wrapper">
169                         <div className="software-product-landing-view-top">
170                             <div className="row">
171                                 <ProductSummary
172                                     currentSoftwareProduct={
173                                         currentSoftwareProduct
174                                     }
175                                     licenceChange={licenceChange}
176                                     onLicenseChange={onLicenseChange}
177                                     externalLicenceEnabled={this.getExternalLicenceFeatureState()}
178                                 />
179                                 <div className="details-panel">
180                                     {this.renderProductAttachments(
181                                         isReadOnlyMode
182                                     )}
183                                 </div>
184                             </div>
185                         </div>
186                     </div>
187                 </Dropzone>
188                 <SoftwareProductComponentsList />
189             </div>
190         );
191     }
192
193     handleOnDragEnter(isReadOnlyMode, isManual) {
194         if (!isReadOnlyMode && !isManual) {
195             this.setState({ dragging: true });
196         }
197     }
198
199     isUploadInProgress() {
200         return (
201             this.state.uploadStatus.complete !== undefined &&
202             !this.state.uploadStatus.complete
203         );
204     }
205
206     renderProductAttachments(isReadOnlyMode) {
207         let { onBrowseVNF, currentSoftwareProduct } = this.props;
208
209         if (this.isUploadInProgress()) {
210             return (
211                 <div>
212                     <div className="software-product-landing-view-heading-title">
213                         {i18n('Software Product Attachments')}
214                     </div>
215                     <div className="software-product-landing-view-top-block-col-upl ">
216                         <div className="upload-status-text">
217                             {this.state.uploadStatus.statusToString()}
218                             {this.state.showProgressBar && (
219                                 <ProgressBar now={this.state.uploadProgress} />
220                             )}
221                         </div>
222                     </div>
223                 </div>
224             );
225         }
226         return (
227             <div>
228                 <div className="software-product-landing-view-heading-title">
229                     {i18n('Software Product Attachments')}
230                 </div>
231                 <VnfRepositorySearchBox
232                     dataTestId="upload-btn"
233                     isReadOnlyMode={isReadOnlyMode}
234                     className={classnames(
235                         'software-product-landing-view-top-block-col-upl',
236                         { disabled: isReadOnlyMode }
237                     )}
238                     onClick={() => this.refs.fileInput.open()}
239                     onBrowseVNF={() => onBrowseVNF(currentSoftwareProduct)}
240                 />
241             </div>
242         );
243     }
244
245     handleImportSubmit(files, isReadOnlyMode, isManual) {
246         if (isReadOnlyMode || isManual) {
247             return;
248         }
249         if (files[0] && files[0].size) {
250             this.setState({
251                 fileName: files[0].name,
252                 dragging: false,
253                 complete: '0'
254             });
255             this.startUploading(files);
256         } else {
257             this.setState({
258                 dragging: false
259             });
260             this.props.onInvalidFileSizeUpload();
261         }
262     }
263
264     onUploadStart = vspUploadStatus => {
265         this.setState({
266             uploadStatus: vspUploadStatus
267         });
268         this.stopUploadStatusChecking();
269         this.showProgressBar();
270     };
271
272     onUploadProgress = progressEvent => {
273         const vspUploadStatus = new VspUploadStatus({
274             status: VspUploadStatus.UPLOADING,
275             complete: false
276         });
277         this.setState({
278             uploadStatus: vspUploadStatus
279         });
280         const percentCompleted = Math.round(
281             progressEvent.loaded * 100 / progressEvent.total
282         );
283         if (percentCompleted === 100) {
284             this.keepCheckingUploadStatus(5000);
285             this.resetUploadProgress(2000);
286         }
287         this.setState({ uploadProgress: percentCompleted });
288     };
289
290     onUploadFinished = () => {
291         this.updateUploadStatus();
292     };
293
294     showProgressBar() {
295         this.setState({ showProgressBar: true });
296     }
297
298     hideProgressBar() {
299         this.setState({ showProgressBar: false });
300     }
301
302     resetUploadProgress(milliseconds) {
303         setTimeout(() => {
304             this.setState({ uploadProgress: 0 });
305             this.hideProgressBar();
306         }, milliseconds);
307     }
308
309     startUploading(files) {
310         let {
311             onUpload,
312             currentSoftwareProduct,
313             onUploadConfirmation
314         } = this.props;
315
316         let { validationData } = currentSoftwareProduct;
317
318         if (!(files && files.length)) {
319             return;
320         }
321         let file = files[0];
322         let formData = new FormData();
323         formData.append('upload', file);
324         this.refs.fileInput.value = '';
325
326         if (validationData) {
327             onUploadConfirmation(
328                 currentSoftwareProduct.id,
329                 formData,
330                 vspUploadStatus => this.onUploadStart(vspUploadStatus),
331                 this.onUploadProgress,
332                 this.onUploadFinished
333             );
334         } else {
335             onUpload(
336                 currentSoftwareProduct.id,
337                 formData,
338                 vspUploadStatus => this.onUploadStart(vspUploadStatus),
339                 this.onUploadProgress,
340                 this.onUploadFinished
341             );
342         }
343     }
344 }
345
346 const ProductSummary = ({
347     currentSoftwareProduct,
348     licenceChange,
349     onLicenseChange,
350     externalLicenceEnabled
351 }) => {
352     let {
353         name = '',
354         description = '',
355         vendorName = '',
356         fullCategoryDisplayName = '',
357         selectedModelList = []
358     } = currentSoftwareProduct;
359     return (
360         <div className="details-panel">
361             <div className="software-product-landing-view-heading-title">
362                 {i18n('Software Product Details')}
363             </div>
364             <div className="software-product-landing-view-top-block">
365                 <div className="details-container">
366                     <div className="single-detail-section title-section">
367                         <div className="single-detail-section title-text">
368                             {name}
369                         </div>
370                     </div>
371                     <div className="details-section">
372                         <div className="multiple-details-section">
373                             <div className="detail-col">
374                                 <div className="title">{i18n('Vendor')}</div>
375                                 <div className="description">{vendorName}</div>
376                             </div>
377                             <div className="detail-col">
378                                 <div className="title">{i18n('Category')}</div>
379                                 <div className="description">
380                                     {fullCategoryDisplayName}
381                                 </div>
382                             </div>
383                             <div className="detail-col">
384                                 <div className="title">{i18n('Model')}</div>
385                                 <div className="description">
386                                     {selectedModelList.length > 0 ? (
387                                         <ul>
388                                             {selectedModelList.map(value => (
389                                                 <li>{value}</li>
390                                             ))}
391                                         </ul>
392                                     ) : (
393                                         i18n('model.sdc.label')
394                                     )}
395                                 </div>
396                             </div>
397                             <div className="detail-col">
398                                 <div className="title extra-large">
399                                     {i18n('License Agreement')}
400                                 </div>
401                                 <div className="description">
402                                     <LicenseAgreement
403                                         licenceChange={licenceChange}
404                                         currentSoftwareProduct={
405                                             currentSoftwareProduct
406                                         }
407                                         onLicenseChange={onLicenseChange}
408                                         externalLicenceEnabled={
409                                             externalLicenceEnabled
410                                         }
411                                     />
412                                 </div>
413                             </div>
414                         </div>
415                         <div className="single-detail-section">
416                             <div className="title">{i18n('Description')}</div>
417                             <div className="description">{description}</div>
418                         </div>
419                     </div>
420                 </div>
421             </div>
422         </div>
423     );
424 };
425
426 const LicenseAgreementWithExternal = ({
427     licenceChange,
428     currentSoftwareProduct,
429     onLicenseChange
430 }) => {
431     return (
432         <div className="missing-license">
433             <form>
434                 <input
435                     type="radio"
436                     value="INTERNAL"
437                     id="INTERNAL"
438                     onChange={event =>
439                         licenceChange(
440                             event,
441                             currentSoftwareProduct,
442                             onLicenseChange
443                         )
444                     }
445                     checked={currentSoftwareProduct.licenseType === 'INTERNAL'}
446                     name="license"
447                 />
448                 <div className="description licenceLabel">
449                     {i18n('Internal license')}
450                 </div>
451                 <br />
452                 <input
453                     type="radio"
454                     value="EXTERNAL"
455                     id="EXTERNAL"
456                     onChange={event =>
457                         licenceChange(
458                             event,
459                             currentSoftwareProduct,
460                             onLicenseChange
461                         )
462                     }
463                     checked={currentSoftwareProduct.licenseType === 'EXTERNAL'}
464                     name="license"
465                 />
466                 <div className="description licenceLabel">
467                     {i18n('External license')}
468                 </div>
469             </form>
470         </div>
471     );
472 };
473
474 const LicenseAgreementWithoutExternal = ({
475     licenceChange,
476     currentSoftwareProduct,
477     onLicenseChange
478 }) => {
479     if (!currentSoftwareProduct.licenseAgreementName) {
480         return (
481             <div
482                 className="missing-license clickable"
483                 onClick={event =>
484                     licenceChange(
485                         event,
486                         currentSoftwareProduct,
487                         onLicenseChange
488                     )
489                 }>
490                 <SVGIcon color="warning" name="exclamationTriangleFull" />
491                 <div className="warning-text">{i18n('Missing')}</div>
492             </div>
493         );
494     }
495     return <div>{currentSoftwareProduct.licenseAgreementName}</div>;
496 };
497
498 const LicenseAgreement = ({
499     licenceChange,
500     currentSoftwareProduct,
501     onLicenseChange,
502     externalLicenceEnabled
503 }) => {
504     if (externalLicenceEnabled) {
505         return (
506             <LicenseAgreementWithExternal
507                 licenceChange={licenceChange}
508                 currentSoftwareProduct={currentSoftwareProduct}
509                 onLicenseChange={onLicenseChange}
510             />
511         );
512     } else {
513         return (
514             <LicenseAgreementWithoutExternal
515                 licenceChange={licenceChange}
516                 currentSoftwareProduct={currentSoftwareProduct}
517                 onLicenseChange={onLicenseChange}
518             />
519         );
520     }
521 };
522
523 export default SoftwareProductLandingPageView;