From ffdd3930100f2e7d5aca8e71e83a29ae0905bcc8 Mon Sep 17 00:00:00 2001 From: benzuzu Date: Wed, 2 Aug 2023 01:38:21 -0400 Subject: [PATCH] Filter by date in users tab --- .../web/rest/UsageAnalysisController.java | 48 ++- .../vm/usageAnalysis/UserOverviewUsage.java | 40 ++- .../ResourceUsageDetailsTable.tsx | 9 +- .../usageAnalysisPage/UsageAnalysisPage.tsx | 115 +------ .../usageAnalysisPage/UsageAnalysisTable.tsx | 319 ++++++++++++++++++ .../UserUsageDetailsTable.tsx | 8 +- .../webapp/app/shared/api/generated/API.ts | 12 +- 7 files changed, 417 insertions(+), 134 deletions(-) create mode 100644 src/main/webapp/app/pages/usageAnalysisPage/UsageAnalysisTable.tsx diff --git a/src/main/java/org/mskcc/cbio/oncokb/web/rest/UsageAnalysisController.java b/src/main/java/org/mskcc/cbio/oncokb/web/rest/UsageAnalysisController.java index 35c230b8d..23308fcce 100644 --- a/src/main/java/org/mskcc/cbio/oncokb/web/rest/UsageAnalysisController.java +++ b/src/main/java/org/mskcc/cbio/oncokb/web/rest/UsageAnalysisController.java @@ -139,11 +139,22 @@ public ResponseEntity> userOverviewUsageGet(@RequestPara HttpStatus status = HttpStatus.OK; int year = TimeUtil.getCurrentNYTime().getYear(); - JSONObject jsonObject = requestData(YEAR_USERS_USAGE_SUMMARY_FILE_PREFIX + year + FileExtension.JSON_FILE.getExtension()); + JSONObject yearSummary = requestData(YEAR_USERS_USAGE_SUMMARY_FILE_PREFIX + year + FileExtension.JSON_FILE.getExtension()); + Map monthSummaries = new HashMap<>(); + int monthsBack = 0; + JSONObject monthSummary; + do { + String month = TimeUtil.getCurrentNYTime().minus(monthsBack, ChronoUnit.MONTHS).format(DateTimeFormatter.ofPattern("yyyy-MM")); + monthSummary = requestData(MONTH_USERS_USAGE_SUMMARY_FILE_PREFIX + month + FileExtension.JSON_FILE.getExtension()); + if (monthSummary != null) { + monthSummaries.put(month, monthSummary); + } + monthsBack++; + } while (monthsBack < 12); List result = new ArrayList<>(); - if (jsonObject != null) { - Set emailSet = jsonObject.keySet(); + if (yearSummary != null) { + Set emailSet = yearSummary.keySet(); if (companyId != null) { emailSet = emailSet.stream().filter(item -> { Optional user = userService.getUserWithAuthoritiesByEmailIgnoreCase((String) item); @@ -158,7 +169,7 @@ public ResponseEntity> userOverviewUsageGet(@RequestPara } for (Object item : emailSet) { String email = (String) item; - JSONObject usageObject = (JSONObject) jsonObject.get(email); + JSONObject usageObject = (JSONObject) yearSummary.get(email); Gson gson = new Gson(); UsageSummary usageSummary = gson.fromJson(usageObject.toString(), UsageSummary.class); UserOverviewUsage cur = new UserOverviewUsage(); @@ -187,9 +198,34 @@ public ResponseEntity> userOverviewUsageGet(@RequestPara } cur.setTotalUsage(totalUsage); cur.setEndpoint(endpoint); - cur.setMaxUsage(maxUsage); + cur.setMaxUsageProportion((int) (1000 * ((float) maxUsage / totalUsage)) / 10f); cur.setNoPrivateEndpoint(noPrivateEndpoint); - cur.setNoPrivateMaxUsage(noPrivateMaxUsage); + cur.setNoPrivateMaxUsageProportion((int) (1000 * ((float) noPrivateMaxUsage / totalUsage)) / 10f); + + Map dayUsage = new HashMap<>(); + Map monthUsage = new HashMap<>(); + if (!monthSummaries.isEmpty()) { + for (Map.Entry entry : monthSummaries.entrySet()) { + if (entry.getValue().containsKey(email)) { + JSONObject monthUsageObject = (JSONObject) entry.getValue().get(email); + long monthCount = 0; + JSONObject dayUsageObject = (JSONObject) monthUsageObject.get("day"); + for (Object dayKey : dayUsageObject.keySet()) { + String day = dayKey.toString(); + JSONObject curDayUsage = (JSONObject) dayUsageObject.get(day); + long dayCount = 0; + for (Object resource : curDayUsage.keySet()) { + dayCount += (long) curDayUsage.get(resource); + monthCount += (long) curDayUsage.get(resource); + } + dayUsage.put(day, dayCount); + } + monthUsage.put(entry.getKey(), monthCount); + } + } + } + cur.setDayUsage(dayUsage); + cur.setMonthUsage(monthUsage); result.add(cur); } diff --git a/src/main/java/org/mskcc/cbio/oncokb/web/rest/vm/usageAnalysis/UserOverviewUsage.java b/src/main/java/org/mskcc/cbio/oncokb/web/rest/vm/usageAnalysis/UserOverviewUsage.java index bf15018ed..3696b48a4 100644 --- a/src/main/java/org/mskcc/cbio/oncokb/web/rest/vm/usageAnalysis/UserOverviewUsage.java +++ b/src/main/java/org/mskcc/cbio/oncokb/web/rest/vm/usageAnalysis/UserOverviewUsage.java @@ -1,5 +1,7 @@ package org.mskcc.cbio.oncokb.web.rest.vm.usageAnalysis; +import java.util.Map; + /** * Created by Yifu Yao on 2020-11-2 */ @@ -9,9 +11,11 @@ public class UserOverviewUsage { private String userEmail; private String endpoint; private String noPrivateEndpoint; - private long maxUsage; - private long noPrivateMaxUsage; + private float maxUsageProportion; + private float noPrivateMaxUsageProportion; private long totalUsage; + private Map dayUsage; + private Map monthUsage; public String getEndpoint() { return endpoint; @@ -29,12 +33,12 @@ public void setUserEmail(String userEmail) { this.userEmail = userEmail; } - public long getMaxUsage() { - return maxUsage; + public float getMaxUsageProportion() { + return maxUsageProportion; } - public void setMaxUsage(long maxUsage) { - this.maxUsage = maxUsage; + public void setMaxUsageProportion(float maxUsageProportion) { + this.maxUsageProportion = maxUsageProportion; } public long getTotalUsage() { @@ -61,12 +65,28 @@ public void setNoPrivateEndpoint(String noPrivateEndpoint) { this.noPrivateEndpoint = noPrivateEndpoint; } - public long getNoPrivateMaxUsage() { - return noPrivateMaxUsage; + public float getNoPrivateMaxUsageProportion() { + return noPrivateMaxUsageProportion; + } + + public void setNoPrivateMaxUsageProportion(float noPrivateMaxUsageProportion) { + this.noPrivateMaxUsageProportion = noPrivateMaxUsageProportion; + } + + public Map getDayUsage() { + return dayUsage; + } + + public void setDayUsage(Map dayUsage) { + this.dayUsage = dayUsage; + } + + public Map getMonthUsage() { + return monthUsage; } - public void setNoPrivateMaxUsage(long noPrivateMaxUsage) { - this.noPrivateMaxUsage = noPrivateMaxUsage; + public void setMonthUsage(Map monthUsage) { + this.monthUsage = monthUsage; } } diff --git a/src/main/webapp/app/pages/usageAnalysisPage/ResourceUsageDetailsTable.tsx b/src/main/webapp/app/pages/usageAnalysisPage/ResourceUsageDetailsTable.tsx index 3fed22e08..f9977e956 100644 --- a/src/main/webapp/app/pages/usageAnalysisPage/ResourceUsageDetailsTable.tsx +++ b/src/main/webapp/app/pages/usageAnalysisPage/ResourceUsageDetailsTable.tsx @@ -1,7 +1,6 @@ import OncoKBTable from 'app/components/oncokbTable/OncoKBTable'; import { filterByKeyword } from 'app/shared/utils/Utils'; import autobind from 'autobind-decorator'; -import _ from 'lodash'; import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; @@ -55,8 +54,8 @@ export default class ResourceUsageDetailsTable extends React.Component< { ...getUsageTableColumnDefinition(UsageTableColumnKey.RESOURCES), Header: emailHeader, - onFilter: (data: UsageRecord, keyword) => - filterByKeyword(data.resource, keyword), + onFilter: (row: UsageRecord, keyword) => + filterByKeyword(row.resource, keyword), }, { ...getUsageTableColumnDefinition(UsageTableColumnKey.USAGE), @@ -67,8 +66,8 @@ export default class ResourceUsageDetailsTable extends React.Component< { ...getUsageTableColumnDefinition(UsageTableColumnKey.TIME), Header: filterDependentTimeHeader(this.timeTypeToggleValue), - onFilter: (data: UsageRecord, keyword) => - filterByKeyword(data.time, keyword), + onFilter: (row: UsageRecord, keyword) => + filterByKeyword(row.time, keyword), }, ]} loading={this.props.loadedData ? false : true} diff --git a/src/main/webapp/app/pages/usageAnalysisPage/UsageAnalysisPage.tsx b/src/main/webapp/app/pages/usageAnalysisPage/UsageAnalysisPage.tsx index 1442e4191..e46bddcc1 100644 --- a/src/main/webapp/app/pages/usageAnalysisPage/UsageAnalysisPage.tsx +++ b/src/main/webapp/app/pages/usageAnalysisPage/UsageAnalysisPage.tsx @@ -24,7 +24,6 @@ import autobind from 'autobind-decorator'; import { Row, Dropdown, DropdownButton } from 'react-bootstrap'; import { PAGE_ROUTE, - USAGE_TOP_USERS_LIMIT, USAGE_ALL_TIME_KEY, } from 'app/config/constants'; import { remoteData } from 'cbioportal-frontend-commons'; @@ -32,9 +31,6 @@ import * as QueryString from 'query-string'; import { UsageToggleGroup } from './UsageToggleGroup'; import { TableCellRenderer } from 'react-table'; import { - emailHeader, - endpointHeader, - noPrivateEndpointHeader, operationHeader, resourceHeader, timeHeader, @@ -42,6 +38,7 @@ import { filterDependentResourceHeader, } from 'app/components/oncokbTable/HeaderConstants'; import UsageText from 'app/shared/texts/UsageText'; +import UsageAnalysisTable from "app/pages/usageAnalysisPage/UsageAnalysisTable"; export type UsageRecord = { resource: string; @@ -56,7 +53,6 @@ enum UsageType { export enum ToggleValue { ALL_USERS = 'All Users', - TOP_USERS = 'Top Users', ALL_RESOURCES = 'All Resources', PUBLIC_RESOURCES = 'Only Public Resources', CUMULATIVE_USAGE = 'Cumulative Usage', @@ -257,102 +253,11 @@ export default class UsageAnalysisPage extends React.Component<{ >
- = USAGE_TOP_USERS_LIMIT; - }) - } - columns={[ - { - id: 'userEmail', - Header: emailHeader, - accessor: 'userEmail', - minWidth: 200, - onFilter: (data: UserOverviewUsage, keyword) => - filterByKeyword(data.userEmail, keyword), - }, - { - id: 'totalUsage', - Header: usageHeader, - minWidth: 100, - accessor: 'totalUsage', - Cell(props: { original: UserOverviewUsage }) { - return ; - }, - }, - this.userTabResourcesTypeToggleValue === - ToggleValue.ALL_RESOURCES - ? { - id: 'endpoint', - Header: endpointHeader, - minWidth: 200, - accessor: 'endpoint', - onFilter: (data: UserOverviewUsage, keyword) => - filterByKeyword(data.endpoint, keyword), - } - : { - id: 'noPrivateEndpoint', - Header: noPrivateEndpointHeader, - minWidth: 200, - accessor: 'noPrivateEndpoint', - onFilter: (data: UserOverviewUsage, keyword) => - filterByKeyword(data.noPrivateEndpoint, keyword), - }, - { - ...getUsageTableColumnDefinition( - UsageTableColumnKey.OPERATION - ), - sortable: false, - className: 'd-flex justify-content-center', - Cell(props: { original: UserOverviewUsage }) { - return ( - props.original.userId && ( - - - - ) - ); - }, - }, - ]} - loading={this.users.isPending} - defaultSorted={[ - { - id: 'totalUsage', - desc: true, - }, - ]} - showPagination={true} - minRows={1} - filters={() => { - return ( - - - - - ); - }} +
@@ -368,8 +273,8 @@ export default class UsageAnalysisPage extends React.Component<{ Header: filterDependentResourceHeader( this.resourceTabResourcesTypeToggleValue ), - onFilter: (data: UsageRecord, keyword) => - filterByKeyword(data.resource, keyword), + onFilter: (row: UsageRecord, keyword) => + filterByKeyword(row.resource, keyword), }, { ...getUsageTableColumnDefinition(UsageTableColumnKey.USAGE), @@ -392,13 +297,13 @@ export default class UsageAnalysisPage extends React.Component<{ props.original.resource )}`} > - + ); }, }, ]} - loading={this.usageDetail.isComplete ? false : true} + loading={!this.usageDetail.isComplete} defaultSorted={[ { id: UsageTableColumnKey.USAGE, diff --git a/src/main/webapp/app/pages/usageAnalysisPage/UsageAnalysisTable.tsx b/src/main/webapp/app/pages/usageAnalysisPage/UsageAnalysisTable.tsx new file mode 100644 index 000000000..6b255538b --- /dev/null +++ b/src/main/webapp/app/pages/usageAnalysisPage/UsageAnalysisTable.tsx @@ -0,0 +1,319 @@ +import OncoKBTable from 'app/components/oncokbTable/OncoKBTable'; +import {filterByKeyword} from 'app/shared/utils/Utils'; +import autobind from 'autobind-decorator'; +import {action, computed, observable} from 'mobx'; +import {observer} from 'mobx-react'; +import React from 'react'; +import {Row} from 'react-bootstrap'; +import { + getUsageTableColumnDefinition, + ToggleValue, + UsageRecord, + UsageTableColumnKey, +} from 'app/pages/usageAnalysisPage/UsageAnalysisPage'; +import {UsageToggleGroup} from './UsageToggleGroup'; +import {UsageAnalysisCalendarButton} from 'app/components/calendarButton/UsageAnalysisCalendarButton'; +import { + emailHeader, + endpointHeader, + filterDependentTimeHeader, + noPrivateEndpointHeader, + usageHeader, +} from 'app/components/oncokbTable/HeaderConstants'; +import {UserOverviewUsage} from "app/shared/api/generated/API"; +import UsageText from "app/shared/texts/UsageText"; +import {PAGE_ROUTE, TABLE_DAY_FORMAT, TABLE_MONTH_FORMAT} from "app/config/constants"; +import moment from "moment"; +import _ from "lodash"; +import {Link} from "react-router-dom"; +import {SortingRule} from "react-table"; + +type IUserUsageDetailsTable = { + data: UserOverviewUsage[]; + loadedData: boolean; + defaultResourcesType: ToggleValue; + defaultTimeType: ToggleValue; + defaultPageSize?: number; +}; + +@observer +export default class UsageAnalysisTable extends React.Component< + IUserUsageDetailsTable, + {} + > { + @observable resourcesTypeToggleValue: ToggleValue = this.props + .defaultResourcesType; + @observable timeTypeToggleValue: ToggleValue = this.props.defaultTimeType; + @observable fromDate: string | undefined; + @observable toDate: string | undefined; + @observable filterToggled: boolean; + @observable dropdownMenuOpen: boolean; + + @autobind + @action + handleResourcesTypeToggleChange(value: ToggleValue) { + this.resourcesTypeToggleValue = value; + } + + @autobind + @action + handleTimeTypeToggleChange(value: ToggleValue) { + this.timeTypeToggleValue = value; + } + + @computed get calculateData(): UserOverviewUsage[] | UsageRecord[] { + if (this.timeTypeToggleValue === ToggleValue.RESULTS_IN_TOTAL) { + return this.props.data; + } + + let data: UsageRecord[] = []; + if (this.timeTypeToggleValue === ToggleValue.RESULTS_BY_MONTH) { + this.props.data.forEach((userOverviewUsage: UserOverviewUsage) => { + for (const curMonth in userOverviewUsage.monthUsage) { + if (Object.prototype.hasOwnProperty.call(userOverviewUsage.monthUsage, curMonth)) { + data.push({ + resource: userOverviewUsage.userEmail, + usage: userOverviewUsage.monthUsage[curMonth], + time: curMonth}) + } + } + }); + } else if (this.timeTypeToggleValue === ToggleValue.RESULTS_BY_DAY) { + this.props.data.forEach((userOverviewUsage: UserOverviewUsage) => { + for (const curDay in userOverviewUsage.dayUsage) { + if (Object.prototype.hasOwnProperty.call(userOverviewUsage.dayUsage, curDay)) { + data.push({ + resource: userOverviewUsage.userEmail, + usage: userOverviewUsage.dayUsage[curDay], + time: curDay + }) + } + } + }); + } + + if (this.filterToggled && data.length > 0 ) { + let tableFormat: string; + if (this.timeTypeToggleValue === ToggleValue.RESULTS_BY_MONTH) { + tableFormat = TABLE_MONTH_FORMAT; + } else if (this.timeTypeToggleValue === ToggleValue.RESULTS_BY_DAY) { + tableFormat = TABLE_DAY_FORMAT; + } + data = data.filter(resource => { + const fromTime = moment(this.fromDate).format(tableFormat); + const toTime = moment(this.toDate).format(tableFormat); + return resource.time >= fromTime && resource.time <= toTime; + }); + } + + if (this.resourcesTypeToggleValue === ToggleValue.ALL_RESOURCES) { + return data; + } else if (this.resourcesTypeToggleValue === ToggleValue.PUBLIC_RESOURCES) { + return ( + _.filter(data, function (usage) { + return !usage.resource.includes('/private/'); + }) + ); + } else { + return []; + } + } + + @computed get resetDefaultSort(): SortingRule[] { + if (this.timeTypeToggleValue === ToggleValue.RESULTS_IN_TOTAL) { + return [{ + id: 'totalUsage', + desc: true, + }]; + } else if (this.timeTypeToggleValue === ToggleValue.RESULTS_BY_MONTH) { + return [{ + id: UsageTableColumnKey.TIME, + desc: true, + }, + { + id: UsageTableColumnKey.USAGE, + desc: true, + }, + { + id: 'monthResetPlaceholder', + desc: true, + }]; + } else if (this.timeTypeToggleValue === ToggleValue.RESULTS_BY_DAY) { + return [{ + id: UsageTableColumnKey.TIME, + desc: true, + }, + { + id: UsageTableColumnKey.USAGE, + desc: true, + }, + { + id: 'dayResetPlaceholder', + desc: true, + }]; + } else if (this.filterToggled) { + return [{ + id: UsageTableColumnKey.TIME, + desc: true, + }, + { + id: UsageTableColumnKey.USAGE, + desc: true, + }, + { + id: 'calendarResetPlaceholder', + desc: true, + }]; + } else { + return []; + } + } + + render() { + return ( + <> + + data={this.calculateData} + columns= + {this.timeTypeToggleValue === ToggleValue.RESULTS_IN_TOTAL + ? [{ + id: 'userEmail', + Header: emailHeader, + accessor: 'userEmail', + onFilter: (row: UserOverviewUsage, keyword: string) => + filterByKeyword(row.userEmail, keyword), + }, + { + id: 'totalUsage', + Header: usageHeader, + accessor: 'totalUsage', + Cell(props: { original: UserOverviewUsage }) { + return ; + }, + }, + this.resourcesTypeToggleValue === + ToggleValue.ALL_RESOURCES + ? { + id: 'endpoint', + Header: endpointHeader, + minWidth: 200, + accessor: (row: UserOverviewUsage) => `${row.endpoint} (${row.maxUsageProportion}%)`, + onFilter: (row: UserOverviewUsage, keyword: string) => + filterByKeyword(row.endpoint, keyword), + } + : { + id: 'noPrivateEndpoint', + Header: noPrivateEndpointHeader, + minWidth: 200, + accessor: (row: UserOverviewUsage) => `${row.endpoint} (${row.noPrivateMaxUsageProportion}%)`, + onFilter: (row: UserOverviewUsage, keyword: string) => + filterByKeyword(row.noPrivateEndpoint, keyword), + }, + { + ...getUsageTableColumnDefinition( + UsageTableColumnKey.OPERATION + ), + sortable: false, + className: 'd-flex justify-content-center', + Cell(props: { original: UserOverviewUsage }) { + return ( + props.original.userId ? ( + + + + ) : + ); + }, + }, + ] + : [{ + id: 'userEmail', + Header: emailHeader, + accessor: 'resource', + onFilter: (row: UsageRecord, keyword: string) => + filterByKeyword(row.resource, keyword), + }, + { + ...getUsageTableColumnDefinition(UsageTableColumnKey.USAGE), + Cell(props: { original: UsageRecord }) { + return ; + }, + }, + { + ...getUsageTableColumnDefinition(UsageTableColumnKey.TIME), + Header: filterDependentTimeHeader(this.timeTypeToggleValue), + onFilter: (row: UsageRecord, keyword: string) => + filterByKeyword(row.time, keyword), + }, + { + ...getUsageTableColumnDefinition( + UsageTableColumnKey.OPERATION + ), + sortable: false, + className: 'd-flex justify-content-center', + Cell(props: { original: UserOverviewUsage }) { + return ( + props.original.userId ? ( + + + + ) : + ); + }, + }, + ] + } + loading={!this.props.loadedData} + defaultSorted={this.resetDefaultSort} + showPagination={true} + minRows={1} + defaultPageSize={this.props.defaultPageSize} + filters={() => { + return ( + + + + { + this.dropdownMenuOpen = isOpen; + }} + fromDate={(newDate: string) => { + this.fromDate = newDate; + }} + toDate={(newDate: string) => { + this.toDate = newDate; + }} + filterToggled={(filterActive: boolean) => { + this.filterToggled = filterActive; + }} + /> + + ); + }} + /> + + ); + } +} diff --git a/src/main/webapp/app/pages/usageAnalysisPage/UserUsageDetailsTable.tsx b/src/main/webapp/app/pages/usageAnalysisPage/UserUsageDetailsTable.tsx index 080695ca9..cf1549e9b 100644 --- a/src/main/webapp/app/pages/usageAnalysisPage/UserUsageDetailsTable.tsx +++ b/src/main/webapp/app/pages/usageAnalysisPage/UserUsageDetailsTable.tsx @@ -130,8 +130,8 @@ export default class UserUsageDetailsTable extends React.Component< { ...getUsageTableColumnDefinition(UsageTableColumnKey.RESOURCES), Header: filterDependentResourceHeader(this.timeTypeToggleValue), - onFilter: (data: UsageRecord, keyword) => - filterByKeyword(data.resource, keyword), + onFilter: (row: UsageRecord, keyword) => + filterByKeyword(row.resource, keyword), }, { ...getUsageTableColumnDefinition(UsageTableColumnKey.USAGE), @@ -142,8 +142,8 @@ export default class UserUsageDetailsTable extends React.Component< { ...getUsageTableColumnDefinition(UsageTableColumnKey.TIME), Header: filterDependentTimeHeader(this.timeTypeToggleValue), - onFilter: (data: UsageRecord, keyword) => - filterByKeyword(data.time, keyword), + onFilter: (row: UsageRecord, keyword) => + filterByKeyword(row.time, keyword), }, ]} loading={!this.props.loadedData} diff --git a/src/main/webapp/app/shared/api/generated/API.ts b/src/main/webapp/app/shared/api/generated/API.ts index cf668828b..68012eb42 100644 --- a/src/main/webapp/app/shared/api/generated/API.ts +++ b/src/main/webapp/app/shared/api/generated/API.ts @@ -335,11 +335,11 @@ export type UserMailsDTO = { export type UserOverviewUsage = { 'endpoint': string - 'maxUsage': number - 'noPrivateEndpoint': string - 'noPrivateMaxUsage': number + 'maxUsageProportion': number + + 'noPrivateMaxUsageProportion': number 'totalUsage': number @@ -347,6 +347,10 @@ export type UserOverviewUsage = { 'userId': string + 'dayUsage': {} + + 'monthUsage': {} + }; export type UserUsage = { 'company': string @@ -5564,4 +5568,4 @@ export default class API { return response.body; }); }; -} \ No newline at end of file +}