aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2020-11-23 10:45:42 +0100
committerChocobozzz <chocobozzz@cpy.re>2020-12-03 10:15:49 +0100
commitd4132d3f56b392a2e4e632db59e6644e4851228e (patch)
treebe903734aa9ce3d669c40656a0f81bd3e9c7978c /client/src/app
parent4a54a93941d1c095bf249331df799c51e39c3774 (diff)
downloadPeerTube-d4132d3f56b392a2e4e632db59e6644e4851228e.tar.gz
PeerTube-d4132d3f56b392a2e4e632db59e6644e4851228e.tar.zst
PeerTube-d4132d3f56b392a2e4e632db59e6644e4851228e.zip
more explicit error messages for file uploads
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.ts8
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html19
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss12
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts57
-rw-r--r--client/src/app/core/notification/notifier.service.ts20
-rw-r--r--client/src/app/helpers/utils.ts33
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts6
7 files changed, 116 insertions, 39 deletions
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts
index 7ea4610d4..d5d019b35 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts
@@ -1,6 +1,8 @@
1import { ViewportScroller } from '@angular/common' 1import { ViewportScroller } from '@angular/common'
2import { HttpErrorResponse } from '@angular/common/http'
2import { AfterViewChecked, Component, OnInit } from '@angular/core' 3import { AfterViewChecked, Component, OnInit } from '@angular/core'
3import { AuthService, Notifier, User, UserService } from '@app/core' 4import { AuthService, Notifier, User, UserService } from '@app/core'
5import { uploadErrorHandler } from '@app/helpers'
4 6
5@Component({ 7@Component({
6 selector: 'my-account-settings', 8 selector: 'my-account-settings',
@@ -44,7 +46,11 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked {
44 this.user.updateAccountAvatar(data.avatar) 46 this.user.updateAccountAvatar(data.avatar)
45 }, 47 },
46 48
47 err => this.notifier.error(err.message) 49 (err: HttpErrorResponse) => uploadErrorHandler({
50 err,
51 name: $localize`avatar`,
52 notifier: this.notifier
53 })
48 ) 54 )
49 } 55 }
50} 56}
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
index 677fa1197..88ee4e32a 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
@@ -44,17 +44,30 @@
44 </div> 44 </div>
45</div> 45</div>
46 46
47<!-- Upload progress/cancel/error/success header -->
47<div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel"> 48<div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel">
48 <div class="progress" i18n-title title="Total video quota"> 49 <div class="progress" i18n-title title="Total video uploaded">
49 <div class="progress-bar" role="progressbar" [style]="{ width: videoUploadPercents + '%' }" [attr.aria-valuenow]="videoUploadPercents" aria-valuemin="0" [attr.aria-valuemax]="100"> 50 <div class="progress-bar" role="progressbar" [style]="{ width: videoUploadPercents + '%' }" [attr.aria-valuenow]="videoUploadPercents" aria-valuemin="0" [attr.aria-valuemax]="100">
50 <span *ngIf="videoUploadPercents === 100 && videoUploaded === false" i18n>Processing…</span> 51 <span *ngIf="videoUploadPercents === 100 && videoUploaded === false" i18n>Processing…</span>
51 <span *ngIf="videoUploadPercents !== 100 || videoUploaded">{{ videoUploadPercents }}%</span> 52 <span *ngIf="videoUploadPercents !== 100 || videoUploaded">{{ videoUploadPercents }}%</span>
52 </div> 53 </div>
53 </div> 54 </div>
54 <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" /> 55 <input *ngIf="videoUploaded === false" type="button" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()" />
55</div> 56</div>
56 57
57<div *ngIf="error" class="alert alert-danger"> 58<div *ngIf="error && enableRetryAfterError" class="upload-progress-retry">
59 <div class="progress">
60 <div class="progress-bar red" role="progressbar" [style]="{ width: '100%' }" [attr.aria-valuenow]="100" aria-valuemin="0" [attr.aria-valuemax]="100">
61 <span>{{ error }}</span>
62 </div>
63 </div>
64 <div class="btn-group" role="group">
65 <input type="button" class="btn" i18n-value="Retry failed upload of a video" value="Retry" (click)="retryUpload()" />
66 <input type="button" class="btn" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()" />
67 </div>
68</div>
69
70<div *ngIf="error && !enableRetryAfterError" class="alert alert-danger">
58 <div i18n>Sorry, but something went wrong</div> 71 <div i18n>Sorry, but something went wrong</div>
59 {{ error }} 72 {{ error }}
60</div> 73</div>
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss
index 9ebfa2f2f..9549257f6 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss
@@ -16,27 +16,27 @@
16 } 16 }
17} 17}
18 18
19.upload-progress-retry,
19.upload-progress-cancel { 20.upload-progress-cancel {
20 display: flex; 21 display: flex;
21 margin-top: 25px;
22 margin-bottom: 40px; 22 margin-bottom: 40px;
23 23
24 .progress { 24 .progress {
25 @include progressbar; 25 @include progressbar;
26 flex-grow: 1; 26 flex-grow: 1;
27 height: 30px; 27 height: 30px;
28 font-size: 15px; 28 font-size: 14px;
29 background-color: rgba(11, 204, 41, 0.16); 29 background-color: rgba(11, 204, 41, 0.16);
30 30
31 .progress-bar { 31 .progress-bar {
32 background-color: $green; 32 background-color: $green;
33 line-height: 30px; 33 line-height: 30px;
34 text-align: left; 34 text-align: left;
35 font-weight: $font-bold; 35 font-weight: $font-semibold;
36 36
37 span { 37 span {
38 color: pvar(--mainBackgroundColor); 38 color: pvar(--mainBackgroundColor);
39 margin-left: 18px; 39 margin-left: 13px;
40 } 40 }
41 } 41 }
42 } 42 }
@@ -47,4 +47,8 @@
47 47
48 margin-left: 10px; 48 margin-left: 10px;
49 } 49 }
50
51 .btn-group > input:not(:first-child) {
52 margin-left: 0;
53 }
50} 54}
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
index 258f5c7a0..fdd0a56e5 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
@@ -1,9 +1,9 @@
1import { Subscription } from 'rxjs' 1import { Subscription } from 'rxjs'
2import { HttpEventType, HttpResponse } from '@angular/common/http' 2import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http'
3import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' 3import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
4import { Router } from '@angular/router' 4import { Router } from '@angular/router'
5import { AuthService, CanComponentDeactivate, Notifier, ServerService, UserService } from '@app/core' 5import { AuthService, CanComponentDeactivate, Notifier, ServerService, UserService } from '@app/core'
6import { scrollToTop } from '@app/helpers' 6import { scrollToTop, uploadErrorHandler } from '@app/helpers'
7import { FormValidatorService } from '@app/shared/shared-forms' 7import { FormValidatorService } from '@app/shared/shared-forms'
8import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' 8import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
9import { LoadingBarService } from '@ngx-loading-bar/core' 9import { LoadingBarService } from '@ngx-loading-bar/core'
@@ -41,11 +41,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
41 id: 0, 41 id: 0,
42 uuid: '' 42 uuid: ''
43 } 43 }
44 formData: FormData
44 45
45 waitTranscodingEnabled = true 46 waitTranscodingEnabled = true
46 previewfileUpload: File 47 previewfileUpload: File
47 48
48 error: string 49 error: string
50 enableRetryAfterError: boolean
49 51
50 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC 52 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
51 53
@@ -118,6 +120,12 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
118 this.uploadFirstStep() 120 this.uploadFirstStep()
119 } 121 }
120 122
123 retryUpload () {
124 this.enableRetryAfterError = false
125 this.error = ''
126 this.uploadVideo()
127 }
128
121 cancelUpload () { 129 cancelUpload () {
122 if (this.videoUploadObservable !== null) { 130 if (this.videoUploadObservable !== null) {
123 this.videoUploadObservable.unsubscribe() 131 this.videoUploadObservable.unsubscribe()
@@ -127,6 +135,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
127 this.videoUploadObservable = null 135 this.videoUploadObservable = null
128 136
129 this.firstStepError.emit() 137 this.firstStepError.emit()
138 this.enableRetryAfterError = false
139 this.error = ''
130 140
131 this.notifier.info($localize`Upload cancelled`) 141 this.notifier.info($localize`Upload cancelled`)
132 } 142 }
@@ -163,20 +173,20 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
163 const downloadEnabled = true 173 const downloadEnabled = true
164 const channelId = this.firstStepChannelId.toString() 174 const channelId = this.firstStepChannelId.toString()
165 175
166 const formData = new FormData() 176 this.formData = new FormData()
167 formData.append('name', name) 177 this.formData.append('name', name)
168 // Put the video "private" -> we are waiting the user validation of the second step 178 // Put the video "private" -> we are waiting the user validation of the second step
169 formData.append('privacy', VideoPrivacy.PRIVATE.toString()) 179 this.formData.append('privacy', VideoPrivacy.PRIVATE.toString())
170 formData.append('nsfw', '' + nsfw) 180 this.formData.append('nsfw', '' + nsfw)
171 formData.append('commentsEnabled', '' + commentsEnabled) 181 this.formData.append('commentsEnabled', '' + commentsEnabled)
172 formData.append('downloadEnabled', '' + downloadEnabled) 182 this.formData.append('downloadEnabled', '' + downloadEnabled)
173 formData.append('waitTranscoding', '' + waitTranscoding) 183 this.formData.append('waitTranscoding', '' + waitTranscoding)
174 formData.append('channelId', '' + channelId) 184 this.formData.append('channelId', '' + channelId)
175 formData.append('videofile', videofile) 185 this.formData.append('videofile', videofile)
176 186
177 if (this.previewfileUpload) { 187 if (this.previewfileUpload) {
178 formData.append('previewfile', this.previewfileUpload) 188 this.formData.append('previewfile', this.previewfileUpload)
179 formData.append('thumbnailfile', this.previewfileUpload) 189 this.formData.append('thumbnailfile', this.previewfileUpload)
180 } 190 }
181 191
182 this.isUploadingVideo = true 192 this.isUploadingVideo = true
@@ -190,7 +200,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
190 previewfile: this.previewfileUpload 200 previewfile: this.previewfileUpload
191 }) 201 })
192 202
193 this.videoUploadObservable = this.videoService.uploadVideo(formData).subscribe( 203 this.uploadVideo()
204 }
205
206 uploadVideo () {
207 this.videoUploadObservable = this.videoService.uploadVideo(this.formData).subscribe(
194 event => { 208 event => {
195 if (event.type === HttpEventType.UploadProgress) { 209 if (event.type === HttpEventType.UploadProgress) {
196 this.videoUploadPercents = Math.round(100 * event.loaded / event.total) 210 this.videoUploadPercents = Math.round(100 * event.loaded / event.total)
@@ -203,13 +217,18 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
203 } 217 }
204 }, 218 },
205 219
206 err => { 220 (err: HttpErrorResponse) => {
207 // Reset progress 221 // Reset progress (but keep isUploadingVideo true)
208 this.isUploadingVideo = false
209 this.videoUploadPercents = 0 222 this.videoUploadPercents = 0
210 this.videoUploadObservable = null 223 this.videoUploadObservable = null
211 this.firstStepError.emit() 224 this.enableRetryAfterError = true
212 this.notifier.error(err.message) 225
226 this.error = uploadErrorHandler({
227 err,
228 name: $localize`video`,
229 notifier: this.notifier,
230 sticky: false
231 })
213 } 232 }
214 ) 233 )
215 } 234 }
diff --git a/client/src/app/core/notification/notifier.service.ts b/client/src/app/core/notification/notifier.service.ts
index f736672bb..165bb0c76 100644
--- a/client/src/app/core/notification/notifier.service.ts
+++ b/client/src/app/core/notification/notifier.service.ts
@@ -7,31 +7,35 @@ export class Notifier {
7 7
8 constructor (private messageService: MessageService) { } 8 constructor (private messageService: MessageService) { }
9 9
10 info (text: string, title?: string, timeout?: number) { 10 info (text: string, title?: string, timeout?: number, sticky?: boolean) {
11 if (!title) title = $localize`Info` 11 if (!title) title = $localize`Info`
12 12
13 return this.notify('info', text, title, timeout) 13 console.info(`${title}: ${text}`)
14 return this.notify('info', text, title, timeout, sticky)
14 } 15 }
15 16
16 error (text: string, title?: string, timeout?: number) { 17 error (text: string, title?: string, timeout?: number, sticky?: boolean) {
17 if (!title) title = $localize`Error` 18 if (!title) title = $localize`Error`
18 19
19 return this.notify('error', text, title, timeout) 20 console.error(`${title}: ${text}`)
21 return this.notify('error', text, title, timeout, sticky)
20 } 22 }
21 23
22 success (text: string, title?: string, timeout?: number) { 24 success (text: string, title?: string, timeout?: number, sticky?: boolean) {
23 if (!title) title = $localize`Success` 25 if (!title) title = $localize`Success`
24 26
25 return this.notify('success', text, title, timeout) 27 console.log(`${title}: ${text}`)
28 return this.notify('success', text, title, timeout, sticky)
26 } 29 }
27 30
28 private notify (severity: 'success' | 'info' | 'warn' | 'error', text: string, title: string, timeout?: number) { 31 private notify (severity: 'success' | 'info' | 'warn' | 'error', text: string, title: string, timeout?: number, sticky?: boolean) {
29 this.messageService.add({ 32 this.messageService.add({
30 severity, 33 severity,
31 summary: title, 34 summary: title,
32 detail: text, 35 detail: text,
33 closable: true, 36 closable: true,
34 life: timeout || this.TIMEOUT 37 life: timeout || this.TIMEOUT,
38 sticky
35 }) 39 })
36 } 40 }
37} 41}
diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts
index 9c805b4ca..f96f26fff 100644
--- a/client/src/app/helpers/utils.ts
+++ b/client/src/app/helpers/utils.ts
@@ -1,7 +1,10 @@
1import { DatePipe } from '@angular/common' 1import { DatePipe } from '@angular/common'
2import { HttpErrorResponse } from '@angular/common/http'
3import { Notifier } from '@app/core'
2import { SelectChannelItem } from '@app/shared/shared-forms' 4import { SelectChannelItem } from '@app/shared/shared-forms'
3import { environment } from '../../environments/environment' 5import { environment } from '../../environments/environment'
4import { AuthService } from '../core/auth' 6import { AuthService } from '../core/auth'
7import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
5 8
6// Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript 9// Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
7function getParameterByName (name: string, url: string) { 10function getParameterByName (name: string, url: string) {
@@ -172,6 +175,33 @@ function isXPercentInViewport (el: HTMLElement, percentVisible: number) {
172 ) 175 )
173} 176}
174 177
178function uploadErrorHandler (parameters: {
179 err: HttpErrorResponse
180 name: string
181 notifier: Notifier
182 sticky?: boolean
183}) {
184 const { err, name, notifier, sticky } = { sticky: false, ...parameters }
185 const title = $localize`The upload failed`
186 let message = err.message
187
188 if (err instanceof ErrorEvent) { // network error
189 message = $localize`The connection was interrupted`
190 notifier.error(message, title, null, sticky)
191 } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) {
192 message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)`
193 notifier.error(message, title, null, sticky)
194 } else if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) {
195 const maxFileSize = err.headers?.get('X-File-Maximum-Size') || '8G'
196 message = $localize`Your ${name} file was too large (max. size: ${maxFileSize})`
197 notifier.error(message, title, null, sticky)
198 } else {
199 notifier.error(err.message, title)
200 }
201
202 return message
203}
204
175export { 205export {
176 sortBy, 206 sortBy,
177 durationToString, 207 durationToString,
@@ -187,5 +217,6 @@ export {
187 removeElementFromArray, 217 removeElementFromArray,
188 scrollToTop, 218 scrollToTop,
189 isInViewport, 219 isInViewport,
190 isXPercentInViewport 220 isXPercentInViewport,
221 uploadErrorHandler
191} 222}
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 70be5d7d2..59860c5cb 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -1,6 +1,6 @@
1import { Observable } from 'rxjs' 1import { Observable, of, throwError } from 'rxjs'
2import { catchError, map, switchMap } from 'rxjs/operators' 2import { catchError, map, mergeMap, switchMap } from 'rxjs/operators'
3import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' 3import { HttpClient, HttpErrorResponse, HttpParams, HttpRequest } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService, AuthService } from '@app/core' 5import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService, AuthService } from '@app/core'
6import { objectToFormData } from '@app/helpers' 6import { objectToFormData } from '@app/helpers'