Improved error notifications

This commit is contained in:
shamoon
2023-08-23 23:50:54 -07:00
parent 0ef3a141a8
commit 423e0768f9
14 changed files with 296 additions and 260 deletions

View File

@@ -5,9 +5,26 @@
(hidden)="toastService.closeToast(toast)">
<p>{{toast.content}}</p>
<details *ngIf="toast.error">
<pre class="p-2 m-0 bg-light text-dark">
{{toast.error}}
</pre>
<div class="p-3">
<dl class="row" *ngIf="isDetailedError(toast.error)">
<dt class="col-sm-3 fw-normal text-end">URL</dt>
<dd class="col-sm-9">{{ toast.error.url }}</dd>
<dt class="col-sm-3 fw-normal text-end" i18n>Status</dt>
<dd class="col-sm-9">{{ toast.error.status }} <em>{{ toast.error.statusText }}</em></dd>
<dt class="col-sm-3 fw-normal text-end" i18n>Error</dt>
<dd class="col-sm-9">{{ getErrorText(toast.error) }}</dd>
</dl>
<div class="row">
<div class="col offset-sm-3">
<button class="btn btn-sm btn-outline-dark" (click)="copyError(toast.error)">
<svg class="sidebaricon" fill="currentColor">
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard" />
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check" />
</svg>&nbsp;<ng-container i18n>Copy Raw Error</ng-container>
</button>
</div>
</div>
</div>
</details>
<p class="mb-0" *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p>
</ngb-toast>

View File

@@ -20,8 +20,3 @@
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;
}
pre {
white-space: pre-line;
--bs-bg-opacity: .25;
}

View File

@@ -11,6 +11,32 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'
import { of } from 'rxjs'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
const toasts = [
{
title: 'Title',
content: 'content',
delay: 5000,
},
{
title: 'Error 1',
content: 'Error 1 content',
delay: 5000,
error: 'Error 1 string',
},
{
title: 'Error 2',
content: 'Error 2 content',
delay: 5000,
error: {
url: 'https://example.com',
status: 500,
statusText: 'Internal Server Error',
message: 'Internal server error 500 message',
error: { detail: 'Error 2 message details' },
},
},
]
describe('ToastsComponent', () => {
let component: ToastsComponent
let fixture: ComponentFixture<ToastsComponent>
@@ -24,20 +50,7 @@ describe('ToastsComponent', () => {
{
provide: ToastService,
useValue: {
getToasts: () =>
of([
{
title: 'Title',
content: 'content',
delay: 5000,
},
{
title: 'Error',
content: 'Error content',
delay: 5000,
error: new Error('Error message'),
},
]),
getToasts: () => of(toasts),
},
},
],
@@ -85,10 +98,41 @@ describe('ToastsComponent', () => {
fixture.detectChanges()
expect(fixture.nativeElement.querySelector('details')).not.toBeNull()
expect(fixture.nativeElement.textContent).toContain('Error message')
expect(fixture.nativeElement.textContent).toContain('Error 1 content')
component.ngOnDestroy()
flush()
discardPeriodicTasks()
}))
it('should show error details, support copy', fakeAsync(() => {
component.ngOnInit()
fixture.detectChanges()
expect(fixture.nativeElement.querySelector('details')).not.toBeNull()
expect(fixture.nativeElement.textContent).toContain(
'Error 2 message details'
)
const copySpy = jest.spyOn(navigator.clipboard, 'writeText')
component.copyError(toasts[2].error)
expect(copySpy).toHaveBeenCalled()
component.ngOnDestroy()
flush()
discardPeriodicTasks()
}))
it('should parse error text, add ellipsis', () => {
expect(component.getErrorText(toasts[2].error)).toEqual(
'Error 2 message details'
)
expect(component.getErrorText({ error: 'Error string no detail' })).toEqual(
'Error string no detail'
)
expect(component.getErrorText('Error string')).toEqual('')
expect(
component.getErrorText({ error: new Array(205).join('a') })
).toContain('...')
})
})

View File

@@ -10,17 +10,50 @@ import { Toast, ToastService } from 'src/app/services/toast.service'
export class ToastsComponent implements OnInit, OnDestroy {
constructor(private toastService: ToastService) {}
subscription: Subscription
private subscription: Subscription
toasts: Toast[] = []
public toasts: Toast[] = []
public copied: boolean = false
ngOnDestroy(): void {
this.subscription?.unsubscribe()
}
ngOnInit(): void {
this.subscription = this.toastService
.getToasts()
.subscribe((toasts) => (this.toasts = toasts))
this.subscription = this.toastService.getToasts().subscribe((toasts) => {
this.toasts = toasts
this.toasts.forEach((t) => {
if (typeof t.error === 'string') {
try {
t.error = JSON.parse(t.error)
} catch (e) {}
}
})
})
}
public isDetailedError(error: any): boolean {
return (
typeof error === 'object' &&
'status' in error &&
'statusText' in error &&
'url' in error &&
'message' in error &&
'error' in error
)
}
public copyError(error: any) {
navigator.clipboard.writeText(JSON.stringify(error))
this.copied = true
setTimeout(() => {
this.copied = false
}, 3000)
}
getErrorText(error: any) {
const text: string = error.error?.detail ?? error.error ?? ''
return `${text.slice(0, 200)}${text.length > 200 ? '...' : ''}`
}
}