From 740713351c909f6667d047a4487bab80a2935ac5 Mon Sep 17 00:00:00 2001 From: QooQooDass Date: Fri, 16 Dec 2022 16:15:40 +0900 Subject: [PATCH] [#9518] Add sorting UI for agent-list in inspector page --- ...ver-and-agent-list-container.component.css | 83 +++++++++++++- ...er-and-agent-list-container.component.html | 9 ++ ...rver-and-agent-list-container.component.ts | 106 ++++++++++++++---- .../server-and-agent-list-data.service.ts | 25 ++++- .../server-and-agent-list.component.css | 2 +- .../server-and-agent-list.component.html | 14 +-- .../server-and-agent-list.component.ts | 16 ++- .../services/web-app-setting-data.service.ts | 7 ++ 8 files changed, 227 insertions(+), 35 deletions(-) diff --git a/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-container.component.css b/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-container.component.css index f09a3ba168b9..470abcc3626f 100644 --- a/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-container.component.css +++ b/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-container.component.css @@ -1,6 +1,6 @@ :host { display: block; - height: calc(100% - 41px); /* 41px: title height */ + height: calc(100% - 40px); /* 40px: title height */ } .guide-text { @@ -21,6 +21,55 @@ gap: 7px; } +.sort-option-wrapper { + display: flex; + align-items: center;; + width: 100%; + height: 32px; + padding:0px 15px 8px; + border-bottom: 1px solid var(--border-primary); + gap: 2px; +} + +.sort-label { + font-size: 12px; + color: var(--text-secondary); +} + +.sort-option-list-wrapper { + flex: 1; + display: flex; + /* gap: 5px; */ +} + +.sort-option { + font-size: 12px; + color: var(--text-secondary); + padding: 3px 6px; + border-radius: 3px; + cursor: pointer; +} + +.sort-option:hover { + /* background: var(--background-hover-default); */ + /* font-weight: bold; */ + color: var(--primary); +} + +.sort-option.active { + font-weight: bold; + color: var(--primary); +} + +.display-option-wrapper { + display: flex; + /* flex-direction: column; */ + gap: 5px; + width: 100%; + padding:0px 17px 8px; + border-bottom: 1px solid var(--border-primary); +} + .search-input-wrapper { flex: 1; height: 100%; @@ -56,7 +105,37 @@ font-size: 12px; } -.fa-question-circle { +.fas { color: var(--icon-default); font-size: 18px; } + +.display-option-select-wrapper, +.sort-option-select-wrapper { + display: flex; + align-items: center; + gap: 3px; + padding: 2px 0; +} + +.option-label { + font-size: 12px; + color: var(--text-secondary); +} + +.option-label .fas { + font-size: 12px; + color: var(--text-secondary); +} + +.option-select { + cursor: pointer; + /* padding: 6px 0; */ + outline: 0; + font-size: 11px; + font-weight: bold; + color: var(--text-primary-lightest); + appearance: none; + display: inline-block; + height: 100%; +} diff --git a/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-container.component.html b/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-container.component.html index 62ceb572c2d2..fb3c7a8cc6fd 100644 --- a/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-container.component.html +++ b/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-container.component.html @@ -10,6 +10,14 @@ +
+
Sort by:
+
+ +
{{sortOption.display}}
+
+
+

{{errorMessage ? errorMessage : emptyText$ | async}}

@@ -20,6 +28,7 @@ [serverKeyList]="filteredServerKeyList" [serverList]="filteredServerList" [agentId]="agentId" + [selectedSortOptionKey]="selectedSortOptionKey" (outSelectAgent)="onSelectAgent($event)">
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 70dba88b71f1..a88361774ab1 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 @@ -20,6 +20,11 @@ import { ServerAndAgentListDataService } from './server-and-agent-list-data.serv import { isEmpty, isThatType } from 'app/core/utils/util'; import { HELP_VIEWER_LIST, HelpViewerPopupContainerComponent } from 'app/core/components/help-viewer-popup/help-viewer-popup-container.component'; +export const enum SortOption { + ID = 'id', + NAME = 'name', + RECENT = 'recent' +} @Component({ selector: 'pp-server-and-agent-list-container', templateUrl: './server-and-agent-list-container.component.html', @@ -28,10 +33,12 @@ import { HELP_VIEWER_LIST, HelpViewerPopupContainerComponent } from 'app/core/co export class ServerAndAgentListContainerComponent implements OnInit, OnDestroy { private unsubscribe = new Subject(); private _query = ''; + private previousParams: {[key: string]: any}; + private cachedData: {[key in SortOption]: IServerAndAgentDataV2[]}; agentId: string; - serverList: {[key: string]: IServerAndAgentData[]}; - filteredServerList: {[key: string]: IServerAndAgentData[]} = {}; + serverList: IServerAndAgentDataV2[]; + filteredServerList: IServerAndAgentDataV2[] = []; filteredServerKeyList: string[] = []; funcImagePath: Function; isEmpty: boolean; @@ -40,6 +47,12 @@ export class ServerAndAgentListContainerComponent implements OnInit, OnDestroy { inputPlaceholder$: Observable; searchUseEnter = false; SEARCH_MIN_LENGTH = 2; + sortOptionList = [ + {display: 'ID', key: SortOption.ID}, + {display: 'Name', key: SortOption.NAME}, + {display: 'Recent', key: SortOption.RECENT} + ]; + selectedSortOptionKey: SortOption; constructor( private newUrlStateNotificationService: NewUrlStateNotificationService, @@ -58,6 +71,7 @@ export class ServerAndAgentListContainerComponent implements OnInit, OnDestroy { ngOnInit() { this.initI18nText(); this.funcImagePath = this.webAppSettingDataService.getImagePathMakeFunc(); + this.selectedSortOptionKey = this.webAppSettingDataService.getAgentListSortOption() as SortOption || SortOption.ID; merge( this.newUrlStateNotificationService.onUrlStateChange$.pipe( @@ -92,11 +106,23 @@ export class ServerAndAgentListContainerComponent implements OnInit, OnDestroy { ) ).pipe( concatMap((range: number[]) => { - const appName = (this.newUrlStateNotificationService.getPathValue(UrlPathId.APPLICATION) as IApplication).getApplicationName(); + const app = (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) => { + return this.serverAndAgentListDataService.getData(app, range, this.selectedSortOptionKey).pipe( + tap(() => { + this.previousParams = {app, range}; + }), + tap(() => { + const urlService = this.newUrlStateNotificationService; + const isAppChanged = urlService.isValueChanged(UrlPathId.APPLICATION); + const isPeriodChanged = urlService.isValueChanged(UrlPathId.PERIOD) || urlService.isValueChanged(UrlPathId.END_TIME); + + if (isAppChanged || isPeriodChanged) { + this.cachedData = {} as {[key in SortOption]: IServerAndAgentDataV2[]}; + } + }), + filter((res: IServerAndAgentDataV2[] | IServerErrorShortFormat) => { // TODO: 민우님께 에러구분 여쭤보기. 401이면 AuthService 활용한다? 근데이럼 IS_ACCESS_DENYED 출처 불분명같은 문제가 있지않을까.. if (isThatType(res, 'errorCode', 'errorMessage')) { this.errorMessage = res.errorMessage; @@ -108,10 +134,10 @@ export class ServerAndAgentListContainerComponent implements OnInit, OnDestroy { return true; } }), - filter((res: {[key: string]: IServerAndAgentData[]}) => { + filter((res: IServerAndAgentDataV2[]) => { if (this.agentId) { - const filteredList = this.filterServerList(res, this.agentId, ({agentId}: IServerAndAgentData) => this.agentId.toLowerCase() === agentId.toLowerCase()); - const isAgentIdValid = Object.keys(filteredList).length !== 0; + const filteredList = this.filterServerList(res, this.agentId, ({agentId}: IAgentDataV2) => this.agentId.toLowerCase() === agentId.toLowerCase()); + const isAgentIdValid = !isEmpty(filteredList); if (isAgentIdValid) { return true; @@ -163,10 +189,10 @@ export class ServerAndAgentListContainerComponent implements OnInit, OnDestroy { }) ); }), - ).subscribe((data: {[key: string]: IServerAndAgentData[]}) => { - this.serverList = data; - this.filteredServerList = this.filterServerList(data, this.query); - this.filteredServerKeyList = Object.keys(this.filteredServerList).sort(); + ).subscribe((data: IServerAndAgentDataV2[]) => { + this.serverList = this.cachedData[this.selectedSortOptionKey] = data; + this.filteredServerList = this.filterServerList(this.serverList, this.query); + this.filteredServerKeyList = this.filteredServerList.map(({groupName}: IServerAndAgentDataV2) => groupName).sort(); this.isEmpty = isEmpty(this.filteredServerList); }); } @@ -203,17 +229,17 @@ export class ServerAndAgentListContainerComponent implements OnInit, OnDestroy { 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[]} { - const filterCallback = predi ? predi : ({agentId, agentName}: IServerAndAgentData) => { + private filterServerList(serverList: IServerAndAgentDataV2[], query: string, predi?: (data: IAgentDataV2) => boolean): IServerAndAgentDataV2[] { + const filterCallback = predi ? predi : ({agentId, agentName}: IAgentDataV2) => { return agentId.toLowerCase().includes(query.toLowerCase()) || (agentName && agentName.toLowerCase().includes(query.toLowerCase())); }; return query === '' ? serverList - : Object.entries(serverList).reduce((acc: {[key: string]: IServerAndAgentData[]}, [key, serverAndAgentDataList]: [string, IServerAndAgentData[]]) => { - const matchedList = serverAndAgentDataList.filter(filterCallback); - - return isEmpty(matchedList) ? acc : {...acc, [key]: matchedList}; - }, {} as {[key: string]: IServerAndAgentData[]}); + : serverList.reduce((acc: IServerAndAgentDataV2[], {groupName, instancesList}: IServerAndAgentDataV2) => { + const matchedList = instancesList.filter(filterCallback); + + return isEmpty(matchedList) ? acc : [...acc, {groupName, instancesList: matchedList}] + }, []); } private set query(query: string) { @@ -255,4 +281,46 @@ export class ServerAndAgentListContainerComponent implements OnInit, OnDestroy { injector: this.injector }); } + + isActiveSortOption(optionKey: SortOption): boolean { + return optionKey === this.selectedSortOptionKey; + } + + onSelectSortOption(optionKey: SortOption): void { + if (optionKey === this.selectedSortOptionKey) { + return; + } + + const {app, range} = this.previousParams; + + if (this.cachedData[optionKey]) { + this.filteredServerList = this.filterServerList(this.cachedData[optionKey], this.query); + this.filteredServerKeyList = this.filteredServerList.map(({groupName}: IServerAndAgentDataV2) => groupName).sort(); + } else { + this.serverAndAgentListDataService.getData(app, range, optionKey).pipe( + catchError((error: IServerError) => { + this.dynamicPopupService.openPopup({ + data: { + title: 'Error', + contents: error + }, + component: ServerErrorPopupContainerComponent + }, { + resolver: this.componentFactoryResolver, + injector: this.injector + }); + + return EMPTY; + }) + ).subscribe((data: IServerAndAgentDataV2[]) => { + this.serverList = this.cachedData[optionKey] = data; + this.filteredServerList = this.filterServerList(this.serverList, this.query); + this.filteredServerKeyList = this.filteredServerList.map(({groupName}: IServerAndAgentDataV2) => groupName).sort(); + }); + } + + this.isEmpty = isEmpty(this.filteredServerList); + this.selectedSortOptionKey = optionKey; + this.webAppSettingDataService.setAgentListSortOption(optionKey); + } } diff --git a/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-data.service.ts b/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-data.service.ts index 6895e10a15e5..aaac26c4ae34 100644 --- a/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-data.service.ts +++ b/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list-data.service.ts @@ -2,21 +2,36 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; +import { SortOption } from './server-and-agent-list-container.component'; + + +const enum SortOptionParamKey { + ID = 'AGENT_ID_ASC', + NAME = 'AGENT_NAME_ASC', + RECENT = 'RECENT' +} @Injectable() export class ServerAndAgentListDataService { - private url = 'getAgentList.pinpoint'; + private url = 'agents/search-application.pinpoint'; constructor( private http: HttpClient, ) {} - getData(applicationName: string, range: number[]): Observable<{[key: string]: IServerAndAgentData[]}> { - return this.http.get<{[key: string]: IServerAndAgentData[]}>(this.url, this.makeRequestOptionsArgs(applicationName, range)); + getData(applicationName: string, range: number[], sortOption: SortOption): Observable { + return this.http.get(this.url, this.makeRequestOptionsArgs(applicationName, range, sortOption)); } - private makeRequestOptionsArgs(application: string, [from, to]: number[]): object { + private makeRequestOptionsArgs(application: string, [from, to]: number[], sortOption: SortOption): object { return { - params: { application, from, to } + params: { + application, + from, + to, + sortBy: sortOption === SortOption.ID ? SortOptionParamKey.ID + : sortOption === SortOption.NAME ? SortOptionParamKey.NAME + : SortOptionParamKey.RECENT + } }; } } diff --git a/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list.component.css b/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list.component.css index f6049116b904..11ba02e218f8 100644 --- a/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list.component.css +++ b/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list.component.css @@ -1,7 +1,7 @@ :host { display: block; /* height: 100%; */ - height: calc(100% - 48px); + height: calc(100% - 48px - 32px); /* search area(48px) + sort area(32px) */ overflow-y: auto; font-size: 13px; } diff --git a/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list.component.html b/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list.component.html index ba41f0949864..31e3469198f8 100644 --- a/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list.component.html +++ b/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list.component.html @@ -1,16 +1,16 @@
    -
  • -
    +
  • +
    - {{serverName}} + {{server.groupName}} - +
    -
      -
    • +
        +
      • - {{agent.agentId}} + {{getAgentLabel(agent)}}
        diff --git a/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list.component.ts b/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list.component.ts index e9f9fc2252af..803c69b165d5 100644 --- a/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list.component.ts +++ b/web/src/main/angular/src/app/core/components/server-and-agent-list/server-and-agent-list.component.ts @@ -1,5 +1,7 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { trigger, state, style, animate, transition } from '@angular/animations'; + +import { SortOption } from './server-and-agent-list-container.component'; @Component({ selector: 'pp-server-and-agent-list', templateUrl: './server-and-agent-list.component.html', @@ -46,8 +48,9 @@ export class ServerAndAgentListComponent implements OnInit { return this._serverKeyList; } - @Input() serverList: {[key: string]: IServerAndAgentData[]}; + @Input() serverList: IServerAndAgentDataV2[]; @Input() agentId: string; + @Input() selectedSortOptionKey: SortOption @Output() outSelectAgent = new EventEmitter(); private _serverKeyList: string[]; @@ -91,4 +94,15 @@ export class ServerAndAgentListComponent implements OnInit { return el.classList.contains('active'); }); } + + getAgentLabel({agentId, agentName}: IServerAndAgentData): string { + switch (this.selectedSortOptionKey) { + case SortOption.ID: + case SortOption.RECENT: + default: + return agentName ? `${agentId} (${agentName})` : `${agentId} (N/A)`; + case SortOption.NAME: + return agentName ? agentName : `N/A (${agentId})`; + } + } } 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 2ea876deb848..7441914edf6c 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 @@ -35,6 +35,7 @@ export class WebAppSettingDataService { LANGUAGE: 'language', THEME: 'theme', SIDE_NAV_BAR_SCALE: 'sideNavigationBarScale', + AGENT_LIST_SORT_OPTION: 'agentListSortOption' }; private IMAGE_PATH = './assets/img/'; private IMAGE_EXT = '.png'; @@ -308,4 +309,10 @@ export class WebAppSettingDataService { getUrlStatFieldNameList(): string[] { return this.componentDefaultSettingDataService.getUrlStatFieldNameList(); } + setAgentListSortOption(sortOption: string): void { + this.localStorageService.set(WebAppSettingDataService.KEYS.AGENT_LIST_SORT_OPTION, sortOption); + } + getAgentListSortOption(): string { + return this.localStorageService.get(WebAppSettingDataService.KEYS.AGENT_LIST_SORT_OPTION); + } }