diff options
author | Chocobozzz <me@florianbigard.com> | 2019-04-11 10:05:43 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-04-11 10:14:08 +0200 |
commit | 2c22613c2fe6f7f9c8c7de66e42be54b27cc7edd (patch) | |
tree | 4692ec7ef34bf418a47624704006e61200e174df /client/src/app/+admin/system | |
parent | fd8710b897a67518d3a61c319e54b6a65ba443ef (diff) | |
download | PeerTube-2c22613c2fe6f7f9c8c7de66e42be54b27cc7edd.tar.gz PeerTube-2c22613c2fe6f7f9c8c7de66e42be54b27cc7edd.tar.zst PeerTube-2c22613c2fe6f7f9c8c7de66e42be54b27cc7edd.zip |
Add logs page in client
Diffstat (limited to 'client/src/app/+admin/system')
-rw-r--r-- | client/src/app/+admin/system/index.ts | 4 | ||||
-rw-r--r-- | client/src/app/+admin/system/jobs/index.ts | 6 | ||||
-rw-r--r-- | client/src/app/+admin/system/jobs/jobs.component.ts | 14 | ||||
-rw-r--r-- | client/src/app/+admin/system/logs/index.ts | 2 | ||||
-rw-r--r-- | client/src/app/+admin/system/logs/log-row.model.ts | 21 | ||||
-rw-r--r-- | client/src/app/+admin/system/logs/logs.component.html | 31 | ||||
-rw-r--r-- | client/src/app/+admin/system/logs/logs.component.scss | 48 | ||||
-rw-r--r-- | client/src/app/+admin/system/logs/logs.component.ts | 111 | ||||
-rw-r--r-- | client/src/app/+admin/system/logs/logs.service.ts | 33 | ||||
-rw-r--r-- | client/src/app/+admin/system/system.component.html | 11 | ||||
-rw-r--r-- | client/src/app/+admin/system/system.component.scss | 4 | ||||
-rw-r--r-- | client/src/app/+admin/system/system.component.ts | 8 | ||||
-rw-r--r-- | client/src/app/+admin/system/system.routes.ts | 44 |
13 files changed, 326 insertions, 11 deletions
diff --git a/client/src/app/+admin/system/index.ts b/client/src/app/+admin/system/index.ts new file mode 100644 index 000000000..226d999d2 --- /dev/null +++ b/client/src/app/+admin/system/index.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export * from './jobs' | ||
2 | export * from './logs' | ||
3 | export * from './system.component' | ||
4 | export * from './system.routes' | ||
diff --git a/client/src/app/+admin/system/jobs/index.ts b/client/src/app/+admin/system/jobs/index.ts index c0e0cc95d..486a745e4 100644 --- a/client/src/app/+admin/system/jobs/index.ts +++ b/client/src/app/+admin/system/jobs/index.ts | |||
@@ -1,4 +1,2 @@ | |||
1 | export * from './shared' | 1 | export * from './job.service' |
2 | export * from './jobs-list' | 2 | export * from './jobs.component' |
3 | export * from './job.routes' | ||
4 | export * from './job.component' | ||
diff --git a/client/src/app/+admin/system/jobs/jobs.component.ts b/client/src/app/+admin/system/jobs/jobs.component.ts index b265e1dd6..ebfb52779 100644 --- a/client/src/app/+admin/system/jobs/jobs.component.ts +++ b/client/src/app/+admin/system/jobs/jobs.component.ts | |||
@@ -5,15 +5,15 @@ import { SortMeta } from 'primeng/primeng' | |||
5 | import { Job } from '../../../../../../shared/index' | 5 | import { Job } from '../../../../../../shared/index' |
6 | import { JobState } from '../../../../../../shared/models' | 6 | import { JobState } from '../../../../../../shared/models' |
7 | import { RestPagination, RestTable } from '../../../shared' | 7 | import { RestPagination, RestTable } from '../../../shared' |
8 | import { JobService } from '../shared' | 8 | import { JobService } from './job.service' |
9 | import { I18n } from '@ngx-translate/i18n-polyfill' | 9 | import { I18n } from '@ngx-translate/i18n-polyfill' |
10 | 10 | ||
11 | @Component({ | 11 | @Component({ |
12 | selector: 'my-jobs-list', | 12 | selector: 'my-jobs', |
13 | templateUrl: './jobs-list.component.html', | 13 | templateUrl: './jobs.component.html', |
14 | styleUrls: [ './jobs-list.component.scss' ] | 14 | styleUrls: [ './jobs.component.scss' ] |
15 | }) | 15 | }) |
16 | export class JobsListComponent extends RestTable implements OnInit { | 16 | export class JobsComponent extends RestTable implements OnInit { |
17 | private static JOB_STATE_LOCAL_STORAGE_STATE = 'jobs-list-state' | 17 | private static JOB_STATE_LOCAL_STORAGE_STATE = 'jobs-list-state' |
18 | 18 | ||
19 | jobState: JobState = 'waiting' | 19 | jobState: JobState = 'waiting' |
@@ -58,12 +58,12 @@ export class JobsListComponent extends RestTable implements OnInit { | |||
58 | } | 58 | } |
59 | 59 | ||
60 | private loadJobState () { | 60 | private loadJobState () { |
61 | const result = peertubeLocalStorage.getItem(JobsListComponent.JOB_STATE_LOCAL_STORAGE_STATE) | 61 | const result = peertubeLocalStorage.getItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE) |
62 | 62 | ||
63 | if (result) this.jobState = result as JobState | 63 | if (result) this.jobState = result as JobState |
64 | } | 64 | } |
65 | 65 | ||
66 | private saveJobState () { | 66 | private saveJobState () { |
67 | peertubeLocalStorage.setItem(JobsListComponent.JOB_STATE_LOCAL_STORAGE_STATE, this.jobState) | 67 | peertubeLocalStorage.setItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE, this.jobState) |
68 | } | 68 | } |
69 | } | 69 | } |
diff --git a/client/src/app/+admin/system/logs/index.ts b/client/src/app/+admin/system/logs/index.ts new file mode 100644 index 000000000..7b56d4237 --- /dev/null +++ b/client/src/app/+admin/system/logs/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './logs.component' | ||
2 | export * from './logs.service' | ||
diff --git a/client/src/app/+admin/system/logs/log-row.model.ts b/client/src/app/+admin/system/logs/log-row.model.ts new file mode 100644 index 000000000..9bc7dafdd --- /dev/null +++ b/client/src/app/+admin/system/logs/log-row.model.ts | |||
@@ -0,0 +1,21 @@ | |||
1 | import { LogLevel } from '@shared/models/server/log-level.type' | ||
2 | import omit from 'lodash-es/omit' | ||
3 | |||
4 | export class LogRow { | ||
5 | date: Date | ||
6 | localeDate: string | ||
7 | level: LogLevel | ||
8 | message: string | ||
9 | meta: string | ||
10 | |||
11 | constructor (row: any) { | ||
12 | this.date = new Date(row.timestamp) | ||
13 | this.localeDate = this.date.toLocaleString() | ||
14 | this.level = row.level | ||
15 | this.message = row.message | ||
16 | |||
17 | const metaObj = omit(row, 'timestamp', 'level', 'message', 'label') | ||
18 | |||
19 | if (Object.keys(metaObj).length !== 0) this.meta = JSON.stringify(metaObj, undefined, 2) | ||
20 | } | ||
21 | } | ||
diff --git a/client/src/app/+admin/system/logs/logs.component.html b/client/src/app/+admin/system/logs/logs.component.html new file mode 100644 index 000000000..45723a655 --- /dev/null +++ b/client/src/app/+admin/system/logs/logs.component.html | |||
@@ -0,0 +1,31 @@ | |||
1 | <div class="header"> | ||
2 | <div class="peertube-select-container"> | ||
3 | <select [(ngModel)]="startDate" (ngModelChange)="refresh()"> | ||
4 | <option *ngFor="let timeChoice of timeChoices" [value]="timeChoice.id">{{ timeChoice.label }}</option> | ||
5 | </select> | ||
6 | </div> | ||
7 | |||
8 | <div class="peertube-select-container"> | ||
9 | <select [(ngModel)]="level" (ngModelChange)="refresh()"> | ||
10 | <option *ngFor="let levelChoice of levelChoices" [value]="levelChoice.id">{{ levelChoice.label }}</option> | ||
11 | </select> | ||
12 | </div> | ||
13 | |||
14 | <my-button i18n-label label="Refresh" icon="refresh" (click)="refresh()"></my-button> | ||
15 | </div> | ||
16 | |||
17 | <div class="logs"> | ||
18 | <div *ngIf="loading">Loading...</div> | ||
19 | |||
20 | <div #logsElement> | ||
21 | <div *ngFor="let log of logs" class="log-row" [ngClass]="{ error: log.level === 'error', warn: log.level === 'warn' }"> | ||
22 | <span class="log-level">{{ log.level }}</span> | ||
23 | |||
24 | <span class="log-date">[{{ log.localeDate }}]</span> | ||
25 | |||
26 | {{ log.message }} | ||
27 | |||
28 | {{ log.meta }} | ||
29 | </div> | ||
30 | </div> | ||
31 | </div> | ||
diff --git a/client/src/app/+admin/system/logs/logs.component.scss b/client/src/app/+admin/system/logs/logs.component.scss new file mode 100644 index 000000000..ab00fb5ae --- /dev/null +++ b/client/src/app/+admin/system/logs/logs.component.scss | |||
@@ -0,0 +1,48 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .logs { | ||
5 | font-family: monospace; | ||
6 | font-size: 13px; | ||
7 | max-height: 500px; | ||
8 | overflow-y: auto; | ||
9 | background: rgba(0, 0, 0, 0.03); | ||
10 | padding: 20px; | ||
11 | |||
12 | .log-row { | ||
13 | margin-top: 1px; | ||
14 | |||
15 | &:hover { | ||
16 | background: rgba(0, 0, 0, 0.07); | ||
17 | } | ||
18 | } | ||
19 | |||
20 | .log-level { | ||
21 | font-weight: $font-semibold; | ||
22 | margin-right: 5px; | ||
23 | } | ||
24 | |||
25 | .warn { | ||
26 | color: $orange-color; | ||
27 | } | ||
28 | |||
29 | .error { | ||
30 | color: $red; | ||
31 | } | ||
32 | } | ||
33 | |||
34 | .header { | ||
35 | display: flex; | ||
36 | justify-content: flex-end; | ||
37 | margin-bottom: 10px; | ||
38 | |||
39 | .peertube-select-container { | ||
40 | @include peertube-select-container(150px); | ||
41 | } | ||
42 | |||
43 | my-button, | ||
44 | .peertube-select-container { | ||
45 | margin-left: 10px; | ||
46 | } | ||
47 | } | ||
48 | |||
diff --git a/client/src/app/+admin/system/logs/logs.component.ts b/client/src/app/+admin/system/logs/logs.component.ts new file mode 100644 index 000000000..17abb8409 --- /dev/null +++ b/client/src/app/+admin/system/logs/logs.component.ts | |||
@@ -0,0 +1,111 @@ | |||
1 | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' | ||
2 | import { LogsService } from '@app/+admin/system/logs/logs.service' | ||
3 | import { Notifier } from '@app/core' | ||
4 | import { LogRow } from '@app/+admin/system/logs/log-row.model' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
6 | import { LogLevel } from '@shared/models/server/log-level.type' | ||
7 | |||
8 | @Component({ | ||
9 | templateUrl: './logs.component.html', | ||
10 | styleUrls: [ './logs.component.scss' ] | ||
11 | }) | ||
12 | export class LogsComponent implements OnInit { | ||
13 | @ViewChild('logsElement') logsElement: ElementRef<HTMLElement> | ||
14 | |||
15 | loading = false | ||
16 | |||
17 | logs: LogRow[] = [] | ||
18 | timeChoices: { id: string, label: string }[] = [] | ||
19 | levelChoices: { id: LogLevel, label: string }[] = [] | ||
20 | |||
21 | startDate: string | ||
22 | level: LogLevel | ||
23 | |||
24 | constructor ( | ||
25 | private logsService: LogsService, | ||
26 | private notifier: Notifier, | ||
27 | private i18n: I18n | ||
28 | ) { } | ||
29 | |||
30 | ngOnInit (): void { | ||
31 | this.buildTimeChoices() | ||
32 | this.buildLevelChoices() | ||
33 | |||
34 | this.load() | ||
35 | } | ||
36 | |||
37 | refresh () { | ||
38 | this.logs = [] | ||
39 | this.load() | ||
40 | } | ||
41 | |||
42 | load () { | ||
43 | this.loading = true | ||
44 | |||
45 | this.logsService.getLogs(this.level, this.startDate) | ||
46 | .subscribe( | ||
47 | logs => { | ||
48 | this.logs = logs | ||
49 | |||
50 | setTimeout(() => { | ||
51 | this.logsElement.nativeElement.scrollIntoView({ block: 'end', inline: 'nearest' }) | ||
52 | }) | ||
53 | }, | ||
54 | |||
55 | err => this.notifier.error(err.message), | ||
56 | |||
57 | () => this.loading = false | ||
58 | ) | ||
59 | } | ||
60 | |||
61 | buildTimeChoices () { | ||
62 | const lastHour = new Date() | ||
63 | lastHour.setHours(lastHour.getHours() - 1) | ||
64 | |||
65 | const lastDay = new Date() | ||
66 | lastDay.setDate(lastDay.getDate() - 1) | ||
67 | |||
68 | const lastWeek = new Date() | ||
69 | lastWeek.setDate(lastWeek.getDate() - 7) | ||
70 | |||
71 | this.timeChoices = [ | ||
72 | { | ||
73 | id: lastWeek.toISOString(), | ||
74 | label: this.i18n('Last week') | ||
75 | }, | ||
76 | { | ||
77 | id: lastDay.toISOString(), | ||
78 | label: this.i18n('Last day') | ||
79 | }, | ||
80 | { | ||
81 | id: lastHour.toISOString(), | ||
82 | label: this.i18n('Last hour') | ||
83 | } | ||
84 | ] | ||
85 | |||
86 | this.startDate = lastHour.toISOString() | ||
87 | } | ||
88 | |||
89 | buildLevelChoices () { | ||
90 | this.levelChoices = [ | ||
91 | { | ||
92 | id: 'debug', | ||
93 | label: this.i18n('Debug') | ||
94 | }, | ||
95 | { | ||
96 | id: 'info', | ||
97 | label: this.i18n('Info') | ||
98 | }, | ||
99 | { | ||
100 | id: 'warn', | ||
101 | label: this.i18n('Warning') | ||
102 | }, | ||
103 | { | ||
104 | id: 'error', | ||
105 | label: this.i18n('Error') | ||
106 | } | ||
107 | ] | ||
108 | |||
109 | this.level = 'info' | ||
110 | } | ||
111 | } | ||
diff --git a/client/src/app/+admin/system/logs/logs.service.ts b/client/src/app/+admin/system/logs/logs.service.ts new file mode 100644 index 000000000..4db79a1fa --- /dev/null +++ b/client/src/app/+admin/system/logs/logs.service.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | import { catchError, map } from 'rxjs/operators' | ||
2 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { Observable } from 'rxjs' | ||
5 | import { environment } from '../../../../environments/environment' | ||
6 | import { RestExtractor, RestService } from '../../../shared' | ||
7 | import { LogRow } from '@app/+admin/system/logs/log-row.model' | ||
8 | import { LogLevel } from '@shared/models/server/log-level.type' | ||
9 | |||
10 | @Injectable() | ||
11 | export class LogsService { | ||
12 | private static BASE_JOB_URL = environment.apiUrl + '/api/v1/server/logs' | ||
13 | |||
14 | constructor ( | ||
15 | private authHttp: HttpClient, | ||
16 | private restService: RestService, | ||
17 | private restExtractor: RestExtractor | ||
18 | ) {} | ||
19 | |||
20 | getLogs (level: LogLevel, startDate: string, endDate?: string): Observable<any> { | ||
21 | let params = new HttpParams() | ||
22 | params = params.append('startDate', startDate) | ||
23 | params = params.append('level', level) | ||
24 | |||
25 | if (endDate) params.append('endDate', endDate) | ||
26 | |||
27 | return this.authHttp.get<any[]>(LogsService.BASE_JOB_URL, { params }) | ||
28 | .pipe( | ||
29 | map(rows => rows.map(r => new LogRow(r))), | ||
30 | catchError(err => this.restExtractor.handleError(err)) | ||
31 | ) | ||
32 | } | ||
33 | } | ||
diff --git a/client/src/app/+admin/system/system.component.html b/client/src/app/+admin/system/system.component.html new file mode 100644 index 000000000..345a101e6 --- /dev/null +++ b/client/src/app/+admin/system/system.component.html | |||
@@ -0,0 +1,11 @@ | |||
1 | <div class="admin-sub-header"> | ||
2 | <div i18n class="form-sub-title">System</div> | ||
3 | |||
4 | <div class="admin-sub-nav"> | ||
5 | <a i18n routerLink="jobs" routerLinkActive="active">Jobs</a> | ||
6 | |||
7 | <a i18n routerLink="logs" routerLinkActive="active">Logs</a> | ||
8 | </div> | ||
9 | </div> | ||
10 | |||
11 | <router-outlet></router-outlet> | ||
diff --git a/client/src/app/+admin/system/system.component.scss b/client/src/app/+admin/system/system.component.scss new file mode 100644 index 000000000..766d7853b --- /dev/null +++ b/client/src/app/+admin/system/system.component.scss | |||
@@ -0,0 +1,4 @@ | |||
1 | .form-sub-title { | ||
2 | flex-grow: 0; | ||
3 | margin-right: 30px; | ||
4 | } | ||
diff --git a/client/src/app/+admin/system/system.component.ts b/client/src/app/+admin/system/system.component.ts new file mode 100644 index 000000000..992d9c8af --- /dev/null +++ b/client/src/app/+admin/system/system.component.ts | |||
@@ -0,0 +1,8 @@ | |||
1 | import { Component } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | templateUrl: './system.component.html', | ||
5 | styleUrls: [ './system.component.scss' ] | ||
6 | }) | ||
7 | export class SystemComponent { | ||
8 | } | ||
diff --git a/client/src/app/+admin/system/system.routes.ts b/client/src/app/+admin/system/system.routes.ts new file mode 100644 index 000000000..e6d45b760 --- /dev/null +++ b/client/src/app/+admin/system/system.routes.ts | |||
@@ -0,0 +1,44 @@ | |||
1 | import { Routes } from '@angular/router' | ||
2 | import { UserRightGuard } from '../../core' | ||
3 | import { UserRight } from '../../../../../shared' | ||
4 | import { JobsComponent } from '@app/+admin/system/jobs/jobs.component' | ||
5 | import { LogsComponent } from '@app/+admin/system/logs' | ||
6 | import { SystemComponent } from '@app/+admin/system/system.component' | ||
7 | |||
8 | export const SystemRoutes: Routes = [ | ||
9 | { | ||
10 | path: 'system', | ||
11 | component: SystemComponent, | ||
12 | data: { | ||
13 | }, | ||
14 | children: [ | ||
15 | { | ||
16 | path: '', | ||
17 | redirectTo: 'jobs', | ||
18 | pathMatch: 'full' | ||
19 | }, | ||
20 | { | ||
21 | path: 'jobs', | ||
22 | canActivate: [ UserRightGuard ], | ||
23 | component: JobsComponent, | ||
24 | data: { | ||
25 | meta: { | ||
26 | userRight: UserRight.MANAGE_JOBS, | ||
27 | title: 'Jobs' | ||
28 | } | ||
29 | } | ||
30 | }, | ||
31 | { | ||
32 | path: 'logs', | ||
33 | canActivate: [ UserRightGuard ], | ||
34 | component: LogsComponent, | ||
35 | data: { | ||
36 | meta: { | ||
37 | userRight: UserRight.MANAGE_LOGS, | ||
38 | title: 'Logs' | ||
39 | } | ||
40 | } | ||
41 | } | ||
42 | ] | ||
43 | } | ||
44 | ] | ||