diff --git a/web/src/main/angular/src/app/core/components/application-inspector-title/application-inspector-title-container.component.css b/web/src/main/angular/src/app/core/components/application-inspector-title/application-inspector-title-container.component.css deleted file mode 100644 index c87348a57d66..000000000000 --- a/web/src/main/angular/src/app/core/components/application-inspector-title/application-inspector-title-container.component.css +++ /dev/null @@ -1,28 +0,0 @@ -:host { - display: block; - height: 40px; -} -.l-wrapper { - cursor: pointer; - display: block; - padding: 12px 6px 10px 13px; - overflow: hidden; - font-size: 14px; - white-space: nowrap; - text-overflow: ellipsis; -} -.l-wrapper img { - float: left; - width: 22px; - height: 18px; - margin-right: 4px; -} -.l-wrapper.active { - color: var(--primary); - font-weight: 600; -} -.l-wrapper:hover { - cursor: pointer; - color: var(--primary); - background-color: var(--background-hover-default); -} diff --git a/web/src/main/angular/src/app/core/components/application-inspector-title/application-inspector-title-container.component.html b/web/src/main/angular/src/app/core/components/application-inspector-title/application-inspector-title-container.component.html deleted file mode 100644 index abbb19b54a7e..000000000000 --- a/web/src/main/angular/src/app/core/components/application-inspector-title/application-inspector-title-container.component.html +++ /dev/null @@ -1,3 +0,0 @@ -
- {{applicationName}} -
diff --git a/web/src/main/angular/src/app/core/components/application-inspector-title/application-inspector-title-container.component.ts b/web/src/main/angular/src/app/core/components/application-inspector-title/application-inspector-title-container.component.ts deleted file mode 100644 index 67a6c99ac0b8..000000000000 --- a/web/src/main/angular/src/app/core/components/application-inspector-title/application-inspector-title-container.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { Subject } from 'rxjs'; -import { takeUntil, filter } from 'rxjs/operators'; - -import { UrlPath, UrlPathId } from 'app/shared/models'; -import { - UrlRouteManagerService, - WebAppSettingDataService, - NewUrlStateNotificationService, - AnalyticsService, - TRACKED_EVENT_LIST -} from 'app/shared/services'; - -@Component({ - selector: 'pp-application-inspector-title-container', - templateUrl: './application-inspector-title-container.component.html', - styleUrls: ['./application-inspector-title-container.component.css'], -}) -export class ApplicationInspectorTitleContainerComponent implements OnInit, OnDestroy { - private unsubscribe = new Subject(); - - agentId: string; - funcImagePath: Function; - applicationServiceType: string; - applicationName: string; - applicationIconImagePath: string; - - constructor( - private webAppSettingDataService: WebAppSettingDataService, - private newUrlStateNotificationService: NewUrlStateNotificationService, - private urlRouteManagerService: UrlRouteManagerService, - private analyticsService: AnalyticsService, - ) {} - - ngOnInit() { - this.funcImagePath = this.webAppSettingDataService.getIconPathMakeFunc(); - this.newUrlStateNotificationService.onUrlStateChange$.pipe( - takeUntil(this.unsubscribe), - filter((urlService: NewUrlStateNotificationService) => urlService.hasValue(UrlPathId.APPLICATION)) - ).subscribe((urlService: NewUrlStateNotificationService) => { - this.applicationName = urlService.getPathValue(UrlPathId.APPLICATION).getApplicationName(); - this.applicationServiceType = urlService.getPathValue(UrlPathId.APPLICATION).getServiceType(); - this.applicationIconImagePath = this.funcImagePath(this.applicationServiceType); - this.agentId = urlService.hasValue(UrlPathId.AGENT_ID) ? urlService.getPathValue(UrlPathId.AGENT_ID) : ''; - }); - } - - ngOnDestroy() { - this.unsubscribe.next(); - this.unsubscribe.complete(); - } - - onSelectApplication() { - const url = this.newUrlStateNotificationService.isRealTimeMode() ? - [ - UrlPath.INSPECTOR, - UrlPath.REAL_TIME, - this.newUrlStateNotificationService.getPathValue(UrlPathId.APPLICATION).getUrlStr(), - ] : - [ - UrlPath.INSPECTOR, - this.newUrlStateNotificationService.getPathValue(UrlPathId.APPLICATION).getUrlStr(), - this.newUrlStateNotificationService.getPathValue(UrlPathId.PERIOD).getValueWithTime(), - this.newUrlStateNotificationService.getPathValue(UrlPathId.END_TIME).getEndTime(), - ]; - - this.urlRouteManagerService.moveOnPage({ url }); - this.analyticsService.trackEvent(TRACKED_EVENT_LIST.GO_TO_APPLICATION_INSPECTOR); - } -} diff --git a/web/src/main/angular/src/app/core/components/application-inspector-title/index.ts b/web/src/main/angular/src/app/core/components/application-inspector-title/index.ts deleted file mode 100644 index b63df12efecb..000000000000 --- a/web/src/main/angular/src/app/core/components/application-inspector-title/index.ts +++ /dev/null @@ -1,18 +0,0 @@ - -import { NgModule } from '@angular/core'; -import { SharedModule } from 'app/shared'; -import { ApplicationInspectorTitleContainerComponent } from './application-inspector-title-container.component'; - -@NgModule({ - declarations: [ - ApplicationInspectorTitleContainerComponent - ], - imports: [ - SharedModule - ], - exports: [ - ApplicationInspectorTitleContainerComponent - ], - providers: [] -}) -export class ApplicationInspectorTitleModule { } diff --git a/web/src/main/angular/src/app/core/components/metric-contents/metric-contents-container.component.css b/web/src/main/angular/src/app/core/components/metric-contents/metric-contents-container.component.css index 7a7ee601ee61..1f765f8bba87 100644 --- a/web/src/main/angular/src/app/core/components/metric-contents/metric-contents-container.component.css +++ b/web/src/main/angular/src/app/core/components/metric-contents/metric-contents-container.component.css @@ -37,12 +37,7 @@ min-width: 0; } -/* .l-chart-wrapper:not(:last-child) { - margin-bottom: 20px; -} */ - .l-page-title-wrapper { - /* padding: 5px 0px; */ margin-bottom: 15px; } diff --git a/web/src/main/angular/src/app/core/components/metric-contents/metric-contents-container.component.html b/web/src/main/angular/src/app/core/components/metric-contents/metric-contents-container.component.html index 9cdc419bce20..1e1187dabaa5 100644 --- a/web/src/main/angular/src/app/core/components/metric-contents/metric-contents-container.component.html +++ b/web/src/main/angular/src/app/core/components/metric-contents/metric-contents-container.component.html @@ -1,5 +1,5 @@
-

System Metric Beta

+

System Metric Beta

diff --git a/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-container.component.ts b/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-container.component.ts index 118190df1c48..68ec34936347 100644 --- a/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-container.component.ts +++ b/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-container.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, OnDestroy, ComponentFactoryResolver, Injector } from '@angular/core'; -import { Subject, iif, of, Observable, EMPTY } from 'rxjs'; -import { tap, concatMap, filter, switchMap, delay, takeUntil, catchError, map } from 'rxjs/operators'; +import { Subject, iif, of, Observable, EMPTY, merge } from 'rxjs'; +import { tap, concatMap, filter, switchMap, delay, takeUntil, catchError, map, pluck } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { @@ -15,7 +15,6 @@ import { TranslateReplaceService, } from 'app/shared/services'; import { ServerErrorPopupContainerComponent } from 'app/core/components/server-error-popup/server-error-popup-container.component'; -import { InspectorPageService, ISourceForServerAndAgentList } from 'app/routes/inspector-page/inspector-page.service'; import { UrlPath, UrlPathId } from 'app/shared/models'; import { ServerAndAgentListDataService } from './server-and-agent-list-data.service'; import { isEmpty, isThatType } from 'app/core/utils/util'; @@ -50,7 +49,6 @@ export class ServerAndAgentListContainerComponent implements OnInit, OnDestroy { private analyticsService: AnalyticsService, private componentFactoryResolver: ComponentFactoryResolver, private injector: Injector, - private inspectorPageService: InspectorPageService, private serverAndAgentListDataService: ServerAndAgentListDataService, private messageQueueService: MessageQueueService, private translateService: TranslateService, @@ -60,86 +58,107 @@ export class ServerAndAgentListContainerComponent implements OnInit, OnDestroy { ngOnInit() { this.initI18nText(); this.funcImagePath = this.webAppSettingDataService.getImagePathMakeFunc(); - this.inspectorPageService.sourceForServerAndAgentList$.pipe( + + merge( + this.newUrlStateNotificationService.onUrlStateChange$.pipe( takeUntil(this.unsubscribe), - filter((data: ISourceForServerAndAgentList) => !!data), - switchMap((data: ISourceForServerAndAgentList) => { + tap((urlService: NewUrlStateNotificationService) => { + this.agentId = urlService.getPathValue(UrlPathId.AGENT_ID); + }), + map((urlService: NewUrlStateNotificationService) => { + if (urlService.isRealTimeMode()) { + const to = urlService.getUrlServerTimeData(); + const from = to - this.webAppSettingDataService.getSystemDefaultPeriod().getMiliSeconds(); + + return [from, to]; + } else { + return [urlService.getStartTimeToNumber(), urlService.getEndTimeToNumber()]; + } + + }) + ), + this.messageQueueService.receiveMessage(this.unsubscribe, MESSAGE_TO.FETCH_AGENT_LIST).pipe( + switchMap((data: {range: number[], emitAfter: number}) => { return iif(() => data.emitAfter === 0, of(data), of(data).pipe(delay(data.emitAfter)) ); }), - tap(({agentId}: ISourceForServerAndAgentList) => { - this.agentId = agentId; - }), - concatMap(({range}: ISourceForServerAndAgentList) => { - const appName = (this.newUrlStateNotificationService.getPathValue(UrlPathId.APPLICATION) as IApplication).getApplicationName(); - const requestStartAt = Date.now(); - - return this.serverAndAgentListDataService.getData(appName, range).pipe( - filter((res: {[key: string]: IServerAndAgentData[]} | IServerErrorShortFormat) => { - // TODO: 민우님께 에러구분 여쭤보기. 401이면 AuthService 활용한다? 근데이럼 IS_ACCESS_DENYED 출처 불분명같은 문제가 있지않을까.. - if (isThatType(res, 'errorCode', 'errorMessage')) { - this.errorMessage = res.errorMessage; - this.messageQueueService.sendMessage({to: MESSAGE_TO.IS_ACCESS_DENYED, param: true}); - return false; - } else { - this.errorMessage = ''; - this.messageQueueService.sendMessage({to: MESSAGE_TO.IS_ACCESS_DENYED, param: false}); + pluck('range'), + ) + ).pipe( + concatMap((range: number[]) => { + const appName = (this.newUrlStateNotificationService.getPathValue(UrlPathId.APPLICATION) as IApplication).getApplicationName(); + const requestStartAt = Date.now(); + + return this.serverAndAgentListDataService.getData(appName, range).pipe( + filter((res: {[key: string]: IServerAndAgentData[]} | IServerErrorShortFormat) => { + // TODO: 민우님께 에러구분 여쭤보기. 401이면 AuthService 활용한다? 근데이럼 IS_ACCESS_DENYED 출처 불분명같은 문제가 있지않을까.. + if (isThatType(res, 'errorCode', 'errorMessage')) { + this.errorMessage = res.errorMessage; + this.messageQueueService.sendMessage({to: MESSAGE_TO.IS_ACCESS_DENYED, param: true}); + return false; + } else { + this.errorMessage = ''; + this.messageQueueService.sendMessage({to: MESSAGE_TO.IS_ACCESS_DENYED, param: false}); + return true; + } + }), + filter((res: {[key: string]: IServerAndAgentData[]}) => { + if (this.agentId) { + const filteredList = this.filterServerList(res, this.agentId, ({agentId}: IServerAndAgentData) => this.agentId.toLowerCase() === agentId.toLowerCase()); + const isAgentIdValid = Object.keys(filteredList).length !== 0; + + if (isAgentIdValid) { return true; - } - }), - filter((res: {[key: string]: IServerAndAgentData[]}) => { - if (this.agentId) { - const filteredList = this.filterServerList(res, this.agentId, ({agentId}: IServerAndAgentData) => this.agentId.toLowerCase() === agentId.toLowerCase()); - const isAgentIdValid = Object.keys(filteredList).length !== 0; - - if (isAgentIdValid) { - return true; - } else { - const url = this.newUrlStateNotificationService.isRealTimeMode() - ? [ UrlPath.INSPECTOR, UrlPathId.REAL_TIME, this.newUrlStateNotificationService.getPathValue(UrlPathId.APPLICATION).getUrlStr() ] - : [ - UrlPath.INSPECTOR, - this.newUrlStateNotificationService.getPathValue(UrlPathId.APPLICATION).getUrlStr(), - this.newUrlStateNotificationService.getPathValue(UrlPathId.PERIOD).getValueWithTime(), - this.newUrlStateNotificationService.getPathValue(UrlPathId.END_TIME).getEndTime() - ]; - - this.urlRouteManagerService.moveOnPage({ url }); - - return false; - } } else { - return true; + const url = this.newUrlStateNotificationService.isRealTimeMode() + ? [this.newUrlStateNotificationService.getStartPath(), UrlPathId.REAL_TIME, this.newUrlStateNotificationService.getPathValue(UrlPathId.APPLICATION).getUrlStr()] + : [ + this.newUrlStateNotificationService.getStartPath(), + this.newUrlStateNotificationService.getPathValue(UrlPathId.APPLICATION).getUrlStr(), + this.newUrlStateNotificationService.getPathValue(UrlPathId.PERIOD).getValueWithTime(), + this.newUrlStateNotificationService.getPathValue(UrlPathId.END_TIME).getEndTime() + ]; + + this.urlRouteManagerService.moveOnPage({url}); + + return false; } - }), - tap(() => { - const responseArriveAt = Date.now(); - const deltaT = responseArriveAt - requestStartAt; - const now = range[1] + deltaT; - - this.messageQueueService.sendMessage({ - to: MESSAGE_TO.INSPECTOR_PAGE_VALID, - param: {range, now} - }); - }), - catchError((error: IServerError) => { - this.dynamicPopupService.openPopup({ - data: { - title: 'Error', - contents: error - }, - component: ServerErrorPopupContainerComponent - }, { - resolver: this.componentFactoryResolver, - injector: this.injector - }); - - return EMPTY; - }) - ); - }), + } else { + return true; + } + }), + tap(() => { + const responseArriveAt = Date.now(); + const deltaT = responseArriveAt - requestStartAt; + const now = range[1] + deltaT; + + this.messageQueueService.sendMessage({ + to: MESSAGE_TO.AGENT_LIST_VALID, + param: { + range, + now, + agentId: this.agentId + } + }); + }), + catchError((error: IServerError) => { + this.dynamicPopupService.openPopup({ + data: { + title: 'Error', + contents: error + }, + component: ServerErrorPopupContainerComponent + }, { + resolver: this.componentFactoryResolver, + injector: this.injector + }); + + return EMPTY; + }) + ); + }), ).subscribe((data: {[key: string]: IServerAndAgentData[]}) => { this.serverList = data; this.filteredServerList = this.filterServerList(data, this.query); @@ -163,21 +182,21 @@ export class ServerAndAgentListContainerComponent implements OnInit, OnDestroy { onSelectAgent(agentId: string) { const url = this.newUrlStateNotificationService.isRealTimeMode() ? [ - UrlPath.INSPECTOR, + this.newUrlStateNotificationService.getStartPath(), UrlPath.REAL_TIME, this.newUrlStateNotificationService.getPathValue(UrlPathId.APPLICATION).getUrlStr(), agentId ] : [ - UrlPath.INSPECTOR, + this.newUrlStateNotificationService.getStartPath(), this.newUrlStateNotificationService.getPathValue(UrlPathId.APPLICATION).getUrlStr(), this.newUrlStateNotificationService.getPathValue(UrlPathId.PERIOD).getValueWithTime(), this.newUrlStateNotificationService.getPathValue(UrlPathId.END_TIME).getEndTime(), agentId ]; - this.urlRouteManagerService.moveOnPage({ url }); - this.analyticsService.trackEvent(TRACKED_EVENT_LIST.GO_TO_AGENT_INSPECTOR); + this.urlRouteManagerService.moveOnPage({url}); + this.analyticsService.trackEvent(TRACKED_EVENT_LIST.SELECT_AGENT_ON_THE_LIST); } private filterServerList(serverList: {[key: string]: IServerAndAgentData[]}, query: string, predi?: (data: IServerAndAgentData) => boolean): {[key: string]: IServerAndAgentData[]} { diff --git a/web/src/main/angular/src/app/core/components/side-navigation-bar/side-navigation-bar-container.component.ts b/web/src/main/angular/src/app/core/components/side-navigation-bar/side-navigation-bar-container.component.ts index 2daf031bce78..bce1fe5105b9 100644 --- a/web/src/main/angular/src/app/core/components/side-navigation-bar/side-navigation-bar-container.component.ts +++ b/web/src/main/angular/src/app/core/components/side-navigation-bar/side-navigation-bar-container.component.ts @@ -33,6 +33,9 @@ interface ISNBMeta { export class SideNavigationBarContainerComponent implements OnInit, OnDestroy { private unsubscribe = new Subject(); + private showMetric: boolean; + private showUrlStat: boolean; + enableRealTime: boolean; logoPath: string; minimize = false; @@ -40,8 +43,6 @@ export class SideNavigationBarContainerComponent implements OnInit, OnDestroy { userId = ''; meta: ISNBMeta; - showMetric: boolean; - constructor( private cd: ChangeDetectorRef, private windowRefService: WindowRefService, @@ -58,6 +59,10 @@ export class SideNavigationBarContainerComponent implements OnInit, OnDestroy { this.showMetric = showMetric; }); + this.webAppSettingDataService.showUrlStat().subscribe((showUrlStat: boolean) => { + this.showUrlStat = showUrlStat; + }); + this.webAppSettingDataService.getUserId().subscribe(userId => { this.userId = userId || 'dev'; this.meta = this.generatNavItemMeta(); @@ -98,6 +103,10 @@ export class SideNavigationBarContainerComponent implements OnInit, OnDestroy { this.urlRouteManagerService.moveToHostMenu(UrlPath.METRIC); } + onClickUrlStat(): void { + this.urlRouteManagerService.moveToAppMenu(UrlPath.URL_STATISTIC); + } + onClickGithubLink() { this.windowRefService.nativeWindow.open('http://github.com/naver/pinpoint'); } @@ -134,7 +143,15 @@ export class SideNavigationBarContainerComponent implements OnInit, OnDestroy { iconClass: 'fas fa-server', showItem: this.showMetric, onClick: () => this.onClickMetric(), - } + }, + { + id: 'url-stat', + title: 'URL Statistic', + path: '/urlStatistic', + iconClass: 'fas fa-chart-bar', + showItem: this.showUrlStat, + onClick: () => this.onClickUrlStat(), + }, ], bottomItems: [ { diff --git a/web/src/main/angular/src/app/core/components/url-statistic-chart/index.ts b/web/src/main/angular/src/app/core/components/url-statistic-chart/index.ts new file mode 100644 index 000000000000..41224b8a51c8 --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-chart/index.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; + +import { SharedModule } from 'app/shared'; +import { UrlStatisticChartContainerComponent } from './url-statistic-chart-container.component'; +import { UrlStatisticChartComponent } from './url-statistic-chart.component'; + +@NgModule({ + imports: [ + SharedModule + ], + exports: [ + UrlStatisticChartContainerComponent + ], + declarations: [ + UrlStatisticChartContainerComponent, + UrlStatisticChartComponent + ], + providers: [], +}) +export class UrlStatisticChartModule { } diff --git a/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart-container.component.css b/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart-container.component.css new file mode 100644 index 000000000000..cd962e8a3b46 --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart-container.component.css @@ -0,0 +1,29 @@ +:host { + display: block; + width: 100%; + height: 100%; + + --chart-most-success: var(--emerald-green-100); + --chart-success: var(--emerald-green-300); + --chart-kinda-success: var(--emerald-green-400); + --chart-almost-normal: var(--emerald-green-500); + --chart-normal: var(--blue-300); + --chart-slow: var(--orange-500); + --chart-very-slow: var(--orange-800); + --chart-fail: var(--red-400); +} + +.dark-mode :host { + --chart-most-success: var(--emerald-green-800); + --chart-success: var(--emerald-green-600); + --chart-kinda-success: var(--emerald-green-500); + --chart-almost-normal: var(--emerald-green-400); + + --chart-very-slow: var(--orange-100); +} + +.chart-area-wrapper { + width: 100%; + height: 100%; + position: relative; +} diff --git a/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart-container.component.html b/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart-container.component.html new file mode 100644 index 000000000000..b2478560a3be --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart-container.component.html @@ -0,0 +1,14 @@ + + +
+ + + + +
+
+ +

{{guideMessage}}

+
diff --git a/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart-container.component.ts b/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart-container.component.ts new file mode 100644 index 000000000000..036ee0f7d296 --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart-container.component.ts @@ -0,0 +1,318 @@ +import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core'; +import { EMPTY, forkJoin, iif, Observable, of, Subject } from 'rxjs'; +import { catchError, map, switchMap, takeUntil, tap } from 'rxjs/operators'; +import { Data, PrimitiveArray, bar, zoom, areaStep } from 'billboard.js'; +import * as moment from 'moment-timezone'; +import { TranslateService } from '@ngx-translate/core'; + +import { MessageQueueService, MESSAGE_TO, NewUrlStateNotificationService, StoreHelperService, WebAppSettingDataService } from 'app/shared/services'; +import { IUrlStatChartDataParams, UrlStatisticChartDataService } from './url-statistic-chart-data.service'; +import { UrlPathId } from 'app/shared/models'; +import { makeYData, makeXData, getMaxTickValue, getStackedData } from 'app/core/utils/chart-util'; + +export enum Layer { + LOADING = 'loading', + RETRY = 'retry', + CHART = 'chart' +} + +@Component({ + selector: 'pp-url-statistic-chart-container', + templateUrl: './url-statistic-chart-container.component.html', + styleUrls: ['./url-statistic-chart-container.component.css'] +}) +export class UrlStatisticChartContainerComponent implements OnInit, OnDestroy { + private unsubscribe = new Subject(); + private defaultYMax = 100; + private chartColorList: string[]; + private timezone: string; + private dateFormatMonth: string; + private dateFormatDay: string; + private cachedData: {[key: string]: {timestamp: number[], metricValues: IMetricValue[]}} = {}; + private fieldNameList: string[]; + private previousParams: IUrlStatChartDataParams; + + isUriSelected: boolean; + selectedUri: string; + chartConfig: IChartConfig; + showLoading: boolean; + showRetry: boolean; + retryMessage: string; + guideMessage: string; + emptyMessage: string; + chartVisibility = {}; + _activeLayer: Layer = Layer.LOADING; + + constructor( + private messageQueueService: MessageQueueService, + private webAppSettingDataService: WebAppSettingDataService, + private storeHelperService: StoreHelperService, + private newUrlStateNotificationService: NewUrlStateNotificationService, + private urlStatisticChartDataService: UrlStatisticChartDataService, + private translateService: TranslateService, + private el: ElementRef, + ) { } + + ngOnInit() { + this.initFieldNameList(); + this.initChartColorList(); + this.initI18nText(); + this.listenToEmitter(); + + this.newUrlStateNotificationService.onUrlStateChange$.pipe( + takeUntil(this.unsubscribe) + ).subscribe(() => { + this.cachedData = {}; + this.isUriSelected = false; + this.selectedUri = null; + this.chartConfig = null; + }); + + this.messageQueueService.receiveMessage(this.unsubscribe, MESSAGE_TO.SELECT_URL_INFO).pipe( + tap((uri) => { + this.isUriSelected = true; + this.selectedUri = uri; + }), + switchMap((uri: string) => { + if (Boolean(this.cachedData[uri])) { + return of(this.cachedData[uri]); + } else { + const urlService = this.newUrlStateNotificationService; + const from = urlService.getStartTimeToNumber(); + const to = urlService.getEndTimeToNumber(); + const applicationName = urlService.getPathValue(UrlPathId.APPLICATION).getApplicationName(); + const agentId = urlService.getPathValue(UrlPathId.AGENT_ID) || ''; + const params = this.previousParams = {from, to, applicationName, agentId, uri}; + + this.activeLayer = Layer.LOADING; + return this.urlStatisticChartDataService.getData(params).pipe( + map(({timestamp, metricValueGroups}: IUrlStatChartData) => { + this.cachedData[uri] = {timestamp, metricValues: metricValueGroups[0].metricValues}; + return this.cachedData[uri]; + }), + catchError((error: IServerError) => { + this.activeLayer = Layer.RETRY; + this.setRetryMessage(error.message); + + return EMPTY; + }) + ); + } + }) + ).subscribe((data: {timestamp: number[], metricValues: IMetricValue[]}) => { + this.setChartConfig(this.makeChartData(data)); + }); + } + + ngOnDestroy(): void { + this.unsubscribe.next(); + this.unsubscribe.complete(); + } + + set activeLayer(layer: Layer) { + this._activeLayer = layer; + this.setChartVisibility(this._activeLayer === Layer.CHART); + } + + get activeLayer(): Layer { + return this._activeLayer; + } + + private setChartVisibility(showChart: boolean): void { + this.chartVisibility = { + 'show-chart': showChart, + 'shady-chart': !showChart && this.chartConfig !== undefined, + }; + } + + onRendered(): void { + this.activeLayer = Layer.CHART; + } + + isActiveLayer(layer: string): boolean { + return this.activeLayer === layer; + } + + onRetry(): void { + this.activeLayer = Layer.LOADING; + this.urlStatisticChartDataService.getData(this.previousParams).pipe( + map(({timestamp, metricValueGroups}: IUrlStatChartData) => { + this.cachedData[this.selectedUri] = {timestamp, metricValues: metricValueGroups[0].metricValues}; + return this.cachedData[this.selectedUri]; + }), + catchError((error: IServerError) => { + this.activeLayer = Layer.RETRY; + this.setRetryMessage(error.message); + return EMPTY; + }), + ).subscribe((data: {timestamp: number[], metricValues: IMetricValue[]}) => { + this.setChartConfig(this.makeChartData(data)); + }); + } + + private initFieldNameList(): void { + this.fieldNameList = this.webAppSettingDataService.getUrlStatFieldNameList(); + } + + private initChartColorList(): void { + const computedStyle = getComputedStyle(this.el.nativeElement); + + this.chartColorList = [ + computedStyle.getPropertyValue('--chart-most-success'), + computedStyle.getPropertyValue('--chart-success'), + computedStyle.getPropertyValue('--chart-kinda-success'), + computedStyle.getPropertyValue('--chart-almost-normal'), + computedStyle.getPropertyValue('--chart-normal'), + computedStyle.getPropertyValue('--chart-slow'), + computedStyle.getPropertyValue('--chart-very-slow'), + computedStyle.getPropertyValue('--chart-fail'), + ] + } + + private initI18nText(): void { + forkJoin([ + this.translateService.get('URL_STAT.SELECT_URL_INFO'), + this.translateService.get('COMMON.NO_DATA') + ]).subscribe(([guideMessage, emptyMessage]: string[]) => { + this.guideMessage = guideMessage; + this.emptyMessage = emptyMessage; + }); + } + + private listenToEmitter(): void { + this.storeHelperService.getTimezone(this.unsubscribe).subscribe((timezone: string) => { + this.timezone = timezone; + }); + + this.storeHelperService.getDateFormatArray(this.unsubscribe, 6, 7).subscribe(([dateFormatMonth, dateFormatDay]: string[]) => { + this.dateFormatMonth = dateFormatMonth; + this.dateFormatDay = dateFormatDay; + }); + } + + private setRetryMessage(message: string): void { + this.retryMessage = message; + } + + private makeChartData({timestamp, metricValues}: {[key: string]: any}): PrimitiveArray[] { + return [ + ['x', ...makeXData(timestamp)], + ...metricValues.map(({values}: IMetricValue, i: number) => { + return [this.fieldNameList[i], ...values.map((v: number) => v < 0 ? null : v)]; + }) + ]; + } + + private setChartConfig(data: PrimitiveArray[]): void { + this.chartConfig = { + dataConfig: this.makeDataOption(data), + elseConfig: this.makeElseOption(getMaxTickValue(getStackedData(data), 1)), + }; + } + + private makeDataOption(columns: PrimitiveArray[]): Data { + const keyList = columns.slice(1).map(([key]: PrimitiveArray) => key as string); + + return { + x: 'x', + columns, + empty: { + label: { + text: this.emptyMessage + } + }, + type: bar(), + // type: areaStep(), + colors: keyList.reduce((acc: {[key: string]: string}, curr: string, i: number) => { + return { ...acc, [curr]: this.chartColorList[i] }; + }, {}), + groups: [keyList], + order: null + }; + } + + private makeElseOption(yMax: number): {[key: string]: any} { + return { + bar: { + width: { + ratio: 0.8 + } + }, + padding: { + top: 20, + bottom: 20, + right: 10 + }, + axis: { + x: { + type: 'timeseries', + tick: { + count: 6, + show: false, + format: (time: Date) => { + return moment(time).tz(this.timezone).format(this.dateFormatMonth) + '\n' + moment(time).tz(this.timezone).format(this.dateFormatDay); + } + }, + padding: { + left: 0, + right: 0 + } + }, + y: { + label: { + text: 'Total Count', + position: 'outer-middle' + }, + tick: { + // count: 3, + format: (v: number): string => this.convertWithUnit(v) + }, + padding: { + // top: 0, + bottom: 0 + }, + min: 0, + // max: yMax, + default: [0, this.defaultYMax] + } + }, + grid: { + y: { + show: true + } + }, + point: { + show: false, + }, + tooltip: { + order: '', + format: { + // value: (v: number): string => this.addComma(v.toString()) + value: (v: number): string => this.convertWithUnit(v) + } + }, + transition: { + duration: 0 + }, + zoom: { + enabled: zoom() + }, + }; + } + + private addComma(str: string): string { + return str.replace(/(\d)(?=(?:\d{3})+(?!\d))/g, '$1,'); + } + + private convertWithUnit(value: number): string { + const unitList = ['', 'K', 'M', 'G']; + + return [...unitList].reduce((acc: string, curr: string, i: number, arr: string[]) => { + const v = Number(acc); + + return v >= 1000 + ? (v / 1000).toString() + : (arr.splice(i + 1), Number.isInteger(v) ? `${v}${curr}` : `${v.toFixed(2)}${curr}`); + }, value.toString()); + } +} diff --git a/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart-data.service.ts b/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart-data.service.ts new file mode 100644 index 000000000000..96badb639a0d --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart-data.service.ts @@ -0,0 +1,32 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +export interface IUrlStatChartDataParams { + from: number; + to: number; + applicationName: string; + agentId: string; + uri: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class UrlStatisticChartDataService { + private url = 'uriStat/chart.pinpoint' + + constructor( + private http: HttpClient, + ) { } + + getData(params: IUrlStatChartDataParams): Observable { + return this.http.get(this.url, this.makeRequestOptionsArgs(params)); + } + + private makeRequestOptionsArgs(params: IUrlStatChartDataParams): object { + return { + params: {...params} + }; + } +} diff --git a/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart.component.css b/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart.component.css new file mode 100644 index 000000000000..68f5927f54aa --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart.component.css @@ -0,0 +1,22 @@ +:host { + display: block; + width: 100%; + height: 100%; + /* background-color: var(--background-default); */ +} + +:host(.show-chart) { + opacity: 1; + transition: all 1s; +} + +:host(.shady-chart) { + opacity: 0.5; + pointer-events: none; + transition: all 0.5s; +} + +.l-chart-section { + width: 100%; + height: 100%; +} diff --git a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-chart.component.html b/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart.component.html similarity index 100% rename from web/src/main/angular/src/app/core/components/url-statistic/url-statistic-chart.component.html rename to web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart.component.html diff --git a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-chart.component.ts b/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart.component.ts similarity index 98% rename from web/src/main/angular/src/app/core/components/url-statistic/url-statistic-chart.component.ts rename to web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart.component.ts index 627019072f38..8b518f37a7c0 100644 --- a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-chart.component.ts +++ b/web/src/main/angular/src/app/core/components/url-statistic-chart/url-statistic-chart.component.ts @@ -59,7 +59,7 @@ export class UrlStatisticChartComponent implements OnInit, OnChanges { : this.getEmptyDataKeys(currColumns); this.chartInstance.config('data.groups', [currKeys.slice(1)]); - this.chartInstance.config('axis.y.max', y.max); + // this.chartInstance.config('axis.y.max', y.max); this.chartInstance.load({ columns: currColumns, colors, diff --git a/web/src/main/angular/src/app/core/components/url-statistic-contents/index.ts b/web/src/main/angular/src/app/core/components/url-statistic-contents/index.ts new file mode 100644 index 000000000000..90a99f323669 --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-contents/index.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; + +import { SharedModule } from 'app/shared'; +import { UrlStatisticChartModule } from 'app/core/components/url-statistic-chart'; +import { UrlStatisticInfoModule } from 'app/core/components/url-statistic-info'; +import { UrlStatisticContentsContainerComponent } from './url-statistic-contents-container.component'; + +@NgModule({ + declarations: [ + UrlStatisticContentsContainerComponent + ], + imports: [ + SharedModule, + UrlStatisticInfoModule, + UrlStatisticChartModule + ], + exports: [ + UrlStatisticContentsContainerComponent + ], + providers: [], +}) +export class UrlStatisticContentsModule { } diff --git a/web/src/main/angular/src/app/core/components/url-statistic-contents/url-statistic-contents-container.component.css b/web/src/main/angular/src/app/core/components/url-statistic-contents/url-statistic-contents-container.component.css new file mode 100644 index 000000000000..2ddcabbd08c9 --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-contents/url-statistic-contents-container.component.css @@ -0,0 +1,46 @@ +:host { + display: block; + width: 100%; + height: 100%; + padding: 15px; +} + +.page-title-wrapper { + margin-bottom: 15px; +} + +.page-title { + font-size: 20px; + display: flex; + align-items: center; +} + +.beta-text { + font-style: italic; + font-weight: bold; + color: var(--primary); + margin-left: 5px; +} + +.page-title-icon { + margin-right: 5px; +} + +.main-contents { + width: 100%; + /* height: 100%; */ + height: calc(100% - 38px); + position: relative; + /* overflow: auto; */ +} + +.chart-wrapper { + width: 100%; + height: 45%; + margin-bottom: 30px; +} + +.info-wrapper { + width: 100%; + height: calc(55% - 30px); +} diff --git a/web/src/main/angular/src/app/core/components/url-statistic-contents/url-statistic-contents-container.component.html b/web/src/main/angular/src/app/core/components/url-statistic-contents/url-statistic-contents-container.component.html new file mode 100644 index 000000000000..143db2dd7b1c --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-contents/url-statistic-contents-container.component.html @@ -0,0 +1,11 @@ +
+

URL Statistic Beta

+
+
+
+ +
+
+ +
+
diff --git a/web/src/main/angular/src/app/core/components/url-statistic-contents/url-statistic-contents-container.component.ts b/web/src/main/angular/src/app/core/components/url-statistic-contents/url-statistic-contents-container.component.ts new file mode 100644 index 000000000000..9ca0527d2fe6 --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-contents/url-statistic-contents-container.component.ts @@ -0,0 +1,11 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'pp-url-statistic-contents-container', + templateUrl: './url-statistic-contents-container.component.html', + styleUrls: ['./url-statistic-contents-container.component.css'] +}) +export class UrlStatisticContentsContainerComponent implements OnInit { + constructor() {} + ngOnInit() {} +} diff --git a/web/src/main/angular/src/app/core/components/url-statistic-info/index.ts b/web/src/main/angular/src/app/core/components/url-statistic-info/index.ts new file mode 100644 index 000000000000..70a27dd2cdf9 --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-info/index.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; + +import { SharedModule } from 'app/shared'; +import { UrlStatisticInfoContainerComponent } from './url-statistic-info-container.component'; +import { UrlStatisticInfoComponent } from './url-statistic-info.component'; + +@NgModule({ + imports: [ + SharedModule + ], + exports: [ + UrlStatisticInfoContainerComponent + ], + declarations: [ + UrlStatisticInfoContainerComponent, + UrlStatisticInfoComponent + ], + providers: [], +}) +export class UrlStatisticInfoModule { } diff --git a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-chart.component.css b/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info-container.component.css similarity index 53% rename from web/src/main/angular/src/app/core/components/url-statistic/url-statistic-chart.component.css rename to web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info-container.component.css index 4f6884f63039..7d6a180af5c3 100644 --- a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-chart.component.css +++ b/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info-container.component.css @@ -2,9 +2,5 @@ display: block; width: 100%; height: 100%; -} - -.l-chart-section { - width: 100%; - height: 100%; + position: relative; } diff --git a/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info-container.component.html b/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info-container.component.html new file mode 100644 index 000000000000..8181ac83a2ad --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info-container.component.html @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info-container.component.ts b/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info-container.component.ts new file mode 100644 index 000000000000..b73bcbc333ad --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info-container.component.ts @@ -0,0 +1,64 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Observable, of, Subject } from 'rxjs'; +import { catchError, switchMap, takeUntil } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; + +import { MessageQueueService, MESSAGE_TO, NewUrlStateNotificationService } from 'app/shared/services'; +import { UrlStatisticInfoDataService } from './url-statistic-info-data.service'; +import { UrlPathId } from 'app/shared/models'; + +@Component({ + selector: 'pp-url-statistic-info-container', + templateUrl: './url-statistic-info-container.component.html', + styleUrls: ['./url-statistic-info-container.component.css'] +}) +export class UrlStatisticInfoContainerComponent implements OnInit, OnDestroy { + private unsubscribe = new Subject(); + + data$: Observable; + emptyMessage$: Observable; + errorMessage: string; + + constructor( + private urlStatisticInfoDataService: UrlStatisticInfoDataService, + private newUrlStateNotificationService: NewUrlStateNotificationService, + private messageQueueService: MessageQueueService, + private translateService: TranslateService, + ) { } + + ngOnInit() { + this.emptyMessage$ = this.translateService.get('COMMON.NO_DATA'); + this.data$ = this.newUrlStateNotificationService.onUrlStateChange$.pipe( + takeUntil(this.unsubscribe), + switchMap((urlService: NewUrlStateNotificationService) => { + const from = urlService.getStartTimeToNumber(); + const to = urlService.getEndTimeToNumber(); + const applicationName = urlService.getPathValue(UrlPathId.APPLICATION).getApplicationName(); + const agentId = urlService.getPathValue(UrlPathId.AGENT_ID) || ''; + const params = {from, to, applicationName, agentId}; + + return this.urlStatisticInfoDataService.getData(params); + }), + catchError((error: IServerError) => { + this.errorMessage = error.message; + return of([]) + }) + ); + } + + ngOnDestroy() { + this.unsubscribe.next(); + this.unsubscribe.complete(); + } + + onSelectUrlInfo(url: string): void { + this.messageQueueService.sendMessage({ + to: MESSAGE_TO.SELECT_URL_INFO, + param: url + }) + } + + onCloseErrorMessage(): void { + this.errorMessage = ''; + } +} diff --git a/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info-data.service.ts b/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info-data.service.ts new file mode 100644 index 000000000000..3d93de8a04c5 --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info-data.service.ts @@ -0,0 +1,32 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +export interface IUrlStatInfoDataParams { + from: number; + to: number; + applicationName: string; + agentId: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class UrlStatisticInfoDataService { + private url = 'uriStat/top50.pinpoint'; + + constructor( + private http: HttpClient, + ) { } + + + getData(params: IUrlStatInfoDataParams): Observable { + return this.http.get(this.url, this.makeRequestOptionsArgs(params)); + } + + private makeRequestOptionsArgs(params: IUrlStatInfoDataParams): object { + return { + params: {...params} + }; + } +} diff --git a/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info.component.css b/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info.component.css new file mode 100644 index 000000000000..93ba35e2a175 --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info.component.css @@ -0,0 +1,117 @@ +:host { + display: block; + width: 100%; + height: 100%; + + --url-count-ratio: var(--blue-200); + --url-count-ratio-background: var(--blue-grey-200); +} + +.url-info-table { + width: 100%; + /* height: 100%; */ + font-family: 'Open Sans',sans-serif; + font-size: 12px; + color: var(--text-primary-lighter); +} + +.url-info-header { + width: 100%; + /* height: 40px; */ +} + +.url-info-row { + display: flex; + height: 36px; + + gap: 15px; + padding-right: 20px; +} + +.url-info-row.active { + background-color: var(--background-hover-secondary); +} + +.url-info-row.active:hover { + background-color: var(--background-hover-secondary) !important; +} + +.url-info-row:not(.header-row, .empty):hover { + background-color: var(--background-hover-default); +} + +.url-info-row:not(.header-row) { + cursor: pointer; + border-top: 1px solid var(--border-primary); +} + +.url-info-row.header-row { + width: 100%; + height: 40px; +} + +.url-info-row.empty { + cursor: default; +} + +.url-info-cell { + display: flex; + align-items: center; + flex: 1; +} + +.url-info-cell.index-cell { + flex-grow: 0; + flex-basis: 40px; + justify-content: center; +} + +.url-info-cell.url-cell { + flex: 1.5; +} + +.url-info-cell.total-count-cell { + gap: 7px; +} + +.url-info-cell.failure-count-cell { + /* flex-grow: 0; + flex-basis: 150px; */ + /* justify-content: right; */ +} + +.url-info-cell.avg-cell, +.url-info-cell.max-cell { + /* flex-grow: 0; + flex-basis: 150px; */ + /* justify-content: right; */ +} + +.url-info-cell.header-cell { + height: 100%; + color: var(--text-primary); + font-weight: bold; + font-size: 13px; +} + +.url-info-body { + width: 100%; + /* height: calc(100% - 40px); */ + overflow: auto; +} + +.url-info-cell.empty { + font-size: 13px; + justify-content: center; + align-items: center; +} + +.ratio-bar { + width: 150px; + height: 20px; +} + +.total-count { + width: 50px; + text-align: right; +} diff --git a/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info.component.html b/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info.component.html new file mode 100644 index 000000000000..80f31e7b2a5c --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info.component.html @@ -0,0 +1,39 @@ + +
+
+
+
#
+
URL
+
Total Count
+
Failure Count
+
Avg(ms)
+
Max(ms)
+
+
+
+ +
+
{{emptyMessage}}
+
+
+ +
+
{{i + 1}}
+
{{urlInfo.uri}}
+
+
{{urlInfo.totalCount | number}}
+
+
+
{{urlInfo.failureCount | number}}
+
{{urlInfo.avgTimeMs | number: '1.0-0'}}
+
{{urlInfo.maxTimeMs | number: '1.0-0'}}
+
+
+
+
+ diff --git a/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info.component.ts b/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info.component.ts new file mode 100644 index 000000000000..98ed305f1df4 --- /dev/null +++ b/web/src/main/angular/src/app/core/components/url-statistic-info/url-statistic-info.component.ts @@ -0,0 +1,67 @@ +import { Component, Input, OnInit, Output, EventEmitter, SimpleChanges, OnChanges, ElementRef, Renderer2, ViewChild } from '@angular/core'; + +import { isEmpty } from 'app/core/utils/util'; + +@Component({ + selector: 'pp-url-statistic-info', + templateUrl: './url-statistic-info.component.html', + styleUrls: ['./url-statistic-info.component.css'] +}) +export class UrlStatisticInfoComponent implements OnInit, OnChanges { + @ViewChild('urlInfoTableBody' , {static: true}) urlInfoTableBody: ElementRef; + @Input() data: IUrlStatInfoData[]; + @Input() emptyMessage: string; + @Output() outSelectUrlInfo = new EventEmitter(); + + totalCount: number; + selectedUrl: string; + isEmpty: boolean; + + constructor( + private el: ElementRef, + private renderer: Renderer2 + ) {} + + ngOnChanges(changes: SimpleChanges) { + const dataChange = changes['data']; + + if (dataChange && dataChange.currentValue) { + const data = dataChange.currentValue as IUrlStatInfoData[]; + + this.isEmpty = isEmpty(data); + if (this.isEmpty) { + return; + } + + this.selectedUrl = ''; + this.totalCount = data.reduce((acc: number, {totalCount}: any) => { + return acc + totalCount; + }, 0); + } + } + + ngOnInit() { + this.renderer.setStyle(this.urlInfoTableBody.nativeElement, 'max-height', `${this.el.nativeElement.offsetHeight - 40}px`); + } + + onSelectUrlInfo(url: string): void { + if (this.selectedUrl === url) { + return; + } + + this.selectedUrl = url; + this.outSelectUrlInfo.emit(url); + } + + isSelectedUrl(url: string): boolean { + return this.selectedUrl === url; + } + + getRatioBackgroundColor(count: number): string { + const computedStyle = getComputedStyle(this.el.nativeElement); + const urlCountRatio = computedStyle.getPropertyValue('--url-count-ratio'); + const urlCountRatioBG = computedStyle.getPropertyValue('--url-count-ratio-background'); + + return `linear-gradient(to right, ${urlCountRatio} ${count / this.totalCount * 100}%, ${urlCountRatioBG} ${count / this.totalCount * 100}% 100%)`; + } +} diff --git a/web/src/main/angular/src/app/core/components/url-statistic/index.ts b/web/src/main/angular/src/app/core/components/url-statistic/index.ts deleted file mode 100644 index 2e5ff6034e8a..000000000000 --- a/web/src/main/angular/src/app/core/components/url-statistic/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NgModule } from '@angular/core'; -import { SharedModule } from 'app/shared'; - -import { UrlStatisticContainerComponent } from './url-statistic-container.component'; -import { UrlStatisticChartComponent } from './url-statistic-chart.component'; -import { UrlStatisticDataService } from './url-statistic-data.service'; -import { UrlStatisticInfoComponent } from './url-statistic-info.component'; - -@NgModule({ - imports: [ - SharedModule - ], - exports: [ - UrlStatisticContainerComponent - ], - declarations: [ - UrlStatisticContainerComponent, - UrlStatisticChartComponent, - UrlStatisticInfoComponent - ], - providers: [ - UrlStatisticDataService - ], -}) -export class UrlStatisticModule { } diff --git a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-container.component.css b/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-container.component.css deleted file mode 100644 index fb40be427b60..000000000000 --- a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-container.component.css +++ /dev/null @@ -1,11 +0,0 @@ -:host { - display: block; - height: 100%; - padding: 25px; -} - -.l-chart-section { - position: relative; - height: 450px; - margin-bottom: 30px; -} diff --git a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-container.component.html b/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-container.component.html deleted file mode 100644 index f1565804e8fc..000000000000 --- a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-container.component.html +++ /dev/null @@ -1,14 +0,0 @@ -
- - - - -
-
- - -
diff --git a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-container.component.ts b/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-container.component.ts deleted file mode 100644 index df8e6ffe95ce..000000000000 --- a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-container.component.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { map, tap } from 'rxjs/operators'; -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { Data, PrimitiveArray, bar, zoom } from 'billboard.js'; -import * as moment from 'moment-timezone'; -import { Subject } from 'rxjs'; - -import { UrlStatisticDataService } from './url-statistic-data.service'; -import { makeYData, makeXData, getMaxTickValue, getStackedData } from 'app/core/utils/chart-util'; -import { StoreHelperService, WebAppSettingDataService } from 'app/shared/services'; - -@Component({ - selector: 'pp-url-statistic-container', - templateUrl: './url-statistic-container.component.html', - styleUrls: ['./url-statistic-container.component.css'] -}) -export class UrlStatisticContainerComponent implements OnInit, OnDestroy { - private unsubscribe = new Subject(); - private defaultYMax = 100; - private chartColors: string[]; - private timezone: string; - private dateFormatMonth: string; - private dateFormatDay: string; - - urlStatisticData: any[]; - chartConfig: IChartConfig; - showLoading = true; - - constructor( - private urlStatisticDataService: UrlStatisticDataService, - private webAppSettingDataService: WebAppSettingDataService, - private storeHelperService: StoreHelperService, - ) { } - - ngOnInit() { - this.initChartColors(); - this.listenToEmitter(); - this.initData(); - } - - ngOnDestroy() { - this.unsubscribe.next(); - this.unsubscribe.complete(); - } - - onRendered(): void { - this.showLoading = false; - } - - onSelectUrl(selectedUrl: string): void { - // TODO: Refactor/extract set chartdata logic - const data = this.urlStatisticData.find(({uri}: any) => uri === selectedUrl); - - this.setChartConfig(this.makeChartData(data)); - } - - private initChartColors(): void { - this.chartColors = this.webAppSettingDataService.getColorByRequestInDetail(); - } - - private listenToEmitter(): void { - this.storeHelperService.getTimezone(this.unsubscribe).subscribe((timezone: string) => { - this.timezone = timezone; - }); - - this.storeHelperService.getDateFormatArray(this.unsubscribe, 6, 7).subscribe(([dateFormatMonth, dateFormatDay]: string[]) => { - this.dateFormatMonth = dateFormatMonth; - this.dateFormatDay = dateFormatDay; - }); - } - - private initData(): void { - // TODO: Add Error handling - this.urlStatisticDataService.getData().pipe( - tap((data: any[]) => { - console.log(data); - this.urlStatisticData = data; - }), - map((data: any[]) => this.makeChartData(data[0])) - ).subscribe((data: PrimitiveArray[]) => { - this.setChartConfig(data); - }); - } - - private makeChartData({charts}: any): PrimitiveArray[] { - // const data = charts.y['HISTOGRAM_BUCKET']; - // const parsedData = data.reduce((acc: number[][], curr: number[]) => { - // return acc.length === 0 ? curr.map((c: number) => [c]) - // : acc.map((a: number[], i: number) => [...a, curr[i]]); - // }, []); - - // * Use mock data temporarily - const size = 128; - const startDatetime = 1607463120000; - const interval = 1000 * 60; - - const x = Array(size) - .fill(0) - .map((v, i) => startDatetime + interval * i); - - const data = [300, 200, 100, 50, 30, 20, 10, 10] - .map(v => Array(size) - .fill(0) - .map(() => Math.round(Math.random() * v)) - ); - - return [ - ['x', ...x], - ['0 ~ 100', ...data[0]], - ['100 ~ 300', ...data[1]], - ['300 ~ 500', ...data[2]], - ['500 ~ 1000', ...data[3]], - ['1000 ~ 3000', ...data[4]], - ['3000 ~ 5000', ...data[5]], - ['5000 ~ 8000', ...data[6]], - ['8000 ~ ', ...data[7]], - // ['x', ...makeXData(charts.x)], - // ['0 ~ 100', ...makeYData(charts.y['HISTOGRAM_BUCKET'], 0)], - // ['100 ~ 300', ...makeYData(charts.y['HISTOGRAM_BUCKET'], 1)], - // ['300 ~ 500', ...makeYData(charts.y['HISTOGRAM_BUCKET'], 2)], - // ['500 ~ 1000', ...makeYData(charts.y['HISTOGRAM_BUCKET'], 3)], - // ['1000 ~ 3000', ...makeYData(charts.y['HISTOGRAM_BUCKET'], 4)], - // ['3000 ~ 5000', ...makeYData(charts.y['HISTOGRAM_BUCKET'], 5)], - // ['5000 ~ 8000', ...makeYData(charts.y['HISTOGRAM_BUCKET'], 6)], - // ['8000 ~ ', ...makeYData(charts.y['HISTOGRAM_BUCKET'], 7)], - ]; - } - - private setChartConfig(data: PrimitiveArray[]): void { - this.chartConfig = { - dataConfig: this.makeDataOption(data), - elseConfig: this.makeElseOption(getMaxTickValue(getStackedData(data), 1)), - }; - } - - private makeDataOption(columns: PrimitiveArray[]): Data { - const keyList = columns.slice(1).map(([key]: PrimitiveArray) => key as string); - - return { - x: 'x', - columns, - // empty: { - // label: { - // text: this.dataEmptyText - // } - // }, - type: bar(), - colors: keyList.reduce((acc: {[key: string]: string}, curr: string, i: number) => { - return { ...acc, [curr]: this.chartColors[i] }; - }, {}), - groups: [keyList], - order: null - }; - } - - private makeElseOption(yMax: number): {[key: string]: any} { - return { - bar: { - width: { - ratio: 0.8 - } - }, - padding: { - top: 20, - bottom: 20, - right: 10 - }, - axis: { - x: { - type: 'timeseries', - tick: { - count: 6, - show: false, - format: (time: Date) => { - return moment(time).tz(this.timezone).format(this.dateFormatMonth) + '\n' + moment(time).tz(this.timezone).format(this.dateFormatDay); - } - }, - padding: { - left: 0, - right: 0 - } - }, - y: { - tick: { - count: 3, - format: (v: number): string => this.convertWithUnit(v) - }, - padding: { - top: 0, - bottom: 0 - }, - min: 0, - max: yMax, - default: [0, this.defaultYMax] - } - }, - grid: { - y: { - show: true - } - }, - tooltip: { - order: '', - format: { - value: (v: number) => this.addComma(v.toString()) - } - }, - transition: { - duration: 0 - }, - zoom: { - enabled: zoom() - }, - }; - } - - private addComma(str: string): string { - return str.replace(/(\d)(?=(?:\d{3})+(?!\d))/g, '$1,'); - } - - private convertWithUnit(value: number): string { - const unitList = ['', 'K', 'M', 'G']; - - return [...unitList].reduce((acc: string, curr: string, i: number, arr: string[]) => { - const v = Number(acc); - - return v >= 1000 - ? (v / 1000).toString() - : (arr.splice(i + 1), Number.isInteger(v) ? `${v}${curr}` : `${v.toFixed(2)}${curr}`); - }, value.toString()); - } -} diff --git a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-data.service.ts b/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-data.service.ts deleted file mode 100644 index 6c45501fdb04..000000000000 --- a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-data.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Observable } from 'rxjs'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; - -@Injectable() -export class UrlStatisticDataService { - private url = 'getAgentStat/uriStat/chartList.pinpoint'; - - constructor( - private http: HttpClient, - ) {} - - getData(): Observable { - return this.http.get(this.url, this.makeRequestOptionsArgs()); - } - - private makeRequestOptionsArgs(): any { - // TODO: Deliver real param - return { - params: { - agentId: 'FrontWAS2', - from: 1614044341000, - to: 1614045541000, - sampleRate: 1 - } - }; - } -} diff --git a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-info.component.css b/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-info.component.css deleted file mode 100644 index a2927b3d78b0..000000000000 --- a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-info.component.css +++ /dev/null @@ -1,50 +0,0 @@ -:host { - display: block; - width: 100%; - height: 100%; -} - -.l-content-group { - height: 36px; -} - -.l-content-group.active { - background-color: var(--background-hover-secondary); -} - -.l-content-group.active:hover { - background-color: var(--background-hover-secondary); -} - -.l-content-group:hover { - background-color: var(--background-hover-default); -} - -.l-content-item { - text-align: center; - border-top: 1px solid var(--border-primary); - padding: 0; -} - -.l-url { - cursor: pointer; -} - -.l-ratio-bar { - display: inline-block; - width: 100px; - height: 20px; - margin-left: 7px; -} - -.l-sub { - height: 100%; - display: flex; - justify-content: center; - align-items: center; -} - -.l-count { - width: 50px; - text-align: right; -} diff --git a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-info.component.html b/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-info.component.html deleted file mode 100644 index b6735300a31a..000000000000 --- a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-info.component.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - -
URLTotal CountAvgMax
{{urlInfo.uri}} -
-
{{urlInfo.totalCount}}
-
- -
-
-
{{urlInfo.avgTime}}{{urlInfo.maxTime}}
-
diff --git a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-info.component.ts b/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-info.component.ts deleted file mode 100644 index 0fc9d11a2e61..000000000000 --- a/web/src/main/angular/src/app/core/components/url-statistic/url-statistic-info.component.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Component, Input, OnInit, Output, EventEmitter, SimpleChanges, OnChanges } from '@angular/core'; - -@Component({ - selector: 'pp-url-statistic-info', - templateUrl: './url-statistic-info.component.html', - styleUrls: ['./url-statistic-info.component.css'] -}) -export class UrlStatisticInfoComponent implements OnInit, OnChanges { - @Input() data: any[]; - @Output() outSelectUrl = new EventEmitter(); - - private totalCount: number; - - selectedUrl: string; - - constructor() {} - ngOnChanges(changes: SimpleChanges) { - const dataChange = changes['data']; - - if (dataChange && dataChange.currentValue) { - this.selectedUrl = dataChange.currentValue[0].uri; - this.totalCount = dataChange.currentValue.reduce((acc: number, {totalCount}: any) => { - return acc + totalCount; - }, 0); - } - } - - ngOnInit() {} - onSelectUrl(url: string): void { - if (this.selectedUrl === url) { - return; - } - - this.selectedUrl = url; - this.outSelectUrl.emit(url); - } - - isSelectedUrl(url: string): boolean { - return this.selectedUrl === url; - } - - getRatioBackgroundColor(count: number): string { - return `linear-gradient(to right, #DAE6F0 ${count / this.totalCount * 100}%, #F2F2F2 ${count / this.totalCount * 100}% 100%)`; - } -} diff --git a/web/src/main/angular/src/app/routes/inspector-page/index.ts b/web/src/main/angular/src/app/routes/inspector-page/index.ts index 52cbd82fc911..cd3c2f023399 100644 --- a/web/src/main/angular/src/app/routes/inspector-page/index.ts +++ b/web/src/main/angular/src/app/routes/inspector-page/index.ts @@ -7,7 +7,6 @@ import { NoticeModule } from 'app/core/components/notice'; import { ApplicationListModule } from 'app/core/components/application-list'; import { PeriodSelectorModule } from 'app/core/components/period-selector'; import { ConfigurationIconModule } from 'app/core/components/configuration-icon'; -import { ApplicationInspectorTitleModule } from 'app/core/components/application-inspector-title'; import { ServerAndAgentListModule } from 'app/core/components/server-and-agent-list'; import { ApplicationInspectorContentsModule } from 'app/core/components/application-inspector-contents'; import { AgentInspectorContentsModule } from 'app/core/components/agent-inspector-contents'; @@ -27,7 +26,6 @@ import { SideNavigationBarModule } from 'app/core/components/side-navigation-bar ApplicationListModule, PeriodSelectorModule, ConfigurationIconModule, - ApplicationInspectorTitleModule, ServerAndAgentListModule, ApplicationInspectorContentsModule, AgentInspectorContentsModule, diff --git a/web/src/main/angular/src/app/routes/inspector-page/inspector-page.component.css b/web/src/main/angular/src/app/routes/inspector-page/inspector-page.component.css index 78b4661c5086..cadf3e9c3de2 100644 --- a/web/src/main/angular/src/app/routes/inspector-page/inspector-page.component.css +++ b/web/src/main/angular/src/app/routes/inspector-page/inspector-page.component.css @@ -78,3 +78,36 @@ button { .l-auth-img { width: 200px; } + +.selected-app-wrapper { + display: flex; + border-bottom: 1px solid var(--border-primary); + height: 40px; + gap: 4px; + padding: 0 6px 0 13px; + align-items: center; +} + +.selected-app-wrapper.active { + color: var(--primary); + font-weight: 600; +} + +.selected-app-wrapper:hover { + cursor: pointer; + color: var(--primary); + background-color: var(--background-hover-default); +} + +.app-img { + width: 22px; + height: 18px; +} + +.selected-app-name { + flex: 1; + font-size: 14px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} \ No newline at end of file diff --git a/web/src/main/angular/src/app/routes/inspector-page/inspector-page.component.html b/web/src/main/angular/src/app/routes/inspector-page/inspector-page.component.html index ba2d80f47516..4f2b4895dbc0 100644 --- a/web/src/main/angular/src/app/routes/inspector-page/inspector-page.component.html +++ b/web/src/main/angular/src/app/routes/inspector-page/inspector-page.component.html @@ -18,9 +18,9 @@
-
- -
+
+ {{selectedAppName}} +
diff --git a/web/src/main/angular/src/app/routes/inspector-page/inspector-page.component.ts b/web/src/main/angular/src/app/routes/inspector-page/inspector-page.component.ts index 9ca6bd3b6181..5b0909877623 100644 --- a/web/src/main/angular/src/app/routes/inspector-page/inspector-page.component.ts +++ b/web/src/main/angular/src/app/routes/inspector-page/inspector-page.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit, ComponentFactoryResolver, Injector, OnDestroy } from '@angular/core'; import { state, style, animate, transition, trigger } from '@angular/animations'; import { Observable, Subject } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { map, takeUntil, tap } from 'rxjs/operators'; -import { NewUrlStateNotificationService, AnalyticsService, TRACKED_EVENT_LIST, DynamicPopupService, WebAppSettingDataService, MessageQueueService, MESSAGE_TO } from 'app/shared/services'; -import { UrlPathId } from 'app/shared/models'; +import { NewUrlStateNotificationService, AnalyticsService, TRACKED_EVENT_LIST, DynamicPopupService, WebAppSettingDataService, MessageQueueService, MESSAGE_TO, UrlRouteManagerService } from 'app/shared/services'; +import { UrlPath, UrlPathId } from 'app/shared/models'; import { HELP_VIEWER_LIST, HelpViewerPopupContainerComponent } from 'app/core/components/help-viewer-popup/help-viewer-popup-container.component'; import { InspectorPageService } from './inspector-page.service'; @@ -34,6 +34,7 @@ import { InspectorPageService } from './inspector-page.service'; }) export class InspectorPageComponent implements OnInit, OnDestroy { private unsubscribe = new Subject(); + private funcImagePath: Function; sideNavigationUI: boolean; unAuthImgPath: string; @@ -42,6 +43,9 @@ export class InspectorPageComponent implements OnInit, OnDestroy { isAccessDenyed$: Observable; mainSectionStyle = {}; + isAppActivated: boolean; + selectedAppImg: string; + selectedAppName: string; constructor( private inspectorPageService: InspectorPageService, @@ -52,14 +56,25 @@ export class InspectorPageComponent implements OnInit, OnDestroy { private injector: Injector, private webAppSettingDataService: WebAppSettingDataService, private messageQueueService: MessageQueueService, + private urlRouteManagerService: UrlRouteManagerService, ) {} ngOnInit() { + this.funcImagePath = this.webAppSettingDataService.getIconPathMakeFunc(); this.sideNavigationUI = this.webAppSettingDataService.getExperimentalOption('sideNavigationUI'); this.unAuthImgPath = this.webAppSettingDataService.getServerMapIconPathMakeFunc()('UNAUTHORIZED'); this.newUrlStateNotificationService.onUrlStateChange$.pipe( takeUntil(this.unsubscribe), + tap((urlService: NewUrlStateNotificationService) => { + if (urlService.hasValue(UrlPathId.APPLICATION)) { + const selectedApp: IApplication = urlService.getPathValue(UrlPathId.APPLICATION); + + this.selectedAppName = selectedApp.getApplicationName(); + this.selectedAppImg = this.funcImagePath(selectedApp.getServiceType()); + this.isAppActivated = !urlService.hasValue(UrlPathId.AGENT_ID); + } + }), map((urlService: NewUrlStateNotificationService) => { return urlService.isRealTimeMode() || urlService.hasValue(UrlPathId.END_TIME); }) @@ -78,6 +93,17 @@ export class InspectorPageComponent implements OnInit, OnDestroy { this.unsubscribe.complete(); } + onSelectApp(): void { + this.urlRouteManagerService.moveOnPage({ + url: [ + UrlPath.INSPECTOR, + this.newUrlStateNotificationService.getPathValue(UrlPathId.APPLICATION).getUrlStr(), + this.newUrlStateNotificationService.getPathValue(UrlPathId.PERIOD).getValueWithTime(), + this.newUrlStateNotificationService.getPathValue(UrlPathId.END_TIME).getEndTime(), + ] + }) + } + onShowHelp($event: MouseEvent): void { this.analyticsService.trackEvent(TRACKED_EVENT_LIST.TOGGLE_HELP_VIEWER, HELP_VIEWER_LIST.NAVBAR); const {left, top, width, height} = ($event.target as HTMLElement).getBoundingClientRect(); diff --git a/web/src/main/angular/src/app/routes/inspector-page/inspector-page.service.ts b/web/src/main/angular/src/app/routes/inspector-page/inspector-page.service.ts index 6641efdf00db..dc88eccd3504 100644 --- a/web/src/main/angular/src/app/routes/inspector-page/inspector-page.service.ts +++ b/web/src/main/angular/src/app/routes/inspector-page/inspector-page.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { map, tap, takeUntil } from 'rxjs/operators'; import { Observable, Subject, BehaviorSubject } from 'rxjs'; import { @@ -11,12 +10,6 @@ import { import { UrlPath, UrlPathId } from 'app/shared/models'; import { Timeline } from 'app/core/components/timeline/class'; -export interface ISourceForServerAndAgentList { - range: number[]; - agentId: string; - emitAfter: number; -} - export interface ISourceForTimeline { timelineInfo: ITimelineInfo; agentId?: string; @@ -37,7 +30,6 @@ export interface ISourceForTimelineCommand { @Injectable() export class InspectorPageService { - private sourceForServerAndAgentList = new BehaviorSubject(null); private sourceForTimeline = new Subject(); private sourceForAgentInfo = new Subject(); private sourceForChart = new Subject(); @@ -46,7 +38,6 @@ export class InspectorPageService { private timelineInfo: ITimelineInfo; private agentId: string; - sourceForServerAndAgentList$: Observable; sourceForTimeline$: Observable; sourceForAgentInfo$: Observable; sourceForChart$: Observable; @@ -57,7 +48,6 @@ export class InspectorPageService { private webAppSettingDataService: WebAppSettingDataService, private messageQueueService: MessageQueueService, ) { - this.sourceForServerAndAgentList$ = this.sourceForServerAndAgentList.asObservable(); this.sourceForTimeline$ = this.sourceForTimeline.asObservable(); this.sourceForAgentInfo$ = this.sourceForAgentInfo.asObservable(); this.sourceForChart$ = this.sourceForChart.asObservable(); @@ -65,27 +55,8 @@ export class InspectorPageService { } activate(unsubscribe: Subject): void { - this.newUrlStateNotificationService.onUrlStateChange$.pipe( - takeUntil(unsubscribe), - tap((urlService: NewUrlStateNotificationService) => { - this.agentId = urlService.getPathValue(UrlPathId.AGENT_ID); - }), - map((urlService: NewUrlStateNotificationService) => { - if (urlService.isRealTimeMode()) { - const to = urlService.getUrlServerTimeData(); - const from = to - this.webAppSettingDataService.getSystemDefaultPeriod().getMiliSeconds(); - - return [from, to]; - } else { - return [urlService.getStartTimeToNumber(), urlService.getEndTimeToNumber()]; - } - - }) - ).subscribe((range: number[]) => { - this.notifyToServerAndAgentList(range); - }); - - this.messageQueueService.receiveMessage(unsubscribe, MESSAGE_TO.INSPECTOR_PAGE_VALID).subscribe(({range, now}: {range: number[], now: number}) => { + this.messageQueueService.receiveMessage(unsubscribe, MESSAGE_TO.AGENT_LIST_VALID).subscribe(({range, now, agentId}: {range: number[], now: number, agentId: string}) => { + this.agentId = agentId; this.notifyToTimeline(range); this.notifyToTimelineCommand(); this.notifyToAgentInfo(); @@ -99,7 +70,13 @@ export class InspectorPageService { const from = to - this.webAppSettingDataService.getSystemDefaultPeriod().getMiliSeconds(); const emitAfter = isDelayed ? 0 : reservedNextTo - now; - this.notifyToServerAndAgentList([from, to], emitAfter); + this.messageQueueService.sendMessage({ + to: MESSAGE_TO.FETCH_AGENT_LIST, + param: { + range: [from, to], + emitAfter + } + }) } }); } @@ -151,14 +128,6 @@ export class InspectorPageService { }); } - private notifyToServerAndAgentList(range: number[], emitAfter = 0): void { - this.sourceForServerAndAgentList.next({ - range, - agentId: this.agentId, - emitAfter - }); - } - private notifyToAgentInfo(): void { this.sourceForAgentInfo.next({ selectedTime: this.timelineInfo.selectedTime, diff --git a/web/src/main/angular/src/app/routes/url-statistic-page/index.ts b/web/src/main/angular/src/app/routes/url-statistic-page/index.ts index 63ffc8d03e1c..941d444f0009 100644 --- a/web/src/main/angular/src/app/routes/url-statistic-page/index.ts +++ b/web/src/main/angular/src/app/routes/url-statistic-page/index.ts @@ -3,7 +3,12 @@ import { NgModule } from '@angular/core'; import { SharedModule } from 'app/shared'; import { UrlStatisticPageRoutingModule } from './url-statistic-page.routing'; import { UrlStatisticPageComponent } from './url-statistic-page.component'; -import { UrlStatisticModule } from 'app/core/components/url-statistic'; +import { ApplicationListModule } from 'app/core/components/application-list'; +import { NoticeModule } from 'app/core/components/notice'; +import { PeriodSelectorModule } from 'app/core/components/period-selector'; +import { ServerAndAgentListModule } from 'app/core/components/server-and-agent-list'; +import { SideNavigationBarModule } from 'app/core/components/side-navigation-bar'; +import { UrlStatisticContentsModule } from 'app/core/components/url-statistic-contents'; @NgModule({ declarations: [ @@ -11,8 +16,13 @@ import { UrlStatisticModule } from 'app/core/components/url-statistic'; ], imports: [ SharedModule, + NoticeModule, + SideNavigationBarModule, + ApplicationListModule, + PeriodSelectorModule, + ServerAndAgentListModule, + UrlStatisticContentsModule, UrlStatisticPageRoutingModule, - UrlStatisticModule ], exports: [], providers: [] diff --git a/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.component.css b/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.component.css index adb41ad830a2..4ec547938f65 100644 --- a/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.component.css +++ b/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.component.css @@ -1,9 +1,80 @@ :host { display: block; + width: 100%; height: 100%; - background-color: var(--background-primary); } -.l-main-container { +.container { + display: flex; +} + +.widget-group { + display: flex; + padding: 0 15px; + height: 50px; + width: 100%; + border-bottom: 1px solid var(--border-primary); + align-items: center; + background: var(--background-default); +} + +.main-container { + display: flex; + flex-flow: row wrap; + height: 100vh; + width: 100%; + overflow-y: hidden; +} + +.sidemenu-wrap { + position: relative; + height: calc(100% - 50px); +} + +.sidemenu-left { + width: 250px; + overflow: visible; + border-right: 1px solid var(--border-primary); + background: var(--background-default); + height: 100%; +} + +.main-section { + /* flex: 1; */ + background: var(--background-primary); + position: relative; height: calc(100% - 50px); } + +.selected-app-wrapper { + display: flex; + border-bottom: 1px solid var(--border-primary); + height: 40px; + gap: 4px; + padding: 0 6px 0 13px; + align-items: center; +} + +.selected-app-wrapper.active { + color: var(--primary); + font-weight: 600; +} + +.selected-app-wrapper:hover { + cursor: pointer; + color: var(--primary); + background-color: var(--background-hover-default); +} + +.app-img { + width: 22px; + height: 18px; +} + +.selected-app-name { + flex: 1; + font-size: 14px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} diff --git a/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.component.html b/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.component.html index 23fbeab94740..fe6ab85fb62e 100644 --- a/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.component.html +++ b/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.component.html @@ -1,6 +1,23 @@ - -
- -
+ + +
+ +
+
+ + +
+
+
+
+ {{selectedAppName}} +
+ +
+
+
+ +
+
+
+
diff --git a/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.component.ts b/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.component.ts index abb762f1a7f9..71a8df6394d8 100644 --- a/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.component.ts +++ b/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.component.ts @@ -1,11 +1,77 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { takeUntil, map, tap } from 'rxjs/operators'; + +import { NewUrlStateNotificationService, UrlRouteManagerService, WebAppSettingDataService } from 'app/shared/services'; +import { UrlPath, UrlPathId } from 'app/shared/models'; @Component({ selector: 'pp-url-statistic-page', templateUrl: './url-statistic-page.component.html', styleUrls: ['./url-statistic-page.component.css'] }) -export class UrlStatisticPageComponent implements OnInit { - constructor() {} - ngOnInit() {} +export class UrlStatisticPageComponent implements OnInit, OnDestroy { + private unsubscribe = new Subject(); + private funcImagePath: Function; + + showUrlStat$: Observable; + showSideMenu: boolean; + mainSectionStyle = {}; + isAppActivated: boolean; + selectedAppImg: string; + selectedAppName: string; + + constructor( + private newUrlStateNotificationService: NewUrlStateNotificationService, + private webAppSettingDataService: WebAppSettingDataService, + private urlRouteManagerService: UrlRouteManagerService, + ) {} + + ngOnInit() { + this.showUrlStat$ = this.webAppSettingDataService.showUrlStat().pipe( + tap((showUrlStat) => { + if (!showUrlStat) { + this.urlRouteManagerService.moveToMain(); + } + }) + ); + + this.funcImagePath = this.webAppSettingDataService.getIconPathMakeFunc(); + this.newUrlStateNotificationService.onUrlStateChange$.pipe( + takeUntil(this.unsubscribe), + tap((urlService: NewUrlStateNotificationService) => { + if (urlService.hasValue(UrlPathId.APPLICATION)) { + const selectedApp: IApplication = urlService.getPathValue(UrlPathId.APPLICATION); + + this.selectedAppName = selectedApp.getApplicationName(); + this.selectedAppImg = this.funcImagePath(selectedApp.getServiceType()); + this.isAppActivated = !urlService.hasValue(UrlPathId.AGENT_ID); + } + }), + map((urlService: NewUrlStateNotificationService) => { + return urlService.isRealTimeMode() || urlService.hasValue(UrlPathId.END_TIME); + }) + ).subscribe((showSideMenu: boolean) => { + this.showSideMenu = showSideMenu; + this.mainSectionStyle = { + width: showSideMenu ? 'calc(100% - 250px)' : '100%' + }; + }); + } + + ngOnDestroy() { + this.unsubscribe.next(); + this.unsubscribe.complete(); + } + + onSelectApp(): void { + this.urlRouteManagerService.moveOnPage({ + url: [ + UrlPath.URL_STATISTIC, + this.newUrlStateNotificationService.getPathValue(UrlPathId.APPLICATION).getUrlStr(), + this.newUrlStateNotificationService.getPathValue(UrlPathId.PERIOD).getValueWithTime(), + this.newUrlStateNotificationService.getPathValue(UrlPathId.END_TIME).getEndTime(), + ] + }) + } } diff --git a/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.routing.ts b/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.routing.ts index 9cb3104a525d..fb3ce67225c8 100644 --- a/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.routing.ts +++ b/web/src/main/angular/src/app/routes/url-statistic-page/url-statistic-page.routing.ts @@ -1,13 +1,72 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { UrlStatisticContentsContainerComponent } from 'app/core/components/url-statistic-contents/url-statistic-contents-container.component'; +import { EmptyContentsComponent } from 'app/shared/components/empty-contents'; +import { UrlRedirectorComponent } from 'app/shared/components/url-redirector'; +import { UrlPath, UrlPathId } from 'app/shared/models'; import { UrlStatisticPageComponent } from './url-statistic-page.component'; const routes: Routes = [ { path: '', - component: UrlStatisticPageComponent - } + component: UrlStatisticPageComponent, + children: [ + { + path: ':' + UrlPathId.APPLICATION, + children: [ + { + path: '', + pathMatch: 'full', + data: { + path: UrlPath.URL_STATISTIC + }, + component: UrlRedirectorComponent + }, + { + path: ':' + UrlPathId.PERIOD, + children: [ + { + path: '', + pathMatch: 'full', + data: { + path: UrlPath.URL_STATISTIC + }, + component: UrlRedirectorComponent + }, + { + path: ':' + UrlPathId.END_TIME, + data: { + showRealTimeButton: false, + enableRealTimeMode: false + }, + children: [ + { + path: '', + pathMatch: 'full', + component: UrlStatisticContentsContainerComponent + }, + { + path: ':' + UrlPathId.AGENT_ID, + component: UrlStatisticContentsContainerComponent + } + ] + } + ] + }, + ] + }, + { + path: '', + pathMatch: 'full', + data: { + showRealTimeButton: false, + enableRealTimeMode: false + }, + component: EmptyContentsComponent + } + ] + }, ]; @NgModule({ diff --git a/web/src/main/angular/src/app/shared/services/analytics.service.ts b/web/src/main/angular/src/app/shared/services/analytics.service.ts index dbb9c1778c4b..edcdb656cc77 100644 --- a/web/src/main/angular/src/app/shared/services/analytics.service.ts +++ b/web/src/main/angular/src/app/shared/services/analytics.service.ts @@ -60,8 +60,7 @@ export enum TRACKED_EVENT_LIST { CLICK_FIXED_PERIOD_MOVE_BUTTON = 'Click Fixed Period Move Button', OPEN_INSPECTOR_WITH_AGENT = 'Open Inspector with Agent', SELECT_AREA_ON_SCATTER = 'Select Area on Scatter', - GO_TO_APPLICATION_INSPECTOR = 'Go to Application Inspector', - GO_TO_AGENT_INSPECTOR = 'Go To Agent Inspector', + SELECT_AGENT_ON_THE_LIST = 'Select Agent on the List', ZOOM_IN_TIMELINE = 'Zoom in Timeline', ZOOM_OUT_TIMELINE = 'Zoom out Timeline', MOVE_TO_PREV_ON_TIMELINE = 'Move to Prev on Timeline', diff --git a/web/src/main/angular/src/app/shared/services/component-default-setting-data.service.ts b/web/src/main/angular/src/app/shared/services/component-default-setting-data.service.ts index 39463c3a582d..ec02632a0429 100644 --- a/web/src/main/angular/src/app/shared/services/component-default-setting-data.service.ts +++ b/web/src/main/angular/src/app/shared/services/component-default-setting-data.service.ts @@ -50,6 +50,16 @@ export class ComponentDefaultSettingDataService { new Period(10080), new Period(20160), new Period(40320), + ], + [UrlPath.URL_STATISTIC]: [ + new Period(5, 'Last'), + new Period(20), + new Period(60), + new Period(720), + new Period(1440), + new Period(10080), + new Period(20160), + new Period(40320), ] }; private maxPeriodTime = 60 * 24 * 2; // 2day @@ -116,6 +126,17 @@ export class ComponentDefaultSettingDataService { 'Mapped Buffer Memory', 'Data Source' ]; + private urlStatFieldNameList = [ + '0 ~ 100ms', + '100 ~ 300ms', + '300 ~ 500ms', + '500 ~ 1000ms', + '1000 ~ 3000ms', + '3000 ~ 5000ms', + '5000 ~ 8000ms', + '8000ms ~ ', + ]; + constructor() {} getInboundList(): number[] { return this.inboundList; @@ -168,4 +189,7 @@ export class ComponentDefaultSettingDataService { getAgentInspectorDefaultChartOrderList(): string[] { return this.agentChartOrderList; } + getUrlStatFieldNameList(): string[] { + return this.urlStatFieldNameList; + } } diff --git a/web/src/main/angular/src/app/shared/services/message-queue.service.ts b/web/src/main/angular/src/app/shared/services/message-queue.service.ts index 42340a809146..74cd89f6f985 100644 --- a/web/src/main/angular/src/app/shared/services/message-queue.service.ts +++ b/web/src/main/angular/src/app/shared/services/message-queue.service.ts @@ -43,7 +43,6 @@ export enum MESSAGE_TO { REAL_TIME_SCATTER_CHART_X_RANGE = 'REAL_TIME_SCATTER_CHART_X_RANGE', - INSPECTOR_PAGE_VALID = 'INSPECTOR_PAGE_VALID', INSPECTOR_CHART_MANAGER_ADD = 'INSPECTOR_CHART_MANAGER_ADD', INSPECTOR_CHART_MANAGER_REMOVE = 'INSPECTOR_CHART_MANAGER_REMOVE', INSPECTOR_CHART_MANAGER_CHANGE_ORDER = 'INSPECTOR_CHART_MANAGER_CHANGE_ORDER', @@ -64,4 +63,9 @@ export enum MESSAGE_TO { IS_ACCESS_DENYED = 'IS_ACCESS_DENYED', SET_CHART_LAYOUT = 'SET_CHART_LAYOUT', + + AGENT_LIST_VALID = 'AGENT_LIST_VALID', + FETCH_AGENT_LIST = 'FETCH_AGENT_LIST', + + SELECT_URL_INFO = 'SELECT_URL_INFO' } diff --git a/web/src/main/angular/src/app/shared/services/system-configuration-data.service.ts b/web/src/main/angular/src/app/shared/services/system-configuration-data.service.ts index dddf69131345..b45c971cc76d 100644 --- a/web/src/main/angular/src/app/shared/services/system-configuration-data.service.ts +++ b/web/src/main/angular/src/app/shared/services/system-configuration-data.service.ts @@ -43,6 +43,7 @@ export class SystemConfigurationDataService { userName: '', userDepartment: '', showSystemMetric: false, + showUrlStat: false }; constructor( diff --git a/web/src/main/angular/src/app/shared/services/web-app-setting-data.service.ts b/web/src/main/angular/src/app/shared/services/web-app-setting-data.service.ts index 16ee212fd8b3..2ea876deb848 100644 --- a/web/src/main/angular/src/app/shared/services/web-app-setting-data.service.ts +++ b/web/src/main/angular/src/app/shared/services/web-app-setting-data.service.ts @@ -80,6 +80,9 @@ export class WebAppSettingDataService { showMetric(): Observable { return this.newUrlStateNotificationService.getConfiguration('showSystemMetric'); } + showUrlStat(): Observable { + return this.newUrlStateNotificationService.getConfiguration('showUrlStat'); + } getImagePath(): string { return this.IMAGE_PATH; } @@ -302,4 +305,7 @@ export class WebAppSettingDataService { getExperimentalConfiguration(): Observable { return this.newUrlStateNotificationService.getConfiguration('experimental'); } + getUrlStatFieldNameList(): string[] { + return this.componentDefaultSettingDataService.getUrlStatFieldNameList(); + } } diff --git a/web/src/main/angular/src/assets/i18n/en.json b/web/src/main/angular/src/assets/i18n/en.json index 6a1ad83a3229..d73207d490cc 100644 --- a/web/src/main/angular/src/assets/i18n/en.json +++ b/web/src/main/angular/src/assets/i18n/en.json @@ -29,6 +29,9 @@ "SELECT_HOST_GROUP": "Select your host-group", "INPUT_HOST_GROUP_NAME_PLACE_HOLDER": "Input host-group name" }, + "URL_STAT": { + "SELECT_URL_INFO": "Select a URL info below" + }, "INSPECTOR": { "APPLICATION_INSPECTOR_USAGE_GUIDE_MESSAGE": "[TEXT]:{value=Application Inspector is not enabled.
To enable Application Inspector, please refer to}|[LINK]:{href=https://pinpoint-apm.gitbook.io/pinpoint/documents/application-inspector\\target=blank\\style=color:#428bca\\linkText=this link}", "APPLICAITION_NAME_ISSUE": { diff --git a/web/src/main/angular/src/assets/i18n/ko.json b/web/src/main/angular/src/assets/i18n/ko.json index b59a7d07d231..88f78f21b6cf 100644 --- a/web/src/main/angular/src/assets/i18n/ko.json +++ b/web/src/main/angular/src/assets/i18n/ko.json @@ -29,6 +29,9 @@ "SELECT_HOST_GROUP": "호스트 그룹을 선택하세요.", "INPUT_HOST_GROUP_NAME_PLACE_HOLDER": "호스트 그룹 이름을 입력하세요." }, + "URL_STAT": { + "SELECT_URL_INFO": "아래에서 URL 정보를 선택하세요." + }, "INSPECTOR": { "APPLICATION_INSPECTOR_USAGE_GUIDE_MESSAGE": "[TEXT]:{value=Application Inspector 기능이 활성화 되어 있지 않습니다.
Application Inspector 기능을 사용하려면,}|[LINK]:{href=https://pinpoint-apm.gitbook.io/pinpoint/documents/application-inspector\\target=_blank\\style=color:#428bca\\linkText=링크}|[TEXT]:{value=를 참고하세요.}", "APPLICAITION_NAME_ISSUE": { diff --git a/web/src/main/angular/src/globals.d.ts b/web/src/main/angular/src/globals.d.ts index da73e3f0bcbc..41083a62923b 100644 --- a/web/src/main/angular/src/globals.d.ts +++ b/web/src/main/angular/src/globals.d.ts @@ -412,6 +412,7 @@ interface ISystemConfiguration { userName?: string; userDepartment?: string; showSystemMetric: boolean; + showUrlStat: boolean; } interface IFormFieldErrorType { @@ -457,15 +458,26 @@ interface IMetricData { unit: string; } -// interface IMetricData { -// title: string; -// timestamp: number[]; -// metricValues: IMetricValue[]; -// } - interface IMetricValue { fieldName: string; // tagList: {name: string, value: string}[]; tags?: any[]; // TODO: Check format values: number[]; } + +interface IUrlStatInfoData { + uri: string; + totalCount: number; + failureCount: number; + maxTimeMs: number; + avgTimeMs: number; +} + +interface IUrlStatChartData { + title: string; + timestamp: number[]; + metricValueGroups: { + groupName: string; + metricValues: IMetricValue[] + }[]; +} \ No newline at end of file diff --git a/web/src/main/angular/src/main.css b/web/src/main/angular/src/main.css index c9ee64edb801..2c3f4abda0be 100644 --- a/web/src/main/angular/src/main.css +++ b/web/src/main/angular/src/main.css @@ -22,7 +22,16 @@ body { --emerald-green-800: #006843; --emerald-green-900: #004c29; /* green */ - --green-default: #41c464; + --green-50: #e5f7e9; + --green-100: #c1eac8; + --green-200: #98dca5; + --green-300: #6acf80; + --green-400: #41c464; + --green-500: #00b947; + --green-600: #00a93d; + --green-700: #009731; + --green-800: #008626; + --green-900: #006612; /* yellow */ --yellow-50: #ffffe6; --yellow-100: #fcfdc0; @@ -120,7 +129,17 @@ body.dark-mode { --emerald-green-800: #acdfd0; --emerald-green-900: #ddf2ed; /* green */ - --green-default: #41c464; + --green-50: #006612; + --green-100: #008626; + --green-200: #009731; + --green-300: #00a93d; + --green-400: #00b947; + --green-500: #41c464; + --green-600: #6acf80; + --green-700: #98dca5; + --green-800: #c1eac8; + --green-900: #e5f7e9; + /* yellow */ --yellow-50: #ff8a00; --yellow-100: #ffb700; @@ -199,7 +218,7 @@ body { --secondary: var(--emerald-green-300); --secondary-lighter: var(--emerald-green-100); - --status-success: var(--green-default); + --status-success: var(--green-400); --status-good: var(--blue-500); --status-warn: var(--orange-600); --status-fail: var(--red-400); @@ -295,7 +314,7 @@ body.dark-mode { --secondary: var(--emerald-green-600); --secondary-lighter: var(--emerald-green-100); - --status-success: var(--green-default); + --status-success: var(--green-400); --status-good: var(--blue-500); --status-warn: var(--orange-600); --status-fail: var(--red-400); diff --git a/web/src/main/angular/src/proxy.conf.js b/web/src/main/angular/src/proxy.conf.js index e3e0022d52a7..06003a5cf5a1 100644 --- a/web/src/main/angular/src/proxy.conf.js +++ b/web/src/main/angular/src/proxy.conf.js @@ -54,7 +54,6 @@ const PROXY_CONFIG = [ "/admin/removeAgentId.pinpoint", "/admin/removeInactiveAgents.pinpoint", "/bind.pinpoint", - "/getAgentStat/uriStat/chartList.pinpoint", "/heatmap/drag.pinpoint", "/traceViewerData.pinpoint", "/application/webhook.pinpoint", @@ -68,7 +67,9 @@ const PROXY_CONFIG = [ "/systemMetric/hostGroup/host/collectedMetricData.pinpoint", "/getApdexScore.pinpoint", "/getApplicationStat/apdexScore/chart.pinpoint", - "/getAgentStat/apdexScore/chart.pinpoint" + "/getAgentStat/apdexScore/chart.pinpoint", + "/uriStat/top50.pinpoint", + "/uriStat/chart.pinpoint" ], target: 'http://localhost:8080', secure: false diff --git a/web/src/main/angular/src/styles.css b/web/src/main/angular/src/styles.css index 06fbf485174c..202a09ffdc94 100644 --- a/web/src/main/angular/src/styles.css +++ b/web/src/main/angular/src/styles.css @@ -230,14 +230,11 @@ caption { } } -.l-agent-inspector-contents .bb-axis-y-label, -.l-agent-inspector-contents .bb-axis-y2-label, -.l-application-inspector-contents .bb-axis-y-label, -.l-transaction-view-top-contents .bb-axis-y-label, -.l-transaction-view-top-contents .bb-axis-y2-label { +.bb-axis-y-label, +.bb-axis-y2-label { text-anchor: middle !important; font-family: 'Open Sans'; - font-size: 0.75vw; + font-size: 13px; fill: var(--text-primary); font-weight: bold; } @@ -409,7 +406,8 @@ caption { opacity: 1 !important; } -.l-load-chart .bb-line { +.l-load-chart .bb-line, +.l-url-statistic-chart .bb-line { stroke-width: 0; }