From e61995e24a03e1c248f6ce566384dca4d872d088 Mon Sep 17 00:00:00 2001 From: fufu Date: Fri, 3 Jan 2025 15:43:02 +0100 Subject: [PATCH] screen to view Processes (sessions) running on a server for a specific duration (#69) * process screen * process screen --------- Co-authored-by: u$f --- src/app/app.module.ts | 5 + src/app/service/router.service.ts | 20 ++- src/app/service/trace.service.ts | 18 +++ src/app/shared/pipe/title-case.pipe.ts | 11 ++ src/app/shared/shared.module.ts | 7 +- .../main/detail-session-main.view.html | 10 +- .../session/main/detail-session-main.view.ts | 17 ++ .../rest/detail-session-rest.view.html | 6 +- .../session/rest/detail-session-rest.view.ts | 21 ++- src/app/views/dump/dump.view.html | 24 +++ src/app/views/dump/dump.view.scss | 17 ++ src/app/views/dump/dump.view.ts | 146 ++++++++++++++++++ .../dump/timeline/dump-timeline.component.ts | 45 ++++++ src/app/views/views.module.ts | 6 +- 14 files changed, 341 insertions(+), 12 deletions(-) create mode 100644 src/app/shared/pipe/title-case.pipe.ts create mode 100644 src/app/views/dump/dump.view.html create mode 100644 src/app/views/dump/dump.view.scss create mode 100644 src/app/views/dump/dump.view.ts create mode 100644 src/app/views/dump/timeline/dump-timeline.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e204762..dddf8bb 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -33,12 +33,17 @@ import {ArchitectureView} from "./views/architecture/architecture.view"; import { NumberFormatterPipe } from './shared/pipe/number.pipe'; import { TreeView } from './views/tree/tree.view'; import {SizePipe} from "./shared/pipe/size.pipe"; +import {DumpView} from "./views/dump/dump.view"; registerLocaleData(localeFr, 'fr-FR'); const routes: Route[] = [ { path: 'session', children: [ + { + path: ':app_name/dump', + component: DumpView + }, { path: 'rest', children: [ diff --git a/src/app/service/router.service.ts b/src/app/service/router.service.ts index 766568c..38a8989 100644 --- a/src/app/service/router.service.ts +++ b/src/app/service/router.service.ts @@ -1,5 +1,5 @@ import {Injectable} from "@angular/core"; -import {NavigationExtras, Router} from "@angular/router"; +import {NavigationExtras, Router, UrlTree} from "@angular/router"; import {Observable} from "rxjs"; @Injectable() @@ -41,6 +41,24 @@ export class EnvRouter { // return Promise.resolve(true); } + createUrlTree(commands: any[], extras?: NavigationExtras): UrlTree { + if (!extras?.queryParams?.env) { + if (this._env) { + if (!extras) { + extras = {} + } + if (!extras.queryParams) { + extras.queryParams = {} + } + extras.queryParams.env = this._env; + } + } + else { + this.env = extras.queryParams.env; + } + return this.router.createUrlTree(commands, extras); + } + open(url?: string | URL, target?: string, features?: string): WindowProxy | null { return window.open(url, target, features); } diff --git a/src/app/service/trace.service.ts b/src/app/service/trace.service.ts index 6113140..48915aa 100644 --- a/src/app/service/trace.service.ts +++ b/src/app/service/trace.service.ts @@ -31,6 +31,15 @@ export class TraceService { return this.http.get>(`${this.server}/session/rest`, { params: params }); } + getRestSessionsForDump(env: string, appName: string, start: Date, end: Date): Observable> { + let params: any = { + 'env': env, + 'start': start.toISOString(), + 'end': end.toISOString() + } + return this.http.get>(`${this.server}/session/rest/${appName}/dump`, { params: params }); + } + getRestSession(id: string): Observable { return this.http.get(`${this.server}/session/rest/${id}`); } @@ -39,6 +48,15 @@ export class TraceService { return this.http.get>(`${this.server}/session/main`, { params: params }); } + getMainSessionsForDump(env: string, appName: string, start: Date, end: Date): Observable> { + let params: any = { + 'env': env, + 'start': start.toISOString(), + 'end': end.toISOString() + } + return this.http.get>(`${this.server}/session/main/${appName}/dump`, { params: params }); + } + getMainSession(id: string): Observable { return this.http.get(`${this.server}/session/main/${id}`); } diff --git a/src/app/shared/pipe/title-case.pipe.ts b/src/app/shared/pipe/title-case.pipe.ts new file mode 100644 index 0000000..af7d060 --- /dev/null +++ b/src/app/shared/pipe/title-case.pipe.ts @@ -0,0 +1,11 @@ +import {Pipe, PipeTransform} from "@angular/core"; + +@Pipe({ + name:"titleCase" +}) +export class TitleCasePipe implements PipeTransform { + transform(value: any): any { + if (!value) return value; + return value[0].toUpperCase() + value.substring(1).toLowerCase(); + } +} \ No newline at end of file diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index dcae002..1f48f23 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -9,6 +9,7 @@ import { AdvancedFilterComponent } from './_component/advanced-filter/advanced-f import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { DurationPipe } from './pipe/duration.pipe'; import { SizePipe } from './pipe/size.pipe'; +import {TitleCasePipe} from "./pipe/title-case.pipe"; @NgModule({ imports: [ @@ -24,7 +25,8 @@ import { SizePipe } from './pipe/size.pipe'; AdvancedFilterTriggerComponent, FilterRowPipe, DurationPipe, - SizePipe + SizePipe, + TitleCasePipe ], exports: [ MaterialModule, @@ -33,7 +35,8 @@ import { SizePipe } from './pipe/size.pipe'; AdvancedFilterRecapComponent, AdvancedFilterTriggerComponent, DurationPipe, - SizePipe + SizePipe, + TitleCasePipe ] }) export class SharedModule { } diff --git a/src/app/views/detail/session/main/detail-session-main.view.html b/src/app/views/detail/session/main/detail-session-main.view.html index 6f054c9..1ddc5da 100644 --- a/src/app/views/detail/session/main/detail-session-main.view.html +++ b/src/app/views/detail/session/main/detail-session-main.view.html @@ -44,19 +44,21 @@ - storage
{{instance.name}} {{instance.version}}
- storage
{{instance.name}} {{instance.version}}
-
- +
+
diff --git a/src/app/views/detail/session/main/detail-session-main.view.ts b/src/app/views/detail/session/main/detail-session-main.view.ts index 4e64e42..d369da9 100644 --- a/src/app/views/detail/session/main/detail-session-main.view.ts +++ b/src/app/views/detail/session/main/detail-session-main.view.ts @@ -112,6 +112,23 @@ export class DetailSessionMainView implements OnInit, OnDestroy { } } + onClickDump(event: MouseEvent) { + let params: {fragments: string[], queryParams: any} = { + fragments: ['session', this.instance.name, 'dump'], + queryParams: { env: this.instance.env, date: new Date(this.session.start * 1000).toISOString() } + }; + if (event.ctrlKey) { + let url = this._router.createUrlTree(params.fragments, { + queryParams: params.queryParams } + ).toString(); + this._router.open(`#/${url}`, '_blank'); + } else { + this._router.navigate(params.fragments, { + queryParams: params.queryParams + }); + } + } + ngOnDestroy() { this.subscriptions.forEach(s => s.unsubscribe()); } diff --git a/src/app/views/detail/session/rest/detail-session-rest.view.html b/src/app/views/detail/session/rest/detail-session-rest.view.html index 321abfd..bae9396 100644 --- a/src/app/views/detail/session/rest/detail-session-rest.view.html +++ b/src/app/views/detail/session/rest/detail-session-rest.view.html @@ -65,8 +65,10 @@
{{instance.name}} {{instance.version}}
-
- +
+
diff --git a/src/app/views/detail/session/rest/detail-session-rest.view.ts b/src/app/views/detail/session/rest/detail-session-rest.view.ts index 32a1a0e..24372a0 100644 --- a/src/app/views/detail/session/rest/detail-session-rest.view.ts +++ b/src/app/views/detail/session/rest/detail-session-rest.view.ts @@ -160,8 +160,8 @@ export class DetailSessionRestView implements OnInit, OnDestroy { case "rest": params.push('statistic', 'rest', this.session.name); break; - case "app": - params.push('statistic', 'app', this.session.appName) + case "dump": + params.push('session', this.instance.name, 'dump') break; case "tree": params.push('session', 'rest', this.session.id, 'tree') @@ -182,6 +182,23 @@ export class DetailSessionRestView implements OnInit, OnDestroy { } } + onClickDump(event: MouseEvent) { + let params: {fragments: string[], queryParams: any} = { + fragments: ['session', this.instance.name, 'dump'], + queryParams: { env: this.instance.env, date: new Date(this.session.start * 1000).toISOString() } + }; + if (event.ctrlKey) { + let url = this._router.createUrlTree(params.fragments, { + queryParams: params.queryParams } + ).toString(); + this._router.open(`#/${url}`, '_blank'); + } else { + this._router.navigate(params.fragments, { + queryParams: params.queryParams + }); + } + } + ngOnDestroy() { this.subscriptions.forEach(s => s.unsubscribe()); } diff --git a/src/app/views/dump/dump.view.html b/src/app/views/dump/dump.view.html new file mode 100644 index 0000000..aba5b14 --- /dev/null +++ b/src/app/views/dump/dump.view.html @@ -0,0 +1,24 @@ + + + +
+
+ + + + +
+ +
+ Loading... + +
+
diff --git a/src/app/views/dump/dump.view.scss b/src/app/views/dump/dump.view.scss new file mode 100644 index 0000000..cd9bd52 --- /dev/null +++ b/src/app/views/dump/dump.view.scss @@ -0,0 +1,17 @@ +.timeline { + height: calc(100vh - 56px - 48px - 1.5em); + display: flex; + flex-direction: column; +} + +.loading { + height: 100%; + width: 15%; + display: flex; + flex-direction: column; + align-items: center; + align-self: center; + justify-content: center; + font-size: 14px; + color: #6c6c6c; +} \ No newline at end of file diff --git a/src/app/views/dump/dump.view.ts b/src/app/views/dump/dump.view.ts new file mode 100644 index 0000000..64d8afb --- /dev/null +++ b/src/app/views/dump/dump.view.ts @@ -0,0 +1,146 @@ +import {Component, ElementRef, inject, OnDestroy, OnInit, Signal, signal, ViewChild} from "@angular/core"; +import {combineLatest, finalize, forkJoin, Subscription} from "rxjs"; +import {ActivatedRoute} from "@angular/router"; +import {DatePipe, Location} from "@angular/common"; +import {EnvRouter} from "../../service/router.service"; +import {TraceService} from "../../service/trace.service"; +import {InstanceMainSession, InstanceRestSession} from "../../model/trace.model"; +import {DataGroup, DataItem, Timeline} from "vis-timeline"; +import {DurationPipe} from "../../shared/pipe/duration.pipe"; +import {sign} from "node:crypto"; + +@Component({ + templateUrl: './dump.view.html', + styleUrls: ['./dump.view.scss'], +}) +export class DumpView implements OnInit, OnDestroy { + private _activatedRoute: ActivatedRoute = inject(ActivatedRoute); + private _router: EnvRouter = inject(EnvRouter); + private _location: Location = inject(Location); + private _traceService: TraceService = inject(TraceService); + + private pipe = new DatePipe('fr-FR'); + private durationPipe = new DurationPipe(); + + + restSessions: Array = []; + mainSessions: Array = []; + loading = signal(false); + zoomableIn = signal(false); + groups: DataGroup[]; + items: DataItem[]; + subscription: Subscription; + + params: Partial<{env: string, app: string, date: Date, step: number}> = {}; + + ngOnInit() { + combineLatest([ + this._activatedRoute.params, + this._activatedRoute.queryParams + ]).subscribe({ + next: ([params, queryParams]) => { + this.params.app = params.app_name; + this.params.env = queryParams.env; + this.params.date = new Date(queryParams.date); + this.params.step = Number.parseInt(queryParams.step) || 10; + this.getSession(); + this._location.replaceState(`${this._router.url.split('?')[0]}?env=${this.params.env}&date=${this.params.date.toISOString()}&step=${this.params.step}`); + } + }); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + getSession() { + if(this.subscription) this.subscription.unsubscribe(); + this.loading.set(true); + this.zoomableIn.set(this.params.step != 1); + let start = new Date(this.params.date.getFullYear(), this.params.date.getMonth(), this.params.date.getDate(), this.params.date.getHours(), this.params.date.getMinutes() - this.params.step, this.params.date.getSeconds()); + let end = new Date(this.params.date.getFullYear(), this.params.date.getMonth(), this.params.date.getDate(), this.params.date.getHours(), this.params.date.getMinutes() + this.params.step, this.params.date.getSeconds()); + this.subscription = forkJoin( + [this._traceService.getRestSessionsForDump(this.params.env, this.params.app, start, end), this._traceService.getMainSessionsForDump(this.params.env, this.params.app, start, end)] + ) + .pipe(finalize(() => {})) + .subscribe({ + next: (sessions) => { + this.restSessions = sessions[0]; + this.mainSessions = sessions[1]; + this.initTimeline(); + } + }); + } + + initTimeline() { + let restGroups = this.restSessions.map(s => ({id: s.threadName, start: s.start})); + let mainGroups = this.mainSessions.map(s => ({id: s.threadName, start: s.start})); + this.groups = [...new Set([restGroups, mainGroups].flat().sort((a, b) => a.start - b.start).map(g => g.id))].map(g => ({id: g, content: g})); + this.items = [this.restSessions.map(s => { + let item: DataItem = { + id: `${s.id}_rest`, + group: s.threadName, + start: s.start * 1000, + end: s.end * 1000, + title: `${this.pipe.transform(new Date(s.start * 1000), 'HH:mm:ss.SSS')} - ${this.pipe.transform(new Date(s.end * 1000), 'HH:mm:ss.SSS')} (${this.durationPipe.transform({start: s.start, end: s.end})})
`, + content: `[${s.method}] ${s?.path ? s?.path : ''}${s?.query ? '?' + s?.query : ''}` + }; + item.type = item.end > item.start ? 'range': 'point'; + if (s.exception?.message || s.exception?.type) { + item.className = 'bdd-failed'; + } + return item; + }), this.mainSessions.map(s => { + let item: DataItem = { + id: `${s.id}_main_${s.type.toLowerCase()}`, + group: s.threadName, + start: s.start * 1000, + end: s.end * 1000, + title: `${this.pipe.transform(new Date(s.start * 1000), 'HH:mm:ss.SSS')} - ${this.pipe.transform(new Date(s.end * 1000), 'HH:mm:ss.SSS')} (${this.durationPipe.transform({start: s.start, end: s.end})})
`, + content: `[${s.type}] ${s?.name}` + }; + item.type = item.end > item.start ? 'range': 'point'; + if (s.exception?.message || s.exception?.type) { + item.className = 'bdd-failed'; + } + return item; + })].flat(); + this.loading.set(false) + } + + zoomOut() { + let step = this.params.step + 1; + this._router.navigate([], { + relativeTo: this._activatedRoute, + queryParamsHandling: 'merge', + queryParams: { step: step } + }) + } + + zoomIn() { + let step = this.params.step - 1; + this._router.navigate([], { + relativeTo: this._activatedRoute, + queryParamsHandling: 'merge', + queryParams: { step: step } + }) + } + + previous() { + let date = new Date(this.params.date.getFullYear(), this.params.date.getMonth(), this.params.date.getDate(), this.params.date.getHours(), this.params.date.getMinutes() - this.params.step, this.params.date.getSeconds()); + this._router.navigate([], { + relativeTo: this._activatedRoute, + queryParamsHandling: 'merge', + queryParams: { date: date } + }) + } + + next() { + let date = new Date(this.params.date.getFullYear(), this.params.date.getMonth(), this.params.date.getDate(), this.params.date.getHours(), this.params.date.getMinutes() + this.params.step, this.params.date.getSeconds()); + this._router.navigate([], { + relativeTo: this._activatedRoute, + queryParamsHandling: 'merge', + queryParams: { date: date } + }) + } +} \ No newline at end of file diff --git a/src/app/views/dump/timeline/dump-timeline.component.ts b/src/app/views/dump/timeline/dump-timeline.component.ts new file mode 100644 index 0000000..2f677fc --- /dev/null +++ b/src/app/views/dump/timeline/dump-timeline.component.ts @@ -0,0 +1,45 @@ +import {Component, ElementRef, inject, Input, OnChanges, OnInit, SimpleChanges, ViewChild} from "@angular/core"; +import {DataGroup, DataItem, Timeline} from "vis-timeline"; +import {EnvRouter} from "../../../service/router.service"; + +@Component({ + selector: 'dump-timeline', + template: '
' +}) +export class DumpTimelineComponent implements OnChanges { + private _router: EnvRouter = inject(EnvRouter); + + timeline: Timeline; + + @Input() items: DataItem[]; + @Input() groups: DataGroup[]; + + @ViewChild('timeline', {static: true}) timelineElement: ElementRef; + + ngOnChanges(changes: SimpleChanges): void { + if(changes.items || changes.groups){ + if(this.items && this.groups){ + if (this.timeline) this.timeline.destroy(); + this.timeline = new Timeline(this.timelineElement.nativeElement, this.items, this.groups, { + margin: { + item: { + horizontal: -1 + } + }, + verticalScroll: true, + zoomKey: 'ctrlKey', + maxHeight: 'calc(100vh - 56px - 48px - 48px - 1.5em)' + }); + let that = this; + this.timeline.on('doubleClick', function (props: any) { + if(props.item) { + let id = props.item.split('_')[0]; + let type_session = props.item.split('_')[1]; + let type_main = props.item.split('_')[2]; + type_session == 'main' ? that._router.open(`#/session/${type_session}/${type_main}/${id}`, '_blank') : that._router.open(`#/session/${type_session}/${id}`, '_blank'); + } + }); + } + } + } +} \ No newline at end of file diff --git a/src/app/views/views.module.ts b/src/app/views/views.module.ts index 2f041f5..8c9dd7b 100644 --- a/src/app/views/views.module.ts +++ b/src/app/views/views.module.ts @@ -46,6 +46,8 @@ import {ServerHistoryTableComponent} from "./statistic/application/history-table import {StatisticClientView} from "./statistic/view/statistic-client.view"; import {ArchitectureView} from "./architecture/architecture.view"; import {DetailLocalTableComponent} from "./detail/session/_component/local-table/detail-local-table.component"; +import {DumpView} from "./dump/dump.view"; +import {DumpTimelineComponent} from "./dump/timeline/dump-timeline.component"; @NgModule({ @@ -89,7 +91,9 @@ import {DetailLocalTableComponent} from "./detail/session/_component/local-table ProtocolExceptionComponent, ServerHistoryTableComponent, ArchitectureView, - TreeView + TreeView, + DumpView, + DumpTimelineComponent ] }) export class ViewsModule { }