add progress bar for student media uploads

closes EVAL-1381
flag=none

Test Plan:
- ensure Assignment enhancements is turned on and notorious plugin is
configured
- have a course with a student
- make a media upload Assignment
- attempt to upload a video file as a student and ensure a progress bar
is shown in the modal after clicking submit
- video should upload and modal should close
- media should display within canvas

Change-Id: I61d582120022908b76279d89b6b3209fd9320be8
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/261074
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Adrian Packel <apackel@instructure.com>
QA-Review: Syed Hussain <shussain@instructure.com>
Product-Review: Syed Hussain <shussain@instructure.com>
This commit is contained in:
Kai Bjorkman 2021-03-18 15:56:27 -06:00
parent edaa30c274
commit f63cba7b55
4 changed files with 106 additions and 23 deletions

View File

@ -24,6 +24,8 @@ import {Heading} from '@instructure/ui-heading'
import {Modal} from '@instructure/ui-modal'
import {Tabs} from '@instructure/ui-tabs'
import {px} from '@instructure/ui-utils'
import {ProgressBar} from '@instructure/ui-progress'
import {Text} from '@instructure/ui-text'
import {ACCEPTED_FILE_TYPES} from './acceptedMediaFileTypes'
import LoadingIndicator from './shared/LoadingIndicator'
@ -74,6 +76,8 @@ export default class UploadMedia extends React.Component {
this.state = {
hasUploadedFile: false,
uploading: false,
progress: 0,
selectedPanel: defaultSelectedPanel,
computerFile: props.computerFile || null,
subtitles: [],
@ -109,8 +113,19 @@ export default class UploadMedia extends React.Component {
}
uploadFile(file) {
this.setState({uploading: true})
this.props.onStartUpload && this.props.onStartUpload(file)
saveMediaRecording(file, this.props.contextId, this.props.contextType, this.saveMediaCallback)
saveMediaRecording(
file,
this.props.contextId,
this.props.contextType,
this.saveMediaCallback,
this.onSaveMediaProgress
)
}
onSaveMediaProgress = progress => {
this.setState({progress})
}
saveMediaCallback = async (err, data) => {
@ -139,11 +154,13 @@ export default class UploadMedia extends React.Component {
modalBodySize.width !== prevState.modalBodySize.width ||
modalBodySize.height !== prevState.modalBodySize.height
) {
if (modalBodySize.width > 0 && modalBodySize.height > 0) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({modalBodySize})
}
}
}
}
renderModalBody = () => {
const {
@ -221,9 +238,24 @@ export default class UploadMedia extends React.Component {
return null
}
const {CLOSE_TEXT, SUBMIT_TEXT} = this.props.uploadMediaTranslations.UploadMediaStrings
const {
CLOSE_TEXT,
SUBMIT_TEXT,
PROGRESS_LABEL
} = this.props.uploadMediaTranslations.UploadMediaStrings
return (
<Modal.Footer>
{this.state.uploading && (
<ProgressBar
screenReaderLabel={PROGRESS_LABEL}
valueNow={this.state.progress}
valueMax={100}
renderValue={({valueNow}) => {
return <Text>{valueNow}%</Text>
}}
/>
)}
&nbsp;
<Button onClick={this.onModalClose}> {CLOSE_TEXT} </Button>
&nbsp;
<Button

View File

@ -56,13 +56,14 @@ describe('saveMediaRecording', () => {
done()
})
it('returns error if k5.filreError is dispatched', () => {
it('returns error if k5.fileError is dispatched', () => {
moxios.stubRequest('/api/v1/services/kaltura_session?include_upload_config=1', {
status: 200,
response: mediaServerSession()
})
const doneFunction = jest.fn()
return saveMediaRecording({}, '1', 'course', doneFunction).then(uploader => {
const progressFunction = jest.fn()
return saveMediaRecording({}, '1', 'course', doneFunction, progressFunction).then(uploader => {
uploader.dispatchEvent('K5.fileError', {error: 'womp womp'}, uploader)
expect(doneFunction).toHaveBeenCalledTimes(1)
expect(doneFunction.mock.calls[0][0].error).toBe('womp womp')
@ -75,13 +76,33 @@ describe('saveMediaRecording', () => {
response: mediaServerSession()
})
const doneFunction = jest.fn()
const progressFunction = jest.fn()
const uploadFileFunc = jest.fn()
return saveMediaRecording({file: 'thing'}, '1', 'course', doneFunction).then(uploader => {
return saveMediaRecording({file: 'thing'}, '1', 'course', doneFunction, progressFunction).then(
uploader => {
uploader.uploadFile = uploadFileFunc
uploader.dispatchEvent('K5.ready', uploader)
expect(uploadFileFunc).toHaveBeenCalledTimes(1)
expect(uploadFileFunc.mock.calls[0][0].file).toBe('thing')
}
)
})
it('k5.progress calls progress function when dispatched', () => {
moxios.stubRequest('/api/v1/services/kaltura_session?include_upload_config=1', {
status: 200,
response: mediaServerSession()
})
const doneFunction = jest.fn()
const progressFunction = jest.fn()
const uploadFileFunc = jest.fn()
return saveMediaRecording({file: 'thing'}, '1', 'course', doneFunction, progressFunction).then(
uploader => {
uploader.uploadFile = uploadFileFunc
uploader.dispatchEvent('K5.progress', uploader)
expect(progressFunction).toHaveBeenCalled()
}
)
})
it('k5.complete calls done with canvasMediaObject data if succeeds', () => {
@ -94,7 +115,8 @@ describe('saveMediaRecording', () => {
response: {data: 'media object data'}
})
const doneFunction2 = jest.fn()
return saveMediaRecording({file: 'thing'}, '1', 'course', doneFunction2).then(
const progressFunction = jest.fn()
return saveMediaRecording({file: 'thing'}, '1', 'course', doneFunction2, progressFunction).then(
async uploader => {
uploader.dispatchEvent('K5.complete', {stuff: 'datatatatatatatat'}, uploader)
await new Promise(setTimeout)
@ -118,7 +140,8 @@ describe('saveMediaRecording', () => {
response: {error: 'womp womp'}
})
const doneFunction2 = jest.fn()
return saveMediaRecording({file: 'thing'}, '1', 'course', doneFunction2).then(
const progressFunction = jest.fn()
return saveMediaRecording({file: 'thing'}, '1', 'course', doneFunction2, progressFunction).then(
async uploader => {
uploader.dispatchEvent('K5.complete', {stuff: 'datatatatatatatat'}, uploader)
await new Promise(setTimeout)

View File

@ -20,6 +20,7 @@ import axios from 'axios'
import K5Uploader from '@instructure/k5uploader'
export const VIDEO_SIZE_OPTIONS = {height: '432px', width: '768px'}
const STARTING_PROGRESS_VALUE = 33
function generateUploadOptions(mediatypes, sessionData) {
const sessionDataCopy = JSON.parse(JSON.stringify(sessionData))
@ -42,6 +43,13 @@ function addUploaderReadyEventListeners(uploader, file) {
})
}
function addUploaderProgressEventListeners(uploader, onProgress) {
uploader.addEventListener('K5.progress', progress => {
const percentUploaded = Math.round(progress.loaded / progress.total) * STARTING_PROGRESS_VALUE
onProgress(STARTING_PROGRESS_VALUE + percentUploaded)
})
}
function addUploaderFileErrorEventListeners(uploader, done, file) {
uploader.addEventListener('K5.fileError', error => {
uploader.destroy()
@ -49,7 +57,7 @@ function addUploaderFileErrorEventListeners(uploader, done, file) {
})
}
function addUploaderFileCompleteEventListeners(uploader, context, file, done) {
function addUploaderFileCompleteEventListeners(uploader, context, file, done, onProgress) {
uploader.addEventListener('K5.complete', async mediaServerMediaObject => {
mediaServerMediaObject.contextCode = `${context.contextType}_${context.contextId}`
mediaServerMediaObject.type = `${context.contextType}_${context.contextId}`
@ -67,7 +75,17 @@ function addUploaderFileCompleteEventListeners(uploader, context, file, done) {
}
try {
const canvasMediaObject = await axios.post('/api/v1/media_objects', body)
const config = {
onUploadProgress: progressEvent => {
const startingValue = 2 * STARTING_PROGRESS_VALUE
const percentUploaded =
Math.round(progressEvent.loaded / progressEvent.total) * (STARTING_PROGRESS_VALUE + 1)
if (onProgress) {
onProgress(startingValue + percentUploaded)
}
}
}
const canvasMediaObject = await axios.post('/api/v1/media_objects', body, config)
uploader.destroy()
doDone(done, null, {mediaObject: canvasMediaObject.data, uploadedFile: file})
} catch (ex) {
@ -77,20 +95,32 @@ function addUploaderFileCompleteEventListeners(uploader, context, file, done) {
})
}
export default async function saveMediaRecording(file, contextId, contextType, done) {
export default async function saveMediaRecording(file, contextId, contextType, done, onProgress) {
try {
window.addEventListener('beforeunload', handleUnloadWhileUploading)
const mediaServerSession = await axios.post(
'/api/v1/services/kaltura_session?include_upload_config=1'
)
if (onProgress) {
onProgress(STARTING_PROGRESS_VALUE)
}
const session = generateUploadOptions(
['video', 'audio', 'webm', 'video/webm', 'audio/webm'],
mediaServerSession.data
)
const k5UploaderSession = new K5Uploader(session)
addUploaderReadyEventListeners(k5UploaderSession, file)
if (onProgress) {
addUploaderProgressEventListeners(k5UploaderSession, onProgress)
}
addUploaderFileErrorEventListeners(k5UploaderSession, done, file)
addUploaderFileCompleteEventListeners(k5UploaderSession, {contextId, contextType}, file, done)
addUploaderFileCompleteEventListeners(
k5UploaderSession,
{contextId, contextType},
file,
done,
onProgress
)
return k5UploaderSession
} catch (err) {
doDone(done, err, {uploadedFile: file})
@ -99,20 +129,17 @@ export default async function saveMediaRecording(file, contextId, contextType, d
export async function saveClosedCaptions(mediaId, files) {
const axiosRequests = []
files.forEach(function(file) {
files.forEach(function (file) {
const p = new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = function(e) {
reader.onload = function (e) {
const content = e.target.result
const params = {
content,
locale: file.locale,
'exclude[]': 'sources'
}
axios
.post(`/media_objects/${mediaId}/media_tracks`, params)
.then(resolve)
.catch(reject)
axios.post(`/media_objects/${mediaId}/media_tracks`, params).then(resolve).catch(reject)
}
reader.readAsText(file.file)
})

View File

@ -41,6 +41,7 @@ const MediaCaptureStrings = {
const UploadMediaStrings = {
LOADING_MEDIA: I18n.t('Loading Media'),
PROGRESS_LABEL: I18n.t('Uploading media Progress'),
ADD_CLOSED_CAPTIONS_OR_SUBTITLES: I18n.t('Add CC/Subtitle'),
COMPUTER_PANEL_TITLE: I18n.t('Computer'),
DRAG_FILE_TEXT: I18n.t('Drag a File Here'),