Add ability to copy admin logs

pull/6544/head
Chocobozzz 2024-08-08 11:44:30 +02:00
parent 5207faeeae
commit 9f57427a79
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
7 changed files with 66 additions and 27 deletions

View File

@ -37,8 +37,15 @@
<div *ngIf="loading" i18n>Loading...</div> <div *ngIf="loading" i18n>Loading...</div>
<div #logsElement> <div #logsElement>
<my-copy-button
*ngIf="logs.length !== 0"
i18n-notification notification="Logs copied" i18n-title title="Copy logs"
withBorder="true" [elementContent]="logsContent"
></my-copy-button>
<div *ngIf="!loading && logs.length === 0" i18n>No log.</div> <div *ngIf="!loading && logs.length === 0" i18n>No log.</div>
<div #logsContent>
<div *ngFor="let log of logs" class="log-row" [ngClass]="{ error: log.level === 'error', warn: log.level === 'warn' }"> <div *ngFor="let log of logs" class="log-row" [ngClass]="{ error: log.level === 'error', warn: log.level === 'warn' }">
<span class="log-level">{{ log.level }}</span> <span class="log-level">{{ log.level }}</span>
@ -52,4 +59,5 @@
<pre>{{ log.meta }}</pre> <pre>{{ log.meta }}</pre>
</div> </div>
</div> </div>
</div>
</div> </div>

View File

@ -9,6 +9,10 @@
background: rgba(0, 0, 0, 0.03); background: rgba(0, 0, 0, 0.03);
padding: 20px; padding: 20px;
> div {
position: relative;
}
.log-row { .log-row {
margin-top: 1px; margin-top: 1px;
word-break: break-word; word-break: break-word;
@ -81,7 +85,11 @@
&.error { &.error {
color: rgb(250, 5, 5); color: rgb(250, 5, 5);
} }
}
my-copy-button {
position: absolute;
right: 5px;
} }
@include on-small-main-col { @include on-small-main-col {

View File

@ -1,28 +1,42 @@
import { DatePipe, NgClass, NgFor, NgIf } from '@angular/common'
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { LocalStorageService, Notifier } from '@app/core' import { LocalStorageService, Notifier } from '@app/core'
import { NgSelectModule } from '@ng-select/ng-select'
import { ServerLogLevel } from '@peertube/peertube-models' import { ServerLogLevel } from '@peertube/peertube-models'
import { SelectTagsComponent } from '../../../shared/shared-forms/select/select-tags.component'
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
import { CopyButtonComponent } from '../../../shared/shared-main/buttons/copy-button.component'
import { LogRow } from './log-row.model' import { LogRow } from './log-row.model'
import { LogsService } from './logs.service' import { LogsService } from './logs.service'
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
import { SelectTagsComponent } from '../../../shared/shared-forms/select/select-tags.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { NgFor, NgIf, NgClass, DatePipe } from '@angular/common'
import { FormsModule } from '@angular/forms'
@Component({ @Component({
templateUrl: './logs.component.html', templateUrl: './logs.component.html',
styleUrls: [ './logs.component.scss' ], styleUrls: [ './logs.component.scss' ],
standalone: true, standalone: true,
imports: [ FormsModule, NgFor, NgSelectModule, NgIf, NgClass, SelectTagsComponent, ButtonComponent, DatePipe ] imports: [
FormsModule,
NgFor,
NgSelectModule,
NgIf,
NgClass,
SelectTagsComponent,
ButtonComponent,
DatePipe,
CopyButtonComponent
]
}) })
export class LogsComponent implements OnInit { export class LogsComponent implements OnInit {
private static LOCAL_STORAGE_LOG_TYPE_CHOICE_KEY = 'admin-logs-log-type-choice' private static LOCAL_STORAGE_LOG_TYPE_CHOICE_KEY = 'admin-logs-log-type-choice'
@ViewChild('logsElement', { static: true }) logsElement: ElementRef<HTMLElement> @ViewChild('logsElement', { static: true }) logsElement: ElementRef<HTMLElement>
@ViewChild('logsContent', { static: true }) logsContent: ElementRef<HTMLElement>
loading = false loading = false
rawLogs: string
logs: LogRow[] = [] logs: LogRow[] = []
timeChoices: { id: string, label: string, dateFormat: string }[] = [] timeChoices: { id: string, label: string, dateFormat: string }[] = []
levelChoices: { id: ServerLogLevel, label: string }[] = [] levelChoices: { id: ServerLogLevel, label: string }[] = []
logTypeChoices: { id: 'audit' | 'standard', label: string }[] = [] logTypeChoices: { id: 'audit' | 'standard', label: string }[] = []
@ -72,6 +86,8 @@ export class LogsComponent implements OnInit {
next: logs => { next: logs => {
this.logs = logs this.logs = logs
this.rawLogs = this.logs.map(l => `${l.level} ${l.localeDate} ${l.message} ${l.meta}`).join('\n')
setTimeout(() => { setTimeout(() => {
this.logsElement.nativeElement.scrollIntoView({ block: 'end', inline: 'nearest' }) this.logsElement.nativeElement.scrollIntoView({ block: 'end', inline: 'nearest' })
}) })

View File

@ -1,9 +1,8 @@
import { Observable } from 'rxjs'
import { catchError, map } from 'rxjs/operators'
import { HttpClient, HttpParams } from '@angular/common/http' import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { RestExtractor, RestService } from '@app/core' import { RestExtractor, RestService } from '@app/core'
import { ServerLogLevel } from '@peertube/peertube-models' import { ServerLogLevel } from '@peertube/peertube-models'
import { catchError, map } from 'rxjs/operators'
import { environment } from '../../../../environments/environment' import { environment } from '../../../../environments/environment'
import { LogRow } from './log-row.model' import { LogRow } from './log-row.model'
@ -24,7 +23,7 @@ export class LogsService {
tagsOneOf?: string[] tagsOneOf?: string[]
level?: ServerLogLevel level?: ServerLogLevel
endDate?: string endDate?: string
}): Observable<any[]> { }) {
const { isAuditLog, startDate, endDate, tagsOneOf } = options const { isAuditLog, startDate, endDate, tagsOneOf } = options
let params = new HttpParams() let params = new HttpParams()
@ -38,7 +37,7 @@ export class LogsService {
? LogsService.BASE_AUDIT_LOG_URL ? LogsService.BASE_AUDIT_LOG_URL
: LogsService.BASE_LOG_URL : LogsService.BASE_LOG_URL
return this.authHttp.get<any[]>(path, { params }) return this.authHttp.get<LogRow[]>(path, { params })
.pipe( .pipe(
map(rows => rows.map(r => new LogRow(r))), map(rows => rows.map(r => new LogRow(r))),
catchError(err => this.restExtractor.handleError(err)) catchError(err => this.restExtractor.handleError(err))

View File

@ -1,7 +1,7 @@
<button <button
class="btn btn-outline-secondary btn-sm copy-button" class="btn btn-outline-secondary btn-sm copy-button"
[cdkCopyToClipboard]="value" (click)="activateCopiedMessage()" (click)="copy()"
[title]="title" [ngClass]="{ 'is-input-group': isInputGroup }" [title]="title" [ngClass]="{ 'is-input-group': isInputGroup, 'with-border': withBorder }"
> >
<my-global-icon iconName="copy"></my-global-icon> <my-global-icon iconName="copy"></my-global-icon>

View File

@ -1,7 +1,7 @@
@use '_variables' as *; @use '_variables' as *;
@use '_mixins' as *; @use '_mixins' as *;
button:not(.is-input-group) { button:not(.with-border, .is-input-group) {
border: 0; border: 0;
} }

View File

@ -1,27 +1,35 @@
import { Component, Input } from '@angular/core' import { Clipboard } from '@angular/cdk/clipboard'
import { NgClass } from '@angular/common'
import { booleanAttribute, Component, Input } from '@angular/core'
import { Notifier } from '@app/core' import { Notifier } from '@app/core'
import { GlobalIconComponent } from '../../shared-icons/global-icon.component' import { GlobalIconComponent } from '../../shared-icons/global-icon.component'
import { NgClass } from '@angular/common'
import { CdkCopyToClipboard } from '@angular/cdk/clipboard'
@Component({ @Component({
selector: 'my-copy-button', selector: 'my-copy-button',
styleUrls: [ './copy-button.component.scss' ], styleUrls: [ './copy-button.component.scss' ],
templateUrl: './copy-button.component.html', templateUrl: './copy-button.component.html',
standalone: true, standalone: true,
imports: [ CdkCopyToClipboard, NgClass, GlobalIconComponent ] providers: [ Clipboard ],
imports: [ NgClass, GlobalIconComponent ]
}) })
export class CopyButtonComponent { export class CopyButtonComponent {
@Input() value: string @Input() value: string
@Input() elementContent: HTMLElement
@Input() title: string @Input() title: string
@Input() notification: string @Input() notification: string
@Input() isInputGroup = false
constructor (private notifier: Notifier) { @Input({ transform: booleanAttribute }) withBorder = false
@Input({ transform: booleanAttribute }) isInputGroup = false
constructor (private notifier: Notifier, private clipboard: Clipboard) {
} }
activateCopiedMessage () { copy () {
this.clipboard.copy(this.value || this.elementContent?.innerText)
if (this.notification) this.notifier.success(this.notification) if (this.notification) this.notifier.success(this.notification)
} }
} }