Frontend system status
This commit is contained in:
@@ -114,6 +114,8 @@ import { FileComponent } from './components/common/input/file/file.component'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component'
|
||||
import { MonetaryComponent } from './components/common/input/monetary/monetary.component'
|
||||
import { SystemStatusDialogComponent } from './components/common/system-status-dialog/system-status-dialog.component'
|
||||
import { NgxFilesizeModule } from 'ngx-filesize'
|
||||
import {
|
||||
archive,
|
||||
arrowCounterclockwise,
|
||||
@@ -129,12 +131,14 @@ import {
|
||||
boxes,
|
||||
calendar,
|
||||
calendarEvent,
|
||||
cardChecklist,
|
||||
caretDown,
|
||||
caretUp,
|
||||
chatLeftText,
|
||||
check,
|
||||
check2All,
|
||||
checkAll,
|
||||
checkCircleFill,
|
||||
checkLg,
|
||||
chevronDoubleLeft,
|
||||
chevronDoubleRight,
|
||||
@@ -149,6 +153,7 @@ import {
|
||||
download,
|
||||
envelope,
|
||||
exclamationTriangle,
|
||||
exclamationTriangleFill,
|
||||
eye,
|
||||
fileEarmark,
|
||||
fileEarmarkCheck,
|
||||
@@ -214,12 +219,14 @@ const icons = {
|
||||
boxes,
|
||||
calendar,
|
||||
calendarEvent,
|
||||
cardChecklist,
|
||||
caretDown,
|
||||
caretUp,
|
||||
chatLeftText,
|
||||
check,
|
||||
check2All,
|
||||
checkAll,
|
||||
checkCircleFill,
|
||||
checkLg,
|
||||
chevronDoubleLeft,
|
||||
chevronDoubleRight,
|
||||
@@ -234,6 +241,7 @@ const icons = {
|
||||
download,
|
||||
envelope,
|
||||
exclamationTriangle,
|
||||
exclamationTriangleFill,
|
||||
eye,
|
||||
fileEarmark,
|
||||
fileEarmarkCheck,
|
||||
@@ -445,6 +453,7 @@ function initializeApp(settings: SettingsService) {
|
||||
FileComponent,
|
||||
ConfirmButtonComponent,
|
||||
MonetaryComponent,
|
||||
SystemStatusDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -459,6 +468,7 @@ function initializeApp(settings: SettingsService) {
|
||||
TourNgBootstrapModule,
|
||||
DragDropModule,
|
||||
NgxBootstrapIconsModule.pick(icons),
|
||||
NgxFilesizeModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
||||
@@ -58,6 +58,10 @@
|
||||
*pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }">
|
||||
<i-bs class="me-2" name="gear"></i-bs><ng-container i18n>Settings</ng-container>
|
||||
</a>
|
||||
<button ngbDropdownItem class="nav-link" (click)="showSystemStatus()"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
||||
<i-bs class="me-2" name="card-checklist"></i-bs> <ng-container i18n>System Status</ng-container>
|
||||
</button>
|
||||
<a ngbDropdownItem class="nav-link d-flex" href="accounts/logout/" (click)="onLogout()">
|
||||
<i-bs class="me-2" name="door-open"></i-bs><ng-container i18n>Logout</ng-container>
|
||||
</a>
|
||||
|
||||
@@ -38,6 +38,7 @@ import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
|
||||
import { SavedView } from 'src/app/data/saved-view'
|
||||
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { SystemStatusDialogComponent } from '../common/system-status-dialog/system-status-dialog.component'
|
||||
|
||||
const saved_views = [
|
||||
{
|
||||
@@ -415,4 +416,10 @@ describe('AppFrameComponent', () => {
|
||||
expect(toastErrorSpy).toHaveBeenCalledTimes(2)
|
||||
expect(toastInfoSpy).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should open system status dialog', () => {
|
||||
const modalOpenSpy = jest.spyOn(modalService, 'open')
|
||||
component.showSystemStatus()
|
||||
expect(modalOpenSpy).toHaveBeenCalledWith(SystemStatusDialogComponent)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
} from '@angular/cdk/drag-drop'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
||||
import { SystemStatusDialogComponent } from '../common/system-status-dialog/system-status-dialog.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-app-frame',
|
||||
@@ -316,4 +317,8 @@ export class AppFrameComponent
|
||||
onLogout() {
|
||||
this.openDocumentsService.closeAll()
|
||||
}
|
||||
|
||||
showSystemStatus() {
|
||||
this.modalService.open(SystemStatusDialogComponent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modal-basic-title" i18n>System Status</h5>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@if (!status) {
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center">
|
||||
<div>
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<h5 i18n>General Info</h5>
|
||||
<dl>
|
||||
<dt i18n>Paperless-ngx Version:</dt>
|
||||
<dd>{{status.pngx_version}}</dd>
|
||||
<dt i18n>Install Type:</dt>
|
||||
<dd>{{status.install_type}}</dd>
|
||||
<dt i18n>Server OS:</dt>
|
||||
<dd>{{status.server_os}}</dd>
|
||||
<dt i18n>Media Storage:</dt>
|
||||
<dd>
|
||||
<ngb-progressbar style="height: 4px;" class="my-2" type="primary" [max]="status.storage.total" [value]="status.storage.total - status.storage.available"></ngb-progressbar>
|
||||
<span class="small">{{status.storage.available | filesize}} <ng-container i18n>available</ng-container> ({{status.storage.total | filesize}} <ng-container i18n>total</ng-container>)</span>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<h5 i18n>Database</h5>
|
||||
<dl>
|
||||
<dt i18n>Type:</dt>
|
||||
<dd>{{status.database.type}}</dd>
|
||||
<dt i18n>URL:</dt>
|
||||
<dd>{{status.database.url}}</dd>
|
||||
<dt i18n>Status:</dt>
|
||||
<dd>
|
||||
{{status.database.status}}
|
||||
@if (status.database.status === 'OK') {
|
||||
<i-bs name="check-circle-fill" class="text-success ms-1"></i-bs>
|
||||
} @else {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-1" ngbPopover="{{status.database.error}}" triggers="mouseenter:mouseleave"></i-bs>
|
||||
}
|
||||
</dd>
|
||||
<dt i18n>Migration Status:</dt>
|
||||
<dd>
|
||||
@if (status.database.migration_status.unapplied_migrations.length === 0) {
|
||||
<ng-container>Up to date</ng-container><i-bs name="check-circle-fill" class="text-success ms-1" [ngbPopover]="migrationStatus" triggers="mouseenter:mouseleave"></i-bs>
|
||||
} @else {
|
||||
<ng-container>{{status.database.migration_status.unapplied_migrations.length}} Pending</ng-container><i-bs name="exclamation-triangle-fill" class="text-warning ms-1" [ngbPopover]="migrationStatus" triggers="mouseenter:mouseleave"></i-bs>
|
||||
}
|
||||
<ng-template #migrationStatus>
|
||||
<ng-container i18n>Latest Migration</ng-container>: {{status.database.migration_status.latest_migration}}<br/>
|
||||
@if (status.database.migration_status.unapplied_migrations.length > 0) {
|
||||
<ng-container i18n>Pending Migrations</ng-container>:
|
||||
<ul>
|
||||
@for (migration of status.database.migration_status.unapplied_migrations; track migration) {
|
||||
<li>{{migration}}</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</ng-template>
|
||||
</dd>
|
||||
@if (status.database.migration_status.unapplied_migrations.length > 0) {
|
||||
<dt i18n>Pending Migrations:</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
@for (migration of status.database.migration_status.unapplied_migrations; track migration) {
|
||||
<li>{{migration}}</li>
|
||||
}
|
||||
</ul>
|
||||
</dd>
|
||||
}
|
||||
</dl>
|
||||
|
||||
<h5 i18n>Redis</h5>
|
||||
<dl>
|
||||
<dt i18n>URL:</dt>
|
||||
<dd>{{status.redis.url}}</dd>
|
||||
<dt i18n>Status:</dt>
|
||||
<dd>
|
||||
{{status.redis.status}}
|
||||
@if (status.redis.status === 'OK') {
|
||||
<i-bs name="check-circle-fill" class="text-success ms-1"></i-bs>
|
||||
} @else {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-1" ngbPopover="{{status.redis.error}}" triggers="mouseenter:mouseleave"></i-bs>
|
||||
}
|
||||
</dd>
|
||||
</dl>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="copy()">
|
||||
@if (!copied) {
|
||||
<i-bs name="clipboard-fill"></i-bs>
|
||||
}
|
||||
@if (copied) {
|
||||
<i-bs name="clipboard-check-fill"></i-bs>
|
||||
}
|
||||
<ng-container i18n>Copy</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import {
|
||||
NgbActiveModal,
|
||||
NgbModalModule,
|
||||
NgbPopoverModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard'
|
||||
import { SystemStatusService } from 'src/app/services/system-status.service'
|
||||
import { SystemStatusDialogComponent } from './system-status-dialog.component'
|
||||
import { of } from 'rxjs'
|
||||
import {
|
||||
PaperlessConnectionStatus,
|
||||
PaperlessInstallType,
|
||||
PaperlessSystemStatus,
|
||||
} from 'src/app/data/system-status'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { NgxFilesizeModule } from 'ngx-filesize'
|
||||
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
url: 'redis://localhost:6379',
|
||||
status: PaperlessConnectionStatus.ERROR,
|
||||
error: 'Error 61 connecting to localhost:6379. Connection refused.',
|
||||
},
|
||||
}
|
||||
|
||||
describe('SystemStatusDialogComponent', () => {
|
||||
let component: SystemStatusDialogComponent
|
||||
let fixture: ComponentFixture<SystemStatusDialogComponent>
|
||||
let systemStatusService: SystemStatusService
|
||||
let clipboard: Clipboard
|
||||
let getStatusSpy
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [SystemStatusDialogComponent],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
NgbModalModule,
|
||||
ClipboardModule,
|
||||
HttpClientTestingModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
NgxFilesizeModule,
|
||||
NgbPopoverModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
systemStatusService = TestBed.inject(SystemStatusService)
|
||||
getStatusSpy = jest
|
||||
.spyOn(systemStatusService, 'get')
|
||||
.mockReturnValue(of(status))
|
||||
fixture = TestBed.createComponent(SystemStatusDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
clipboard = TestBed.inject(Clipboard)
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should subscribe to system status service', () => {
|
||||
expect(getStatusSpy).toHaveBeenCalled()
|
||||
expect(component.status).toEqual(status)
|
||||
})
|
||||
|
||||
it('should close the active modal', () => {
|
||||
const closeSpy = jest.spyOn(component.activeModal, 'close')
|
||||
component.close()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should copy the system status to clipboard', fakeAsync(() => {
|
||||
jest.spyOn(clipboard, 'copy')
|
||||
component.copy()
|
||||
expect(clipboard.copy).toHaveBeenCalledWith(
|
||||
JSON.stringify(component.status)
|
||||
)
|
||||
expect(component.copied).toBeTruthy()
|
||||
tick(3000)
|
||||
expect(component.copied).toBeFalsy()
|
||||
}))
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PaperlessSystemStatus } from 'src/app/data/system-status'
|
||||
import { SystemStatusService } from 'src/app/services/system-status.service'
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-system-status-dialog',
|
||||
templateUrl: './system-status-dialog.component.html',
|
||||
styleUrl: './system-status-dialog.component.scss',
|
||||
})
|
||||
export class SystemStatusDialogComponent {
|
||||
public status: PaperlessSystemStatus
|
||||
|
||||
public copied: boolean = false
|
||||
|
||||
constructor(
|
||||
public activeModal: NgbActiveModal,
|
||||
private systemStatusService: SystemStatusService,
|
||||
private clipboard: Clipboard
|
||||
) {
|
||||
this.systemStatusService.get().subscribe((status) => {
|
||||
this.status = status
|
||||
})
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.activeModal.close()
|
||||
}
|
||||
|
||||
public copy() {
|
||||
this.clipboard.copy(JSON.stringify(this.status))
|
||||
this.copied = true
|
||||
setTimeout(() => {
|
||||
this.copied = false
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
34
src-ui/src/app/data/system-status.ts
Normal file
34
src-ui/src/app/data/system-status.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export enum PaperlessInstallType {
|
||||
Containerized = 'containerized',
|
||||
BareMetal = 'bare-metal',
|
||||
}
|
||||
|
||||
export enum PaperlessConnectionStatus {
|
||||
OK = 'OK',
|
||||
ERROR = 'ERROR',
|
||||
}
|
||||
|
||||
export interface PaperlessSystemStatus {
|
||||
pngx_version: string
|
||||
server_os: string
|
||||
install_type: PaperlessInstallType
|
||||
storage: {
|
||||
total: number
|
||||
available: number
|
||||
}
|
||||
database: {
|
||||
type: string
|
||||
url: string
|
||||
status: PaperlessConnectionStatus
|
||||
error?: string
|
||||
migration_status: {
|
||||
latest_migration: string
|
||||
unapplied_migrations: string[]
|
||||
}
|
||||
}
|
||||
redis: {
|
||||
url: string
|
||||
status: PaperlessConnectionStatus
|
||||
error: string
|
||||
}
|
||||
}
|
||||
35
src-ui/src/app/services/system-status.service.spec.ts
Normal file
35
src-ui/src/app/services/system-status.service.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
|
||||
import { SystemStatusService } from './system-status.service'
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
describe('SystemStatusService', () => {
|
||||
let httpTestingController: HttpTestingController
|
||||
let service: SystemStatusService
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [SystemStatusService],
|
||||
imports: [HttpClientTestingModule],
|
||||
})
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
service = TestBed.inject(SystemStatusService)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
httpTestingController.verify()
|
||||
})
|
||||
|
||||
it('calls get statys endpoint', () => {
|
||||
service.get().subscribe()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}status/`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
})
|
||||
})
|
||||
20
src-ui/src/app/services/system-status.service.ts
Normal file
20
src-ui/src/app/services/system-status.service.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Observable } from 'rxjs'
|
||||
import { PaperlessSystemStatus } from '../data/system-status'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SystemStatusService {
|
||||
private endpoint = 'status'
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
get(): Observable<PaperlessSystemStatus> {
|
||||
return this.http.get<PaperlessSystemStatus>(
|
||||
`${environment.apiBaseUrl}${this.endpoint}/`
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user