Add alert badge to button if errors

This commit is contained in:
shamoon 2024-02-12 10:43:52 -08:00
parent c40b2adad7
commit 4f4a7aee14
5 changed files with 94 additions and 26 deletions

View File

@ -7,11 +7,20 @@
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"> <button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
<i-bs class="me-1" name="airplane"></i-bs>&nbsp;<ng-container i18n>Start tour</ng-container> <i-bs class="me-1" name="airplane"></i-bs>&nbsp;<ng-container i18n>Start tour</ng-container>
</button> </button>
<button class="btn btn-sm btn-outline-primary ms-5" (click)="showSystemStatus()" <button class="btn btn-sm btn-outline-primary position-relative ms-5" (click)="showSystemStatus()"
[disabled]="!systemStatus"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }"> *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
<i-bs class="me-1" name="card-checklist"></i-bs>&nbsp;<ng-container i18n>System Status</ng-container> @if (!systemStatus) {
<div class="spinner-border spinner-border-sm me-1" role="status"></div>
} @else {
<i-bs class="me-1" name="card-checklist"></i-bs>
@if (systemStatusHasErrors) {
<span class="badge bg-danger position-absolute top-0 start-100 translate-middle rounded-pill py-1 px-2">!</span>
}
}
&nbsp;<ng-container i18n>System Status</ng-container>
</button> </button>
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-2" href="admin/" target="_blank"> <a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
<ng-container i18n>Open Django Admin</ng-container> <ng-container i18n>Open Django Admin</ng-container>
&nbsp;<i-bs name="arrow-up-right"></i-bs> &nbsp;<i-bs name="arrow-up-right"></i-bs>
</a> </a>

View File

@ -42,6 +42,12 @@ import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component' import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component' import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
import { SystemStatusService } from 'src/app/services/system-status.service'
import {
PaperlessSystemStatus,
PaperlessInstallType,
PaperlessConnectionStatus,
} from 'src/app/data/system-status'
const savedViews = [ const savedViews = [
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true }, { id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
@ -69,6 +75,7 @@ describe('SettingsComponent', () => {
let permissionsService: PermissionsService let permissionsService: PermissionsService
let groupService: GroupService let groupService: GroupService
let modalService: NgbModal let modalService: NgbModal
let systemStatusService: SystemStatusService
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -113,6 +120,7 @@ describe('SettingsComponent', () => {
userService = TestBed.inject(UserService) userService = TestBed.inject(UserService)
permissionsService = TestBed.inject(PermissionsService) permissionsService = TestBed.inject(PermissionsService)
modalService = TestBed.inject(NgbModal) modalService = TestBed.inject(NgbModal)
systemStatusService = TestBed.inject(SystemStatusService)
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest jest
.spyOn(permissionsService, 'currentUserHasObjectPermissions') .spyOn(permissionsService, 'currentUserHasObjectPermissions')
@ -379,6 +387,36 @@ describe('SettingsComponent', () => {
expect(toastErrorSpy).toBeCalled() expect(toastErrorSpy).toBeCalled()
}) })
it('should load system status on initialize, show errors if needed', () => {
const status: PaperlessSystemStatus = {
pngx_version: '2.4.3',
server_os: 'macOS-14.1.1-arm64-arm-64bit',
install_type: PaperlessInstallType.BareMetal,
storage: { total: 494384795648, available: 13573525504 },
database: {
type: 'sqlite',
url: '/paperless-ngx/data/db.sqlite3',
status: PaperlessConnectionStatus.ERROR,
error: null,
migration_status: {
latest_migration: 'socialaccount.0006_alter_socialaccount_extra_data',
unapplied_migrations: [],
},
},
tasks: {
redis_url: 'redis://localhost:6379',
redis_status: PaperlessConnectionStatus.ERROR,
redis_error:
'Error 61 connecting to localhost:6379. Connection refused.',
celery_status: PaperlessConnectionStatus.ERROR,
},
}
jest.spyOn(systemStatusService, 'get').mockReturnValue(of(status))
completeSetup()
expect(component['systemStatus']).toEqual(status) // private
expect(component.systemStatusHasErrors).toBeTruthy()
})
it('should open system status dialog', () => { it('should open system status dialog', () => {
const modalOpenSpy = jest.spyOn(modalService, 'open') const modalOpenSpy = jest.spyOn(modalService, 'open')
completeSetup() completeSetup()

View File

@ -9,7 +9,11 @@ import {
} from '@angular/core' } from '@angular/core'
import { FormGroup, FormControl } from '@angular/forms' import { FormGroup, FormControl } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { NgbModal, NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap' import {
NgbModal,
NgbModalRef,
NgbNavChangeEvent,
} from '@ng-bootstrap/ng-bootstrap'
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms' import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
import { TourService } from 'ngx-ui-tour-ng-bootstrap' import { TourService } from 'ngx-ui-tour-ng-bootstrap'
import { import {
@ -41,6 +45,11 @@ import {
import { ToastService, Toast } from 'src/app/services/toast.service' import { ToastService, Toast } from 'src/app/services/toast.service'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component' import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
import { SystemStatusService } from 'src/app/services/system-status.service'
import {
PaperlessConnectionStatus,
PaperlessSystemStatus,
} from 'src/app/data/system-status'
enum SettingsNavIDs { enum SettingsNavIDs {
General = 1, General = 1,
@ -112,6 +121,17 @@ export class SettingsComponent
users: User[] users: User[]
groups: Group[] groups: Group[]
private systemStatus: PaperlessSystemStatus
get systemStatusHasErrors(): boolean {
return (
this.systemStatus.database.status === PaperlessConnectionStatus.ERROR ||
this.systemStatus.tasks.redis_status ===
PaperlessConnectionStatus.ERROR ||
this.systemStatus.tasks.celery_status === PaperlessConnectionStatus.ERROR
)
}
get computedDateLocale(): string { get computedDateLocale(): string {
return ( return (
this.settingsForm.value.dateLocale || this.settingsForm.value.dateLocale ||
@ -133,7 +153,8 @@ export class SettingsComponent
private groupsService: GroupService, private groupsService: GroupService,
private router: Router, private router: Router,
public permissionsService: PermissionsService, public permissionsService: PermissionsService,
private modalService: NgbModal private modalService: NgbModal,
private systemStatusService: SystemStatusService
) { ) {
super() super()
this.settings.settingsSaved.subscribe(() => { this.settings.settingsSaved.subscribe(() => {
@ -362,6 +383,17 @@ export class SettingsComponent
// prevents loss of unsaved changes // prevents loss of unsaved changes
this.settingsForm.patchValue(currentFormValue) this.settingsForm.patchValue(currentFormValue)
} }
if (
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.Admin
)
) {
this.systemStatusService.get().subscribe((status) => {
this.systemStatus = status
})
}
} }
private emptyGroup(group: FormGroup) { private emptyGroup(group: FormGroup) {
@ -569,8 +601,12 @@ export class SettingsComponent
} }
showSystemStatus() { showSystemStatus() {
this.modalService.open(SystemStatusDialogComponent, { const modal: NgbModalRef = this.modalService.open(
size: 'xl', SystemStatusDialogComponent,
}) {
size: 'xl',
}
)
modal.componentInstance.status = this.systemStatus
} }
} }

View File

@ -48,9 +48,7 @@ const status: PaperlessSystemStatus = {
describe('SystemStatusDialogComponent', () => { describe('SystemStatusDialogComponent', () => {
let component: SystemStatusDialogComponent let component: SystemStatusDialogComponent
let fixture: ComponentFixture<SystemStatusDialogComponent> let fixture: ComponentFixture<SystemStatusDialogComponent>
let systemStatusService: SystemStatusService
let clipboard: Clipboard let clipboard: Clipboard
let getStatusSpy
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
@ -66,21 +64,13 @@ describe('SystemStatusDialogComponent', () => {
], ],
}).compileComponents() }).compileComponents()
systemStatusService = TestBed.inject(SystemStatusService)
getStatusSpy = jest
.spyOn(systemStatusService, 'get')
.mockReturnValue(of(status))
fixture = TestBed.createComponent(SystemStatusDialogComponent) fixture = TestBed.createComponent(SystemStatusDialogComponent)
component = fixture.componentInstance component = fixture.componentInstance
component.status = status
clipboard = TestBed.inject(Clipboard) clipboard = TestBed.inject(Clipboard)
fixture.detectChanges() fixture.detectChanges()
}) })
it('should subscribe to system status service', () => {
expect(getStatusSpy).toHaveBeenCalled()
expect(component.status).toEqual(status)
})
it('should close the active modal', () => { it('should close the active modal', () => {
const closeSpy = jest.spyOn(component.activeModal, 'close') const closeSpy = jest.spyOn(component.activeModal, 'close')
component.close() component.close()

View File

@ -1,4 +1,4 @@
import { Component } from '@angular/core' import { Component, Input } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { PaperlessSystemStatus } from 'src/app/data/system-status' import { PaperlessSystemStatus } from 'src/app/data/system-status'
import { SystemStatusService } from 'src/app/services/system-status.service' import { SystemStatusService } from 'src/app/services/system-status.service'
@ -16,13 +16,8 @@ export class SystemStatusDialogComponent {
constructor( constructor(
public activeModal: NgbActiveModal, public activeModal: NgbActiveModal,
private systemStatusService: SystemStatusService,
private clipboard: Clipboard private clipboard: Clipboard
) { ) {}
this.systemStatusService.get().subscribe((status) => {
this.status = status
})
}
public close() { public close() {
this.activeModal.close() this.activeModal.close()