Move to settings, add celery, updated styling

This commit is contained in:
shamoon 2024-02-12 10:04:45 -08:00
parent 87c2efef4d
commit c40b2adad7
12 changed files with 179 additions and 113 deletions

View File

@ -117,6 +117,7 @@ import { MonetaryComponent } from './components/common/input/monetary/monetary.c
import { SystemStatusDialogComponent } from './components/common/system-status-dialog/system-status-dialog.component'
import { NgxFilesizeModule } from 'ngx-filesize'
import {
airplane,
archive,
arrowCounterclockwise,
arrowDown,
@ -205,6 +206,7 @@ import {
} from 'ngx-bootstrap-icons'
const icons = {
airplane,
archive,
arrowCounterclockwise,
arrowDown,

View File

@ -4,10 +4,16 @@
info="Options to customize appearance, notifications, saved views and more. Settings apply to the <strong>current user only</strong>."
i18n-info
>
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
<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>
</button>
<button class="btn btn-sm btn-outline-primary ms-5" (click)="showSystemStatus()"
*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>
</button>
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-2" href="admin/" target="_blank">
<ng-container i18n>Open Django Admin</ng-container>
<i-bs name="arrow-up-right"></i-bs>
&nbsp;<i-bs name="arrow-up-right"></i-bs>
</a>
</pngx-page-header>

View File

@ -9,6 +9,8 @@ import {
NgbModule,
NgbAlertModule,
NgbNavLink,
NgbModal,
NgbModalModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { of, throwError } from 'rxjs'
@ -39,6 +41,7 @@ import { SettingsComponent } from './settings.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
const savedViews = [
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
@ -65,6 +68,7 @@ describe('SettingsComponent', () => {
let userService: UserService
let permissionsService: PermissionsService
let groupService: GroupService
let modalService: NgbModal
beforeEach(async () => {
TestBed.configureTestingModule({
@ -96,6 +100,7 @@ describe('SettingsComponent', () => {
NgbAlertModule,
NgSelectModule,
NgxBootstrapIconsModule.pick(allIcons),
NgbModalModule,
],
}).compileComponents()
@ -107,6 +112,7 @@ describe('SettingsComponent', () => {
settingsService.currentUser = users[0]
userService = TestBed.inject(UserService)
permissionsService = TestBed.inject(PermissionsService)
modalService = TestBed.inject(NgbModal)
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
@ -372,4 +378,13 @@ describe('SettingsComponent', () => {
fixture.detectChanges()
expect(toastErrorSpy).toBeCalled()
})
it('should open system status dialog', () => {
const modalOpenSpy = jest.spyOn(modalService, 'open')
completeSetup()
component.showSystemStatus()
expect(modalOpenSpy).toHaveBeenCalledWith(SystemStatusDialogComponent, {
size: 'xl',
})
})
})

View File

@ -9,7 +9,7 @@ import {
} from '@angular/core'
import { FormGroup, FormControl } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router'
import { NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap'
import { NgbModal, NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap'
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
import {
@ -40,6 +40,7 @@ import {
} from 'src/app/services/settings.service'
import { ToastService, Toast } from 'src/app/services/toast.service'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
enum SettingsNavIDs {
General = 1,
@ -131,7 +132,8 @@ export class SettingsComponent
private usersService: UserService,
private groupsService: GroupService,
private router: Router,
public permissionsService: PermissionsService
public permissionsService: PermissionsService,
private modalService: NgbModal
) {
super()
this.settings.settingsSaved.subscribe(() => {
@ -565,4 +567,10 @@ export class SettingsComponent
clearThemeColor() {
this.settingsForm.get('themeColor').patchValue('')
}
showSystemStatus() {
this.modalService.open(SystemStatusDialogComponent, {
size: 'xl',
})
}
}

View File

@ -58,10 +58,6 @@
*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>&nbsp;<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>

View File

@ -38,7 +38,6 @@ 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 = [
{
@ -416,10 +415,4 @@ 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)
})
})

View File

@ -46,7 +46,6 @@ 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',
@ -317,8 +316,4 @@ export class AppFrameComponent
onLogout() {
this.openDocumentsService.closeAll()
}
showSystemStatus() {
this.modalService.open(SystemStatusDialogComponent)
}
}

View File

@ -11,28 +11,42 @@
</div>
</div>
} @else {
<h5 i18n>General Info</h5>
<dl>
<dt i18n>Paperless-ngx Version:</dt>
<div class="row row-cols-1 row-cols-md-3 g-3">
<div class="col">
<div class="card bg-light h-100">
<div class="card-header">
<h5 class="card-title mb-0" i18n>Environment</h5>
</div>
<div class="card-body">
<dl class="card-text">
<dt i18n>Paperless-ngx Version</dt>
<dd>{{status.pngx_version}}</dd>
<dt i18n>Install Type:</dt>
<dt i18n>Install Type</dt>
<dd>{{status.install_type}}</dd>
<dt i18n>Server OS:</dt>
<dt i18n>Server OS</dt>
<dd>{{status.server_os}}</dd>
<dt i18n>Media Storage:</dt>
<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>
<ngb-progressbar style="height: 4px;" class="mt-2 mb-1" 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>
</div>
</div>
</div>
<h5 i18n>Database</h5>
<dl>
<dt i18n>Type:</dt>
<div class="col">
<div class="card bg-light h-100">
<div class="card-header">
<h5 class="card-title mb-0" i18n>Database</h5>
</div>
<div class="card-body">
<dl class="card-text">
<dt i18n>Type</dt>
<dd>{{status.database.type}}</dd>
<dt i18n>URL:</dt>
<dt i18n>URL</dt>
<dd>{{status.database.url}}</dd>
<dt i18n>Status:</dt>
<dt i18n>Status</dt>
<dd>
{{status.database.status}}
@if (status.database.status === 'OK') {
@ -41,7 +55,7 @@
<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>
<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>
@ -71,21 +85,42 @@
</dd>
}
</dl>
</div>
</div>
</div>
<h5 i18n>Redis</h5>
<dl>
<dt i18n>URL:</dt>
<dd>{{status.redis.url}}</dd>
<dt i18n>Status:</dt>
<div class="col">
<div class="card bg-light h-100">
<div class="card-header">
<h5 class="card-title mb-0" i18n>Tasks</h5>
</div>
<div class="card-body">
<dl class="card-text">
<dt i18n>Redis URL</dt>
<dd>{{status.tasks.redis_url}}</dd>
<dt i18n>Redis Status</dt>
<dd>
{{status.redis.status}}
@if (status.redis.status === 'OK') {
{{status.tasks.redis_status}}
@if (status.tasks.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>
<i-bs name="exclamation-triangle-fill" class="text-danger ms-1" ngbPopover="{{status.tasks.redis_error}}" triggers="mouseenter:mouseleave"></i-bs>
}
</dd>
<dt i18n>Celery Status</dt>
<dd>
{{status.tasks.celery_status}}
@if (status.tasks.celery_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"></i-bs>
}
</dd>
</dl>
</div>
</div>
</div>
</div>
}
</div>
<div class="modal-footer">

View File

@ -37,10 +37,11 @@ const status: PaperlessSystemStatus = {
unapplied_migrations: [],
},
},
redis: {
url: 'redis://localhost:6379',
status: PaperlessConnectionStatus.ERROR,
error: 'Error 61 connecting to localhost:6379. Connection refused.',
tasks: {
redis_url: 'redis://localhost:6379',
redis_status: PaperlessConnectionStatus.ERROR,
redis_error: 'Error 61 connecting to localhost:6379. Connection refused.',
celery_status: PaperlessConnectionStatus.ERROR,
},
}

View File

@ -26,9 +26,10 @@ export interface PaperlessSystemStatus {
unapplied_migrations: string[]
}
}
redis: {
url: string
status: PaperlessConnectionStatus
error: string
tasks: {
redis_url: string
redis_status: PaperlessConnectionStatus
redis_error: string
celery_status: PaperlessConnectionStatus
}
}

View File

@ -29,6 +29,6 @@ class TestSystemStatusView(APITestCase):
self.assertEqual(response.data["database"]["status"], "OK")
self.assertIsNone(response.data["database"]["error"])
self.assertIsNotNone(response.data["database"]["migration_status"])
self.assertEqual(response.data["redis"]["url"], "redis://localhost:6379")
self.assertEqual(response.data["redis"]["status"], "ERROR")
self.assertIsNotNone(response.data["redis"]["error"])
self.assertEqual(response.data["tasks"]["redis_url"], "redis://localhost:6379")
self.assertEqual(response.data["tasks"]["redis_status"], "ERROR")
self.assertIsNotNone(response.data["tasks"]["redis_error"])

View File

@ -14,6 +14,7 @@ from unicodedata import normalize
from urllib.parse import quote
import pathvalidate
from celery import Celery
from django.conf import settings
from django.contrib.auth.models import User
from django.db import connections
@ -1555,6 +1556,12 @@ class SystemStatusView(GenericAPIView, PassUserMixin):
current_version = version.__full_version_str__
install_type = "bare-metal"
if os.environ.get("KUBERNETES_SERVICE_HOST") is not None:
install_type = "kubernetes"
elif os.environ.get("PNGX_CONTAINERIZED") == "1":
install_type = "docker"
media_stats = os.statvfs(settings.MEDIA_ROOT)
db_conn = connections["default"]
@ -1583,15 +1590,21 @@ class SystemStatusView(GenericAPIView, PassUserMixin):
redis_status = "ERROR"
redis_error = str(e)
try:
app = Celery("paperless")
app.config_from_object("django.conf:settings", namespace="CELERY")
ping = app.control.inspect().ping()
first_worker_ping = ping[next(iter(ping.keys()))]
if first_worker_ping["ok"] == "pong":
celery_active = "OK"
except Exception:
celery_active = "ERROR"
return Response(
{
"pngx_version": current_version,
"server_os": platform.platform(),
"install_type": (
"containerized"
if os.environ.get("PNGX_CONTAINERIZED") == "1"
else "bare-metal"
),
"install_type": install_type,
"storage": {
"total": media_stats.f_frsize * media_stats.f_blocks,
"available": media_stats.f_frsize * media_stats.f_bavail,
@ -1608,10 +1621,11 @@ class SystemStatusView(GenericAPIView, PassUserMixin):
],
},
},
"redis": {
"url": redis_url,
"status": redis_status,
"error": redis_error,
"tasks": {
"redis_url": redis_url,
"redis_status": redis_status,
"redis_error": redis_error,
"celery_status": celery_active,
},
},
)