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 { SystemStatusDialogComponent } from './components/common/system-status-dialog/system-status-dialog.component'
import { NgxFilesizeModule } from 'ngx-filesize' import { NgxFilesizeModule } from 'ngx-filesize'
import { import {
airplane,
archive, archive,
arrowCounterclockwise, arrowCounterclockwise,
arrowDown, arrowDown,
@ -205,6 +206,7 @@ import {
} from 'ngx-bootstrap-icons' } from 'ngx-bootstrap-icons'
const icons = { const icons = {
airplane,
archive, archive,
arrowCounterclockwise, arrowCounterclockwise,
arrowDown, 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>." info="Options to customize appearance, notifications, saved views and more. Settings apply to the <strong>current user only</strong>."
i18n-info i18n-info
> >
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button> <button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank"> <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> <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> </a>
</pngx-page-header> </pngx-page-header>

View File

@ -9,6 +9,8 @@ import {
NgbModule, NgbModule,
NgbAlertModule, NgbAlertModule,
NgbNavLink, NgbNavLink,
NgbModal,
NgbModalModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
@ -39,6 +41,7 @@ import { SettingsComponent } from './settings.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive' 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'
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 },
@ -65,6 +68,7 @@ describe('SettingsComponent', () => {
let userService: UserService let userService: UserService
let permissionsService: PermissionsService let permissionsService: PermissionsService
let groupService: GroupService let groupService: GroupService
let modalService: NgbModal
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -96,6 +100,7 @@ describe('SettingsComponent', () => {
NgbAlertModule, NgbAlertModule,
NgSelectModule, NgSelectModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
NgbModalModule,
], ],
}).compileComponents() }).compileComponents()
@ -107,6 +112,7 @@ describe('SettingsComponent', () => {
settingsService.currentUser = users[0] settingsService.currentUser = users[0]
userService = TestBed.inject(UserService) userService = TestBed.inject(UserService)
permissionsService = TestBed.inject(PermissionsService) permissionsService = TestBed.inject(PermissionsService)
modalService = TestBed.inject(NgbModal)
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest jest
.spyOn(permissionsService, 'currentUserHasObjectPermissions') .spyOn(permissionsService, 'currentUserHasObjectPermissions')
@ -372,4 +378,13 @@ describe('SettingsComponent', () => {
fixture.detectChanges() fixture.detectChanges()
expect(toastErrorSpy).toBeCalled() 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' } 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 { NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap' import { NgbModal, 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 {
@ -40,6 +40,7 @@ import {
} from 'src/app/services/settings.service' } from 'src/app/services/settings.service'
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'
enum SettingsNavIDs { enum SettingsNavIDs {
General = 1, General = 1,
@ -131,7 +132,8 @@ export class SettingsComponent
private usersService: UserService, private usersService: UserService,
private groupsService: GroupService, private groupsService: GroupService,
private router: Router, private router: Router,
public permissionsService: PermissionsService public permissionsService: PermissionsService,
private modalService: NgbModal
) { ) {
super() super()
this.settings.settingsSaved.subscribe(() => { this.settings.settingsSaved.subscribe(() => {
@ -565,4 +567,10 @@ export class SettingsComponent
clearThemeColor() { clearThemeColor() {
this.settingsForm.get('themeColor').patchValue('') 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 }"> *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }">
<i-bs class="me-2" name="gear"></i-bs><ng-container i18n>Settings</ng-container> <i-bs class="me-2" name="gear"></i-bs><ng-container i18n>Settings</ng-container>
</a> </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()"> <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> <i-bs class="me-2" name="door-open"></i-bs><ng-container i18n>Logout</ng-container>
</a> </a>

View File

@ -38,7 +38,6 @@ import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
import { SavedView } from 'src/app/data/saved-view' import { SavedView } from 'src/app/data/saved-view'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component' import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { SystemStatusDialogComponent } from '../common/system-status-dialog/system-status-dialog.component'
const saved_views = [ const saved_views = [
{ {
@ -416,10 +415,4 @@ describe('AppFrameComponent', () => {
expect(toastErrorSpy).toHaveBeenCalledTimes(2) expect(toastErrorSpy).toHaveBeenCalledTimes(2)
expect(toastInfoSpy).toHaveBeenCalledTimes(3) 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' } from '@angular/cdk/drag-drop'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component' import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
import { SystemStatusDialogComponent } from '../common/system-status-dialog/system-status-dialog.component'
@Component({ @Component({
selector: 'pngx-app-frame', selector: 'pngx-app-frame',
@ -317,8 +316,4 @@ export class AppFrameComponent
onLogout() { onLogout() {
this.openDocumentsService.closeAll() this.openDocumentsService.closeAll()
} }
showSystemStatus() {
this.modalService.open(SystemStatusDialogComponent)
}
} }

View File

@ -11,28 +11,42 @@
</div> </div>
</div> </div>
} @else { } @else {
<h5 i18n>General Info</h5> <div class="row row-cols-1 row-cols-md-3 g-3">
<dl> <div class="col">
<dt i18n>Paperless-ngx Version:</dt> <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> <dd>{{status.pngx_version}}</dd>
<dt i18n>Install Type:</dt> <dt i18n>Install Type</dt>
<dd>{{status.install_type}}</dd> <dd>{{status.install_type}}</dd>
<dt i18n>Server OS:</dt> <dt i18n>Server OS</dt>
<dd>{{status.server_os}}</dd> <dd>{{status.server_os}}</dd>
<dt i18n>Media Storage:</dt> <dt i18n>Media Storage</dt>
<dd> <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> <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> </dd>
</dl> </dl>
</div>
</div>
</div>
<h5 i18n>Database</h5> <div class="col">
<dl> <div class="card bg-light h-100">
<dt i18n>Type:</dt> <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> <dd>{{status.database.type}}</dd>
<dt i18n>URL:</dt> <dt i18n>URL</dt>
<dd>{{status.database.url}}</dd> <dd>{{status.database.url}}</dd>
<dt i18n>Status:</dt> <dt i18n>Status</dt>
<dd> <dd>
{{status.database.status}} {{status.database.status}}
@if (status.database.status === 'OK') { @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> <i-bs name="exclamation-triangle-fill" class="text-danger ms-1" ngbPopover="{{status.database.error}}" triggers="mouseenter:mouseleave"></i-bs>
} }
</dd> </dd>
<dt i18n>Migration Status:</dt> <dt i18n>Migration Status</dt>
<dd> <dd>
@if (status.database.migration_status.unapplied_migrations.length === 0) { @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> <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> </dd>
} }
</dl> </dl>
</div>
</div>
</div>
<h5 i18n>Redis</h5> <div class="col">
<dl> <div class="card bg-light h-100">
<dt i18n>URL:</dt> <div class="card-header">
<dd>{{status.redis.url}}</dd> <h5 class="card-title mb-0" i18n>Tasks</h5>
<dt i18n>Status:</dt> </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> <dd>
{{status.redis.status}} {{status.tasks.redis_status}}
@if (status.redis.status === 'OK') { @if (status.tasks.redis_status === 'OK') {
<i-bs name="check-circle-fill" class="text-success ms-1"></i-bs> <i-bs name="check-circle-fill" class="text-success ms-1"></i-bs>
} @else { } @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> </dd>
</dl> </dl>
</div>
</div>
</div>
</div>
} }
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

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

View File

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

View File

@ -14,6 +14,7 @@ from unicodedata import normalize
from urllib.parse import quote from urllib.parse import quote
import pathvalidate import pathvalidate
from celery import Celery
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import connections from django.db import connections
@ -1555,6 +1556,12 @@ class SystemStatusView(GenericAPIView, PassUserMixin):
current_version = version.__full_version_str__ 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) media_stats = os.statvfs(settings.MEDIA_ROOT)
db_conn = connections["default"] db_conn = connections["default"]
@ -1583,15 +1590,21 @@ class SystemStatusView(GenericAPIView, PassUserMixin):
redis_status = "ERROR" redis_status = "ERROR"
redis_error = str(e) 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( return Response(
{ {
"pngx_version": current_version, "pngx_version": current_version,
"server_os": platform.platform(), "server_os": platform.platform(),
"install_type": ( "install_type": install_type,
"containerized"
if os.environ.get("PNGX_CONTAINERIZED") == "1"
else "bare-metal"
),
"storage": { "storage": {
"total": media_stats.f_frsize * media_stats.f_blocks, "total": media_stats.f_frsize * media_stats.f_blocks,
"available": media_stats.f_frsize * media_stats.f_bavail, "available": media_stats.f_frsize * media_stats.f_bavail,
@ -1608,10 +1621,11 @@ class SystemStatusView(GenericAPIView, PassUserMixin):
], ],
}, },
}, },
"redis": { "tasks": {
"url": redis_url, "redis_url": redis_url,
"status": redis_status, "redis_status": redis_status,
"error": redis_error, "redis_error": redis_error,
"celery_status": celery_active,
}, },
}, },
) )