New backend copy package support
[osm/UI.git] / skyquake / plugins / composer / src / src / stores / CatalogPackageManagerStore.js
1
2 /*
3 *
4 * Copyright 2016 RIFT.IO Inc
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19 'use strict';
20
21 import _delay from 'lodash/delay'
22 import _pick from 'lodash/pick'
23 import alt from '../alt'
24 import guid from '../libraries/guid'
25 import numeral from 'numeral'
26 import moment from 'moment'
27 import utils from '../libraries/utils'
28 import CatalogPackageManagerSource from '../sources/CatalogPackageManagerSource'
29 import CatalogPackageManagerActions from '../actions/CatalogPackageManagerActions'
30 import CatalogDataSource from '../sources/CatalogDataSource'
31
32 import imgDownload from '../../../node_modules/open-iconic/svg/cloud-download.svg'
33 import imgOnboard from '../../../node_modules/open-iconic/svg/cloud-upload.svg'
34 import imgUpdate from '../../../node_modules/open-iconic/svg/data-transfer-upload.svg'
35 import imgCopy from '../../../node_modules/open-iconic/svg/layers.svg'
36
37 const defaults = {
38 operation: {
39 id: '',
40 name: '',
41 icon: '',
42 transactionId: '',
43 progress: 0,
44 message: 'Requested',
45 args: {},
46 pending: false,
47 success: false,
48 error: false,
49 },
50 downloadPackage: {
51 id: '',
52 name: '',
53 icon: imgDownload,
54 catalogItems: [],
55 transactionId: '',
56 progress: 0,
57 message: 'Requesting catalog package export...',
58 pending: false,
59 success: false,
60 error: false,
61 url: '',
62 urlValidUntil: ''
63 },
64 checkStatusDelayInSeconds: 2,
65 downloadUrlTimeToLiveInMinutes: 5
66 };
67
68 const exception = function ignoreException() {};
69
70 const packagePropertyNames = Object.keys(defaults.downloadPackage);
71
72 function getCatalogPackageManagerServerOrigin() {
73 return utils.getSearchParams(window.location).upload_server + ':4567';
74 }
75
76 function delayStatusCheck(statusCheckFunction, operation) {
77 if (!operation.checkStatusTimeoutId) {
78 const delayCallback = function () {
79 delete operation.checkStatusTimeoutId;
80 statusCheckFunction(operation).catch(exception);
81 };
82 operation.checkStatusTimeoutId = _delay(delayCallback, defaults.checkStatusDelayInSeconds * 1000);
83 }
84 }
85
86 class CatalogPackageManagerStore {
87
88 constructor() {
89
90 this.operations = [];
91
92 this.registerAsync(CatalogDataSource);
93 this.registerAsync(CatalogPackageManagerSource);
94 this.bindAction(CatalogPackageManagerActions.REMOVE_CATALOG_OPERATION, this.removeCatalogOperation);
95 this.bindAction(CatalogPackageManagerActions.DOWNLOAD_CATALOG_PACKAGE, this.downloadCatalogPackage);
96 this.bindAction(CatalogPackageManagerActions.DOWNLOAD_CATALOG_PACKAGE_STATUS_UPDATED, this.onDownloadCatalogPackageStatusUpdated);
97 this.bindAction(CatalogPackageManagerActions.DOWNLOAD_CATALOG_PACKAGE_ERROR, this.onDownloadCatalogPackageError);
98 this.bindAction(CatalogPackageManagerActions.UPLOAD_CATALOG_PACKAGE, this.uploadCatalogPackage);
99 this.bindAction(CatalogPackageManagerActions.UPLOAD_CATALOG_PACKAGE_STATUS_UPDATED, this.onUploadCatalogPackageStatusUpdated);
100 this.bindAction(CatalogPackageManagerActions.UPLOAD_CATALOG_PACKAGE_ERROR, this.onUploadCatalogPackageError);
101 this.bindAction(CatalogPackageManagerActions.COPY_CATALOG_PACKAGE, this.copyCatalogPackage);
102 this.bindAction(CatalogPackageManagerActions.UPDATE_STATUS, this.updateOperationStatus);
103 }
104
105 addOperation(operation) {
106 const operations = [operation].concat(this.operations);
107 this.setState({operations});
108 }
109
110 updateOperation(operation) {
111 const operations = this.operations.map(d => {
112 if (d.id === operation.id) {
113 return Object.assign({}, d, operation);
114 }
115 return d;
116 });
117 this.setState({operations});
118 }
119
120 removeCatalogOperation(operation) {
121 const operations = this.operations.filter(d => d.id !== operation.id);
122 this.setState({operations});
123 }
124
125 copyCatalogPackage(sourcePackage) {
126 let operationInfo = Object.assign({}, defaults.operation);
127 operationInfo.name = "Duplication of " + sourcePackage.name;
128 operationInfo.id = guid();
129 operationInfo.icon = imgCopy;
130 operationInfo.type = 'copy';
131 operationInfo.message = 'Requesting package duplication.';
132 operationInfo.args.packageType = sourcePackage['uiState']['type'].toUpperCase();
133 operationInfo.args.id = sourcePackage.id;
134 operationInfo.args.name = sourcePackage.name + ' copy';
135
136 this.addOperation(operationInfo);
137 this.getInstance().requestCatalogPackageCopy(operationInfo, sourcePackage);
138 }
139
140 updateOperationStatus(operation) {
141 console.debug('package manager operation status update', operation);
142 this.updateOperation(operation);
143 if (operation.pending) {
144 delayStatusCheck(this.getInstance().requestCatalogPackageCopyStatus, operation);
145 }
146 }
147
148 uploadCatalogPackage(file) {
149 file.id = file.id || guid();
150 const operation = _pick(file, packagePropertyNames);
151 operation.icon = file.riftAction === 'onboard' ? imgOnboard : imgUpdate;
152 operation.type = 'upload';
153 this.addOperation(operation);
154 // note DropZone.js handles the async upload so we don't have to invoke any async action creators
155 }
156
157 onUploadCatalogPackageStatusUpdated(response) {
158 const upload = updateStatusInfo(response);
159 this.updateOperation(upload);
160 console.log('updating package upload')
161 // if pending with no transaction id - do nothing
162 // bc DropZone.js will notify upload progress
163 if (upload.pending && upload.transactionId) {
164 console.log('checking status')
165 delayStatusCheck(this.getInstance().requestCatalogPackageUploadStatus, upload);
166 } else if (upload.success) {
167 this.getInstance().loadCatalogs();
168 console.log('finished uploading to node, requesting status from rest')
169 }
170 }
171
172 onUploadCatalogPackageError(response) {
173 console.warn('onUploadCatalogPackageError', response);
174 const operation = updateStatusInfo(response);
175 this.updateOperation(operation);
176 }
177
178 downloadCatalogPackage(data) {
179 let catalogItems = data['selectedItems'] || [];
180 let schema = data['selectedFormat'] || 'mano';
181 let grammar = data['selectedGrammar'] || 'osm';
182 let format = "YAML";
183 if (catalogItems.length) {
184 const operation = Object.assign({}, defaults.downloadPackage, {id: guid()});
185 operation.name = catalogItems[0].name;
186 operation.type = 'download';
187 if (catalogItems.length > 1) {
188 operation.name += ' (' + catalogItems.length + ' items)';
189 }
190 operation.ids = catalogItems.map(d => d.id).sort().toString();
191 operation.catalogItems = catalogItems;
192 this.addOperation(operation);
193 this.getInstance().requestCatalogPackageDownload(operation, format, grammar, schema).catch(exception);
194 }
195 }
196
197 onDownloadCatalogPackageStatusUpdated(response) {
198 const download = updateStatusInfo(response);
199 this.updateOperation(download);
200 if (download.pending) {
201 delayStatusCheck(this.getInstance().requestCatalogPackageDownloadStatus, download);
202 }
203 }
204
205 onDownloadCatalogPackageError(response) {
206 console.warn('onDownloadCatalogPackageError', response);
207 const operation = updateStatusInfo(response);
208 this.updateOperation(operation);
209 }
210
211 }
212
213 function calculateUploadProgressMessage(size = 0, progress = 0, bytesSent = 0) {
214 const amount = parseFloat(progress) || 0;
215 const loaded = amount === 100 ? size : size * amount / 100;
216 let progressText;
217 if (amount === 100) {
218 progressText = numeral(loaded).format('0.0b') + ' loaded ';
219 } else if (typeof amount === 'number' && amount != 0) {
220 progressText = numeral(bytesSent).format('0.0b') + ' out of ' + numeral(size).format('0.0b');
221 } else {
222 progressText = progress;
223 }
224 return progressText;
225 }
226
227 function updateStatusInfo(response) {
228 // returns the operation object with the status fields updated based on the server response
229 const statusInfo = {
230 pending: false,
231 success: false,
232 error: false
233 };
234 const responseData = (response.data.output) ? response.data.output : response.data;
235 const operation = response.state;
236 if ( typeof response.data.status !== "number" ) {
237 switch(response.data.status) {
238 case 'upload-progress':
239 statusInfo.pending = true;
240 statusInfo.progress = parseFloat(responseData.progress) || 0;
241 statusInfo.message = calculateUploadProgressMessage(operation.size, responseData.progress, responseData.bytesSent);
242 break;
243 case 'upload-success':
244 statusInfo.pending = true;
245 statusInfo.progress = 100;
246 statusInfo.message = 'Upload completed.';
247 statusInfo.transactionId = responseData['transaction_id'] || responseData['transaction-id'] || operation.transactionId;
248 break;
249 case 'upload-error':
250 statusInfo.error = true;
251 statusInfo.message = responseData.message;
252 break;
253 case 'download-requested':
254 statusInfo.pending = true;
255 statusInfo.progress = 25;
256 statusInfo.transactionId = responseData['transaction_id'] || responseData['transaction-id'] || operation.transactionId;
257 break;
258 case 'pending':
259 statusInfo.pending = true;
260 statusInfo.progress = 50;
261 statusInfo.message = responseData.events[responseData.events.length - 1].text;
262 break;
263 case 'success':
264 statusInfo.success = true;
265 statusInfo.progress = 100;
266 statusInfo.message = responseData.events[responseData.events.length - 1].text;
267 if (operation.type === 'download') {
268 statusInfo.urlValidUntil = moment().add(defaults.downloadUrlTimeToLiveInMinutes, 'minutes').toISOString();
269 if (responseData.filename) {
270 statusInfo.url = getCatalogPackageManagerServerOrigin() + '/api/export/' + responseData.filename;
271 } else {
272 statusInfo.url = getCatalogPackageManagerServerOrigin() + '/api/export/' + operation.transactionId + '.tar.gz';
273 }
274 }
275 break;
276 case 'failure':
277 statusInfo.error = true;
278 statusInfo.message = responseData.errors[0].value;
279 break;
280 default:
281 throw new ReferenceError('a status of "request", "success", "failure", "pending", "upload-completed", "upload-error", "download-requested", "upload-progress", "upload-action" is required');
282 }
283 } else {
284 // typically get here due to unexpected development errors (backend exceptions, down/up load server access issues)
285 statusInfo.error = true;
286 statusInfo.message = responseData.statusText || 'Error';
287 }
288 return Object.assign({}, operation, statusInfo);
289 }
290
291 export default alt.createStore(CatalogPackageManagerStore, 'CatalogPackageManagerStore');