diff --git a/js/app.js b/js/app.js index 520bedbd..cb7e3ab5 100644 --- a/js/app.js +++ b/js/app.js @@ -69,7 +69,7 @@ class AppGateway { load(settings) { let type = 'default'; let subdomain = 'www'; - let redirectRoute = (); + let redirectRoute = (); if (settings) { type = settings.type; diff --git a/js/components/ascribe_forms/form_register_piece.js b/js/components/ascribe_forms/form_register_piece.js index b09d29ac..1c05b4d2 100644 --- a/js/components/ascribe_forms/form_register_piece.js +++ b/js/components/ascribe_forms/form_register_piece.js @@ -34,7 +34,11 @@ let RegisterPieceForm = React.createClass({ children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element - ]) + ]), + + onSingleTestComplete: React.PropTypes.func, + onTestsStart: React.PropTypes.func, + onTestsComplete: React.PropTypes.func }, getDefaultProps() { @@ -73,36 +77,51 @@ let RegisterPieceForm = React.createClass({ }); }, + getUploadTests() { + return [ + { + 'name': 'Direct to S3', + 'endpoint': 'https://ascribe0.s3.amazonaws.com' + }, + { + 'name': 'Direct to S3 with large chunking', + 'endpoint': 'https://ascribe0.s3.amazonaws.com', + 'chunkSize': 52428800 + }, + { + 'name': 'Fastly to S3', + 'endpoint': 'http://www.ascribe.io.global.prod.fastly.net' + }, + { + 'name': 'Fastly to S3 with large chunking', + 'endpoint': 'http://www.ascribe.io.global.prod.fastly.net', + 'chunkSize': 52428800 + } + ]; + }, + + onTestComplete(testInfo) { + this.props.onSingleTestComplete(testInfo); + + const uploadTests = this.getUploadTests(); + if (testInfo.name === uploadTests[uploadTests.length - 1].name) { + this.props.onTestsComplete(); + } + }, + render() { let currentUser = this.state.currentUser; let enableLocalHashing = currentUser && currentUser.profile ? currentUser.profile.hash_locally : false; enableLocalHashing = enableLocalHashing && this.props.enableLocalHashing; return ( -
- {this.props.submitMessage} - - } - spinner={ - - - - }> +
-

{this.props.headerMessage}

+

Upload test

+ + uploadTests={this.getUploadTests()} + onTestsStart={this.props.onTestsStart} + onTestComplete={this.onTestComplete} /> - - - - - - - - - - {this.props.children} - +
); } }); diff --git a/js/components/ascribe_forms/input_fineuploader.js b/js/components/ascribe_forms/input_fineuploader.js index 948521c0..0a859c7e 100644 --- a/js/components/ascribe_forms/input_fineuploader.js +++ b/js/components/ascribe_forms/input_fineuploader.js @@ -51,7 +51,12 @@ const InputFineUploader = React.createClass({ fileClassToUpload: shape({ singular: string, plural: string - }) + }), + + + + uploadTests: React.PropTypes.array, + onTestComplete: React.PropTypes.func }, getDefaultProps() { @@ -142,7 +147,10 @@ const InputFineUploader = React.createClass({ onInactive={this.props.onLoggedOut} enableLocalHashing={this.props.enableLocalHashing} uploadMethod={this.props.uploadMethod} - fileClassToUpload={this.props.fileClassToUpload} /> + fileClassToUpload={this.props.fileClassToUpload} + uploadTests={this.props.uploadTests} + onTestsStart={this.props.onTestsStart} + onTestComplete={this.props.onTestComplete} /> ); } }); diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js index 66564ff3..deb7c3e8 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js @@ -46,65 +46,19 @@ let FileDragAndDropDialog = React.createClass({ if (hasFiles) { return null; } else { - if (enableLocalHashing && !uploadMethod) { - const currentQueryParams = getCurrentQueryParams(); + const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular) + : getLangText('choose a %s to upload', fileClassToUpload.singular); - const queryParamsHash = Object.assign({}, currentQueryParams); - queryParamsHash.method = 'hash'; - - const queryParamsUpload = Object.assign({}, currentQueryParams); - queryParamsUpload.method = 'upload'; - - return ( -
-

{getLangText('Would you rather')}

- - - {getLangText('Hash your work')} - - - - or - - - - {getLangText('Upload and hash your work')} - - -
- ); - } else { - if (multipleFiles) { - return ( - - {this.getDragDialog(fileClassToUpload.plural)} - - {getLangText('choose %s to upload', fileClassToUpload.plural)} - - - ); - } else { - const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular) - : getLangText('choose a %s to upload', fileClassToUpload.singular); - - return ( - - {this.getDragDialog(fileClassToUpload.singular)} - - {dialog} - - - ); - } - } + return ( + + {this.getDragDialog(fileClassToUpload.singular)} + + {dialog} + + + ); } } }); diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index bf4250c5..670be18d 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -17,6 +17,7 @@ import { computeHashOfFile } from '../../utils/file_utils'; import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils'; import { getCookie } from '../../utils/fetch_api_utils'; import { getLangText } from '../../utils/lang_utils'; +import { startTimer, endTimer } from '../../utils/timer_utils'; let ReactS3FineUploader = React.createClass({ propTypes: { @@ -128,7 +129,14 @@ let ReactS3FineUploader = React.createClass({ fileInputElement: React.PropTypes.oneOfType([ React.PropTypes.func, React.PropTypes.element - ]) + ]), + + + + + uploadTests: React.PropTypes.array, + onTestsStart: React.PropTypes.func, + onTestComplete: React.PropTypes.func }, getDefaultProps() { @@ -188,6 +196,11 @@ let ReactS3FineUploader = React.createClass({ }, getInitialState() { + const uploadTestDeferreds = []; + this.props.uploadTests.forEach(() => { + uploadTestDeferreds.push(Q.defer()); + }); + return { filesToUpload: [], uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()), @@ -198,7 +211,11 @@ let ReactS3FineUploader = React.createClass({ hashingProgress: -2, // this is for logging - chunks: {} + chunks: {}, + + + curUploadTest: 0, + uploadTestDeferreds }; }, @@ -226,11 +243,13 @@ let ReactS3FineUploader = React.createClass({ let objectProperties = this.props.objectProperties; objectProperties.key = this.requestKey; + let request = this.props.request; + return { autoUpload: this.props.autoUpload, debug: this.props.debug, objectProperties: objectProperties, // do a special key handling here - request: this.props.request, + request: request, signature: this.props.signature, uploadSuccess: this.props.uploadSuccess, cors: this.props.cors, @@ -274,10 +293,6 @@ let ReactS3FineUploader = React.createClass({ // Cancel uploads and clear previously selected files on the input element cancelUploads(id) { !!id ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll(); - - // Reset the file input element to clear the previously selected files so that - // the user can reselect them again. - this.clearFileSelection(); }, clearFileSelection() { @@ -419,6 +434,13 @@ let ReactS3FineUploader = React.createClass({ }); // onError will catch any errors, so we can ignore them here } else if (!res.error || res.success) { + const completedTime = endTimer() / 1000; + this.props.onTestComplete({ + 'name': this.props.uploadTests[this.state.curUploadTest].name, + 'time': completedTime + }); + + let files = this.state.filesToUpload; // Set the state of the completed file to 'upload successful' in order to @@ -432,27 +454,7 @@ let ReactS3FineUploader = React.createClass({ // Only after the blob has been created server-side, we can make the form submittable. this.createBlob(files[id]) .then(() => { - // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile - // are optional, we'll only trigger them when they're actually defined - if(this.props.submitFile) { - this.props.submitFile(files[id]); - } else { - console.warn('You didn\'t define submitFile as a prop in react-s3-fine-uploader'); - } - - // for explanation, check comment of if statement above - if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) { - // also, lets check if after the completion of this upload, - // the form is ready for submission or not - if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) { - // if so, set uploadstatus to true - this.props.setIsUploadReady(true); - } else { - this.props.setIsUploadReady(false); - } - } else { - console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader'); - } + this.handleDeleteFile(id); }) .catch(this.onErrorPromiseProxy); } @@ -470,6 +472,14 @@ let ReactS3FineUploader = React.createClass({ }, onError(id, name, errorReason, xhr) { + const errorTime = endTimer() / 1000; + this.props.onTestComplete({ + 'name': this.props.uploadTests[this.state.curUploadTest].name, + 'time': errorTime, + 'progress': this.state.filesToUpload[id] ? this.state.filesToUpload[id].progress : 0, + 'error': errorReason + }); + console.logGlobal(errorReason, false, { files: this.state.filesToUpload, chunks: this.state.chunks, @@ -477,10 +487,12 @@ let ReactS3FineUploader = React.createClass({ }); this.props.setIsUploadReady(true); - this.cancelUploads(); + this.cancelUploads(id); let notification = new GlobalNotificationModel(errorReason || this.props.defaultErrorMessage, 'danger', 5000); GlobalNotificationActions.appendGlobalNotification(notification); + + this.state.uploadTestDeferreds[this.state.curUploadTest].resolve(); }, getXhrErrorComment(xhr) { @@ -515,19 +527,6 @@ let ReactS3FineUploader = React.createClass({ let notification = new GlobalNotificationModel(getLangText('File upload canceled'), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); - // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile - // are optional, we'll only trigger them when they're actually defined - if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) { - if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) { - // if so, set uploadstatus to true - this.props.setIsUploadReady(true); - } else { - this.props.setIsUploadReady(false); - } - } else { - console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader'); - } - return true; }, @@ -574,28 +573,9 @@ let ReactS3FineUploader = React.createClass({ onDeleteComplete(id, xhr, isError) { if(isError) { this.setStatusOfFile(id, 'online'); - - let notification = new GlobalNotificationModel(getLangText('There was an error deleting your file.'), 'danger', 10000); - GlobalNotificationActions.appendGlobalNotification(notification); - } else { - let notification = new GlobalNotificationModel(getLangText('File deleted'), 'success', 5000); - GlobalNotificationActions.appendGlobalNotification(notification); } - // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile - // are optional, we'll only trigger them when they're actually defined - if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) { - // also, lets check if after the completion of this upload, - // the form is ready for submission or not - if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) { - // if so, set uploadstatus to true - this.props.setIsUploadReady(true); - } else { - this.props.setIsUploadReady(false); - } - } else { - console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader'); - } + this.state.uploadTestDeferreds[this.state.curUploadTest].resolve(); }, handleDeleteFile(fileId) { @@ -655,13 +635,6 @@ let ReactS3FineUploader = React.createClass({ // for submission this.props.setIsUploadReady(false); - // If multiple set and user already uploaded its work, - // cancel upload - if(!this.props.multiple && this.state.filesToUpload.filter(displayValidFilesFilter).length > 0) { - this.clearFileSelection(); - return; - } - // validate each submitted file if it fits the file size let validFiles = []; for(let i = 0; i < files.length; i++) { @@ -690,99 +663,8 @@ let ReactS3FineUploader = React.createClass({ GlobalNotificationActions.appendGlobalNotification(notification); } - // As mentioned already in the propTypes declaration, in some instances we need to calculate the - // md5 hash of a file locally and just upload a txt file containing that hash. - // - // In the view this only happens when the user is allowed to do local hashing as well - // as when the correct method prop is present ('hash' and not 'upload') - if (this.props.enableLocalHashing && this.props.uploadMethod === 'hash') { - const convertedFilePromises = []; - let overallFileSize = 0; - - // "files" is not a classical Javascript array but a Javascript FileList, therefore - // we can not use map to convert values - for(let i = 0; i < files.length; i++) { - // for calculating the overall progress of all submitted files - // we'll need to calculate the overall sum of all files' sizes - overallFileSize += files[i].size; - - // also, we need to set the files' initial progress value - files[i].progress = 0; - - // since the actual computation of a file's hash is an async task , - // we're using promises to handle that - let hashedFilePromise = computeHashOfFile(files[i]); - convertedFilePromises.push(hashedFilePromise); - } - - // To react after the computation of all files, we define the resolvement - // with the all function for iterables and essentially replace all original files - // with their txt representative - Q.all(convertedFilePromises) - .progress(({index, value: {progress, reject}}) => { - // hashing progress has been aborted from outside - // To get out of the executing, we need to call reject from the - // inside of the promise's execution. - // This is why we're passing (along with value) a function that essentially - // just does that (calling reject(err)) - // - // In the promises catch method, we're then checking if the interruption - // was due to that error or another generic one. - if(this.state.hashingProgress === -1) { - reject(new Error(getLangText('Hashing canceled'))); - } - - // update file's progress - files[index].progress = progress; - - // calculate weighted average for overall progress of all - // currently hashing files - let overallHashingProgress = 0; - for(let i = 0; i < files.length; i++) { - let filesSliceOfOverall = files[i].size / overallFileSize; - overallHashingProgress += filesSliceOfOverall * files[i].progress; - } - - // Multiply by 100, since react-progressbar expects decimal numbers - this.setState({ hashingProgress: overallHashingProgress * 100}); - }) - .then((convertedFiles) => { - // clear hashing progress, since its done - this.setState({ hashingProgress: -2}); - - // actually replacing all files with their txt-hash representative - files = convertedFiles; - - // routine for adding all the files submitted to fineuploader for actual uploading them - // to the server - this.state.uploader.addFiles(files); - this.synchronizeFileLists(files); - - }) - .catch((err) => { - // If the error is that hashing has been canceled, we want to display a success - // message instead of a danger message - let typeOfMessage = 'danger'; - - if(err.message === getLangText('Hashing canceled')) { - typeOfMessage = 'success'; - this.setState({ hashingProgress: -2 }); - } else { - // if there was a more generic error, we also log it - console.logGlobal(err); - } - - let notification = new GlobalNotificationModel(err.message, typeOfMessage, 5000); - GlobalNotificationActions.appendGlobalNotification(notification); - }); - - // if we're not hashing the files locally, we're just going to hand them over to fineuploader - // to upload them to the server - } else { - if(files.length > 0) { - this.state.uploader.addFiles(files); - this.synchronizeFileLists(files); - } + if (files.length > 0) { + this.startTests(files); } }, @@ -886,6 +768,40 @@ let ReactS3FineUploader = React.createClass({ } }, + + startTests(files) { + this.props.onTestsStart(files); + + this.props.uploadTests.reduce((deferred, test, testIndex) => { + return deferred.then(() => { + this.startTest(test, files, testIndex); + return this.state.uploadTestDeferreds[testIndex].promise; + }); + }, Q.when()); + }, + + startTest(test, files, testIndex) { + const uploaderConfig = this.propsToConfig(); + uploaderConfig.request = Object.assign({}, uploaderConfig.request); + uploaderConfig.chunking = Object.assign({}, uploaderConfig.chunking); + + if (!!test.endpoint) { + uploaderConfig.request.endpoint = test.endpoint; + } + if (!!test.chunkSize) { + uploaderConfig.chunking.partSize = test.chunkSize; + } + + this.setState({ + uploader: new fineUploader.s3.FineUploaderBasic(uploaderConfig), + curUploadTest: testIndex + }, () => { + startTimer(); + this.state.uploader.addFiles(files); + this.synchronizeFileLists(files); + }); + }, + render() { const { multiple, diff --git a/js/components/header.js b/js/components/header.js index 51f91318..07cdd09d 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -82,7 +82,7 @@ let Header = React.createClass({ return ( - + ); }, @@ -203,16 +203,15 @@ let Header = React.createClass({ toggleNavKey={0} fixedTop={true}> - - - {navRoutesLinks} diff --git a/js/components/login_container.js b/js/components/login_container.js index 46c14d65..b5ce8642 100644 --- a/js/components/login_container.js +++ b/js/components/login_container.js @@ -37,10 +37,6 @@ let LoginContainer = React.createClass({ message={this.props.message} onLogin={this.props.onLogin} location={this.props.location}/> -
- {getLangText('Not an ascribe user')}? {getLangText('Sign up')}...
- {getLangText('Forgot my password')}? {getLangText('Rescue me')}... -
); } diff --git a/js/components/register_piece.js b/js/components/register_piece.js index 43ac7bb7..3e9a314b 100644 --- a/js/components/register_piece.js +++ b/js/components/register_piece.js @@ -6,18 +6,8 @@ import { History } from 'react-router'; import Col from 'react-bootstrap/lib/Col'; import Row from 'react-bootstrap/lib/Row'; -import WhitelabelActions from '../actions/whitelabel_actions'; -import WhitelabelStore from '../stores/whitelabel_store'; - -import PieceListStore from '../stores/piece_list_store'; -import PieceListActions from '../actions/piece_list_actions'; - import UserStore from '../stores/user_store'; -import GlobalNotificationModel from '../models/global_notification_model'; -import GlobalNotificationActions from '../actions/global_notification_actions'; - -import PropertyCollapsible from './ascribe_forms/property_collapsible'; import RegisterPieceForm from './ascribe_forms/form_register_piece'; import { mergeOptions } from '../utils/general_utils'; @@ -43,25 +33,22 @@ let RegisterPiece = React.createClass( { getInitialState(){ return mergeOptions( UserStore.getState(), - WhitelabelStore.getState(), - PieceListStore.getState(), { selectedLicense: 0, - isFineUploaderActive: false + isFineUploaderActive: false, + uploadInfos: [], + testFileSize: 0, + testStarted: false, + testComplete: false }); }, componentDidMount() { - PieceListStore.listen(this.onChange); UserStore.listen(this.onChange); - WhitelabelStore.listen(this.onChange); - WhitelabelActions.fetchWhitelabel(); }, componentWillUnmount() { - PieceListStore.unlisten(this.onChange); UserStore.unlisten(this.onChange); - WhitelabelStore.unlisten(this.onChange); }, onChange(state) { @@ -75,36 +62,44 @@ let RegisterPiece = React.createClass( { } }, - handleSuccess(response){ - let notification = new GlobalNotificationModel(response.notification, 'success', 10000); - GlobalNotificationActions.appendGlobalNotification(notification); - - // once the user was able to register a piece successfully, we need to make sure to keep - // the piece list up to date - PieceListActions.fetchPieceList( - this.state.page, - this.state.pageSize, - this.state.searchTerm, - this.state.orderBy, - this.state.orderAsc, - this.state.filterBy - ); - - this.history.pushState(null, `/pieces/${response.piece.id}`); + onSingleTestComplete(uploadInfo) { + this.setState({ + uploadInfos: this.state.uploadInfos.concat([uploadInfo]) + }); }, - getSpecifyEditions() { - if(this.state.whitelabel && this.state.whitelabel.acl_create_editions || Object.keys(this.state.whitelabel).length === 0) { + onTestsStart(files) { + this.setState({ + testStarted: true, + testFileSize: files[0].size + }); + }, + + onTestsComplete() { + this.setState({ + testComplete: true + }, () => { + alert('Tests are complete. Please send the results to brett@ascribe.io'); + }); + }, + + getUploadedInfo() { + if (this.state.uploadInfos.length > 0 && this.state.testStarted) { return ( - - {getLangText('Editions')} - - +
+

{this.state.testComplete? 'Results:' : 'Test in progress...'}

+ For file of size: {this.state.testFileSize} +
    + {this.state.uploadInfos.map((uploadInfo) => { + if (!uploadInfo.error) { + return (
  • {uploadInfo.name}: {uploadInfo.time}s
  • ); + } else { + return (
  • Error: {uploadInfo.error} after {uploadInfo.time}s on completing {uploadInfo.progress}%
  • ); + } + })} +
+

Please send these results by screenshot or by copying the values to brett@ascribe.io

+
); } }, @@ -118,11 +113,13 @@ let RegisterPiece = React.createClass( { - {this.props.children} - {this.getSpecifyEditions()} - + isFineUploaderEditable={!this.state.testComplete} + location={this.props.location} + + onSingleTestComplete={this.onSingleTestComplete} + onTestsStart={this.onTestsStart} + onTestsComplete={this.onTestsComplete} /> + {this.getUploadedInfo()} ); diff --git a/js/routes.js b/js/routes.js index 116fb663..a4d19f0f 100644 --- a/js/routes.js +++ b/js/routes.js @@ -32,33 +32,17 @@ let COMMON_ROUTES = ( + component={AuthProxyHandler({to: '/register_piece', when: 'loggedIn'})(LoginContainer)} /> - + component={AuthProxyHandler({to: '/register_piece', when: 'loggedIn'})(SignupContainer)} /> - - - - - - ); diff --git a/js/utils/timer_utils.js b/js/utils/timer_utils.js new file mode 100644 index 00000000..c8452947 --- /dev/null +++ b/js/utils/timer_utils.js @@ -0,0 +1,15 @@ +'use strict' + +var startTime = null; + +export function startTimer() { + startTime = new Date(); +}; + +export function endTimer() { + if (startTime) { + const time = new Date() - startTime; + startTime = null; + return time; + } +}