Enhancement: settings reorganization & improvements, separate admin section (#4251)

* Separate admin / manage sections

* Move mail settings to its own component

* Move users and groups to its own component

* Move default permissions to its own settings tab

* Unify list styling, add tour step, refactor components

* Only patch saved views that have changed on settings save

* Update messages.xlf

* Remove unused methods in settings.component.ts

* Drop admin section to bottom of sidebar, cleanup outdated, add docs link to dropdown

* Better visually unify management list & other list pages
This commit is contained in:
shamoon
2023-09-24 19:24:28 -07:00
committed by GitHub
parent dc642152d1
commit b5d29e821e
39 changed files with 4098 additions and 3760 deletions

View File

@@ -0,0 +1,25 @@
<pngx-page-header title="Logs" i18n-title>
</pngx-page-header>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeLog" (activeIdChange)="reloadLogs()" class="nav-tabs">
<li *ngFor="let logFile of logFiles" [ngbNavItem]="logFile">
<a ngbNavLink>{{logFile}}.log</a>
</li>
<div *ngIf="isLoading && !logFiles.length" class="pb-2">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</div>
</ul>
<div [ngbNavOutlet]="nav" class="mt-2"></div>
<div class="bg-dark p-3 text-light font-monospace log-container" #logContainer>
<div *ngIf="isLoading && logFiles.length">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</div>
<p
class="m-0 p-0 log-entry-{{getLogLevel(log)}}"
*ngFor="let log of logs">{{log}}</p>
</div>

View File

@@ -0,0 +1,26 @@
.log-entry-10 {
color: lightslategray !important;
}
.log-entry-30 {
color: yellow !important;
}
.log-entry-40 {
color: red !important;
}
.log-entry-50 {
color: lightcoral !important;
font-weight: bold;
}
.log-container {
overflow-y: scroll;
height: calc(100vh - 200px);
top: 70px;
p {
white-space: pre-wrap;
}
}

View File

@@ -0,0 +1,71 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { LogService } from 'src/app/services/rest/log.service'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { LogsComponent } from './logs.component'
import { of, throwError } from 'rxjs'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
import { BrowserModule, By } from '@angular/platform-browser'
const paperless_logs = [
'[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.',
'[2023-05-29 04:05:00,622] [DEBUG] [paperless.classifier] Gathering data from database...',
'[2023-05-29 04:05:01,213] [DEBUG] [paperless.tasks] Training data unchanged.',
'[2023-06-11 00:30:01,774] [INFO] [paperless.sanity_checker] Document contains no OCR data',
'[2023-06-11 00:30:01,774] [WARNING] [paperless.sanity_checker] Made up',
'[2023-06-11 00:30:01,774] [ERROR] [paperless.sanity_checker] Document contains no OCR data',
'[2023-06-11 00:30:01,774] [CRITICAL] [paperless.sanity_checker] Document contains no OCR data',
]
const mail_logs = [
'[2023-06-09 01:10:00,666] [DEBUG] [paperless_mail] Rule inbox@example.com.Incoming: Searching folder with criteria (SINCE 10-May-2023 UNSEEN)',
'[2023-06-09 01:10:01,385] [DEBUG] [paperless_mail] Rule inbox@example.com.Incoming: Processed 3 matching mail(s)',
]
describe('LogsComponent', () => {
let component: LogsComponent
let fixture: ComponentFixture<LogsComponent>
let logService: LogService
let logSpy
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [LogsComponent, PageHeaderComponent],
providers: [],
imports: [HttpClientTestingModule, BrowserModule, NgbModule],
}).compileComponents()
logService = TestBed.inject(LogService)
jest.spyOn(logService, 'list').mockReturnValue(of(['paperless', 'mail']))
logSpy = jest.spyOn(logService, 'get')
logSpy.mockImplementation((id) => {
return of(id === 'paperless' ? paperless_logs : mail_logs)
})
fixture = TestBed.createComponent(LogsComponent)
component = fixture.componentInstance
window.HTMLElement.prototype.scroll = function () {} // mock scroll
fixture.detectChanges()
})
it('should display logs with first log initially', () => {
expect(logSpy).toHaveBeenCalledWith('paperless')
fixture.detectChanges()
expect(fixture.debugElement.nativeElement.textContent).toContain(
paperless_logs[0]
)
})
it('should load log when tab clicked', () => {
fixture.debugElement
.queryAll(By.directive(NgbNavLink))[1]
.nativeElement.dispatchEvent(new MouseEvent('click'))
expect(logSpy).toHaveBeenCalledWith('mail')
})
it('should handle error with no logs', () => {
logSpy.mockReturnValueOnce(
throwError(() => new Error('error getting logs'))
)
component.reloadLogs()
expect(component.logs).toHaveLength(0)
})
})

View File

@@ -0,0 +1,94 @@
import {
Component,
ElementRef,
OnInit,
AfterViewChecked,
ViewChild,
OnDestroy,
} from '@angular/core'
import { Subject, takeUntil } from 'rxjs'
import { LogService } from 'src/app/services/rest/log.service'
@Component({
selector: 'pngx-logs',
templateUrl: './logs.component.html',
styleUrls: ['./logs.component.scss'],
})
export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
constructor(private logService: LogService) {}
public logs: string[] = []
public logFiles: string[] = []
public activeLog: string
private unsubscribeNotifier: Subject<any> = new Subject()
public isLoading: boolean = false
@ViewChild('logContainer') logContainer: ElementRef
ngOnInit(): void {
this.isLoading = true
this.logService
.list()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((result) => {
this.logFiles = result
this.isLoading = false
if (this.logFiles.length > 0) {
this.activeLog = this.logFiles[0]
this.reloadLogs()
}
})
}
ngAfterViewChecked() {
this.scrollToBottom()
}
ngOnDestroy(): void {
this.unsubscribeNotifier.next(true)
this.unsubscribeNotifier.complete()
}
reloadLogs() {
this.isLoading = true
this.logService
.get(this.activeLog)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (result) => {
this.logs = result
this.isLoading = false
},
error: () => {
this.logs = []
this.isLoading = false
},
})
}
getLogLevel(log: string) {
if (log.indexOf('[DEBUG]') != -1) {
return 10
} else if (log.indexOf('[WARNING]') != -1) {
return 30
} else if (log.indexOf('[ERROR]') != -1) {
return 40
} else if (log.indexOf('[CRITICAL]') != -1) {
return 50
} else {
return 20
}
}
scrollToBottom(): void {
this.logContainer?.nativeElement.scroll({
top: this.logContainer.nativeElement.scrollHeight,
left: 0,
behavior: 'auto',
})
}
}