From 77480c73b95d1287b3f48bbb68c19a944f10b8a1 Mon Sep 17 00:00:00 2001 From: Dan Labrecque Date: Sat, 20 Apr 2024 23:17:45 -0400 Subject: [PATCH 1/2] Added cost model support for network and storage costs https://issues.redhat.com/browse/COST-4781 https://issues.redhat.com/browse/COST-4856 --- locales/data.json | 111 ++++++++++++++++- locales/translations.json | 9 +- src/api/costModels.ts | 4 + src/locales/messages.ts | 39 +++++- .../costModels/components/rateForm/utils.tsx | 2 + .../costModels/costModel/distribution.tsx | 23 +++- .../costModel/updateDistributionDialog.tsx | 114 +++++++++++++++++- .../costModels/costModelWizard/context.ts | 4 + .../costModelWizard/costModelWizard.tsx | 20 +++ .../costModelWizard/distribution.tsx | 95 +++++++++++++-- .../costModels/costModelWizard/review.tsx | 37 +++++- 11 files changed, 427 insertions(+), 31 deletions(-) diff --git a/locales/data.json b/locales/data.json index d6f7e0a86..8d404f1a2 100644 --- a/locales/data.json +++ b/locales/data.json @@ -3439,6 +3439,91 @@ } ], "distributeCosts": [ + { + "options": { + "false": { + "value": [ + { + "type": 0, + "value": "Do not distribute " + }, + { + "options": { + "network": { + "value": [ + { + "type": 0, + "value": "network" + } + ] + }, + "other": { + "value": [] + }, + "storage": { + "value": [ + { + "type": 0, + "value": "storage" + } + ] + } + }, + "type": 5, + "value": "type" + }, + { + "type": 0, + "value": " costs" + } + ] + }, + "other": { + "value": [] + }, + "true": { + "value": [ + { + "type": 0, + "value": "Distribute " + }, + { + "options": { + "network": { + "value": [ + { + "type": 0, + "value": "network" + } + ] + }, + "other": { + "value": [] + }, + "storage": { + "value": [ + { + "type": 0, + "value": "storage" + } + ] + } + }, + "type": 5, + "value": "type" + }, + { + "type": 0, + "value": " costs" + } + ] + } + }, + "type": 5, + "value": "value" + } + ], + "distributeUnallocatedCapacity": [ { "options": { "false": { @@ -3526,7 +3611,7 @@ "distributionModelDesc": [ { "type": 0, - "value": "This choice is for users to direct how their raw costs are distributed either by CPU or Memory on the project level breakdowns." + "value": "Choose how your raw costs are distributed at the project level." } ], "distributionType": [ @@ -10317,6 +10402,18 @@ "value": "count" } ], + "network": [ + { + "type": 0, + "value": "Network" + } + ], + "networkDesc": [ + { + "type": 0, + "value": "Distribute the cost of network traffic to projects based on distribution type." + } + ], "next": [ { "type": 0, @@ -12102,12 +12199,24 @@ "value": "Status/Actions" } ], + "storage": [ + { + "type": 0, + "value": "Storage" + } + ], "storageClass": [ { "type": 0, "value": "StorageClass" } ], + "storageDesc": [ + { + "type": 0, + "value": "Distribute the cost of storage to projects based on distribution type." + } + ], "suggestions": [ { "type": 0, diff --git a/locales/translations.json b/locales/translations.json index 37747ff4b..c81287ad1 100644 --- a/locales/translations.json +++ b/locales/translations.json @@ -240,8 +240,9 @@ "disabled": "Disabled", "discountMinus": "Discount (-)", "distribute": "Distribute", - "distributeCosts": "{value, select, true {Distribute {type, select, platform {platform} worker {worker} other {}} unallocated capacity}false {Do not distribute {type, select, platform {platform} worker {worker} other {}} unallocated capacity}other {}}", - "distributionModelDesc": "This choice is for users to direct how their raw costs are distributed either by CPU or Memory on the project level breakdowns.", + "distributeCosts": "{value, select, true {Distribute {type, select, network {network} storage {storage} other {}} costs}false {Do not distribute {type, select, network {network} storage {storage} other {}} costs}other {}}", + "distributeUnallocatedCapacity": "{value, select, true {Distribute {type, select, platform {platform} worker {worker} other {}} unallocated capacity}false {Do not distribute {type, select, platform {platform} worker {worker} other {}} unallocated capacity}other {}}", + "distributionModelDesc": "Choose how your raw costs are distributed at the project level.", "distributionType": "Distribution type", "distributionTypeDesc": "{type, select, cpu {Distribute costs based on CPU usage}memory {Distribute costs based on memory usage}other {}}", "doNotDistribute": "Do not distribute", @@ -380,6 +381,8 @@ "metricsOperatorVersion": "Cost Management operator version", "monthOverMonthChange": "Month over month change", "names": "{count, plural, one {Name} other {Names}}", + "network": "Network", + "networkDesc": "Distribute the cost of network traffic to projects based on distribution type.", "next": "next", "no": "No", "noDataForDate": "No data available for {dateRange}", @@ -546,7 +549,9 @@ "start": "Start", "status": "{value, select, pending {Pending} running {Running} failed {Failed} other {}}", "statusActions": "Status/Actions", + "storage": "Storage", "storageClass": "StorageClass", + "storageDesc": "Distribute the cost of storage to projects based on distribution type.", "suggestions": "Suggestions", "sumPlatformCosts": "Sum platform costs", "summary": "Summary", diff --git a/src/api/costModels.ts b/src/api/costModels.ts index 3687b4951..bfe3d15cf 100644 --- a/src/api/costModels.ts +++ b/src/api/costModels.ts @@ -15,7 +15,9 @@ export interface CostModel { description: string; distribution_info?: { distribution_type?: string; + network_cost?: boolean; platform_cost?: boolean; + storage_cost?: boolean; worker_cost?: boolean; }; markup: { value: string; unit: string }; @@ -32,7 +34,9 @@ export interface CostModelRequest { description: string; distribution_info?: { distribution_type?: string; + network_cost?: boolean; platform_cost?: boolean; + storage_cost?: boolean; worker_cost?: boolean; }; markup: { value: string; unit: string }; diff --git a/src/locales/messages.ts b/src/locales/messages.ts index 3d67d9700..872288913 100644 --- a/src/locales/messages.ts +++ b/src/locales/messages.ts @@ -1410,17 +1410,24 @@ export default defineMessages({ distributeCosts: { defaultMessage: '{value, select, ' + - 'true {Distribute {type, select, platform {platform} worker {worker} other {}} unallocated capacity}' + - 'false {Do not distribute {type, select, platform {platform} worker {worker} other {}} unallocated capacity}' + + 'true {Distribute {type, select, network {network} storage {storage} other {}} costs}' + + 'false {Do not distribute {type, select, network {network} storage {storage} other {}} costs}' + 'other {}}', description: 'distribute costs', id: 'distributeCosts', }, - distributionModelDesc: { + distributeUnallocatedCapacity: { defaultMessage: - 'This choice is for users to direct how their raw costs are distributed either by CPU or Memory on the project level breakdowns.', - description: - 'This choice is for users to direct how their raw costs are distributed either by CPU or Memory on the project level breakdowns.', + '{value, select, ' + + 'true {Distribute {type, select, platform {platform} worker {worker} other {}} unallocated capacity}' + + 'false {Do not distribute {type, select, platform {platform} worker {worker} other {}} unallocated capacity}' + + 'other {}}', + description: 'distribute unallocated capacity', + id: 'distributeUnallocatedCapacity', + }, + distributionModelDesc: { + defaultMessage: 'Choose how your raw costs are distributed at the project level.', + description: 'Choose how your raw costs are distributed at the project level.', id: 'distributionModelDesc', }, distributionType: { @@ -2457,6 +2464,16 @@ export default defineMessages({ description: 'Name plural or singular', id: 'names', }, + network: { + defaultMessage: 'Network', + description: 'Network', + id: 'network', + }, + networkDesc: { + defaultMessage: 'Distribute the cost of network traffic to projects based on distribution type.', + description: 'Distribute the cost of network traffic to projects based on distribution type.', + id: 'networkDesc', + }, next: { defaultMessage: 'next', description: 'next', @@ -3364,11 +3381,21 @@ export default defineMessages({ description: 'Status/Actions', id: 'statusActions', }, + storage: { + defaultMessage: 'Storage', + description: 'Storage', + id: 'storage', + }, storageClass: { defaultMessage: 'StorageClass', description: 'StorageClass', id: 'storageClass', }, + storageDesc: { + defaultMessage: 'Distribute the cost of storage to projects based on distribution type.', + description: 'Distribute the cost of storage to projects based on distribution type.', + id: 'storageDesc', + }, suggestions: { defaultMessage: 'Suggestions', description: 'Suggestions', diff --git a/src/routes/settings/costModels/components/rateForm/utils.tsx b/src/routes/settings/costModels/components/rateForm/utils.tsx index a4f7ad582..1952cb985 100644 --- a/src/routes/settings/costModels/components/rateForm/utils.tsx +++ b/src/routes/settings/costModels/components/rateForm/utils.tsx @@ -173,7 +173,9 @@ export const mergeToRequest = ( description: costModel.description, distribution_info: { distribution_type: costModel.distribution_info ? costModel.distribution_info.distribution_type : undefined, + network_cost: costModel.distribution_info ? costModel.distribution_info.network_cost : undefined, platform_cost: costModel.distribution_info ? costModel.distribution_info.platform_cost : undefined, + storage_cost: costModel.distribution_info ? costModel.distribution_info.storage_cost : undefined, worker_cost: costModel.distribution_info ? costModel.distribution_info.worker_cost : undefined, }, source_uuids: costModel.sources.map(src => src.uuid), diff --git a/src/routes/settings/costModels/costModel/distribution.tsx b/src/routes/settings/costModels/costModel/distribution.tsx index 871d5d87f..4ef301fe3 100644 --- a/src/routes/settings/costModels/costModel/distribution.tsx +++ b/src/routes/settings/costModels/costModel/distribution.tsx @@ -1,5 +1,6 @@ import { Button, ButtonVariant, Card, CardBody, CardHeader, Title, TitleSizes } from '@patternfly/react-core'; import type { CostModel } from 'api/costModels'; +import { useIsOcpCloudNetworkingToggleEnabled, useIsOcpProjectStorageToggleEnabled } from 'components/featureToggle'; import messages from 'locales/messages'; import React from 'react'; import { useIntl } from 'react-intl'; @@ -26,6 +27,8 @@ const DistributionCardBase: React.FC = ({ isUpdateDialogOpen, }) => { const intl = useIntl(); + const isOcpCloudNetworkingToggleEnabled = useIsOcpCloudNetworkingToggleEnabled(); + const isOcpProjectStorageToggleEnabled = useIsOcpProjectStorageToggleEnabled(); return ( <> @@ -62,17 +65,33 @@ const DistributionCardBase: React.FC = ({ })}
- {intl.formatMessage(messages.distributeCosts, { + {intl.formatMessage(messages.distributeUnallocatedCapacity, { value: current.distribution_info.platform_cost, type: 'platform', })}
- {intl.formatMessage(messages.distributeCosts, { + {intl.formatMessage(messages.distributeUnallocatedCapacity, { value: current.distribution_info.worker_cost, type: 'worker', })}
+ {isOcpCloudNetworkingToggleEnabled && ( +
+ {intl.formatMessage(messages.distributeCosts, { + value: current.distribution_info.network_cost, + type: 'network', + })} +
+ )} + {isOcpProjectStorageToggleEnabled && ( +
+ {intl.formatMessage(messages.distributeCosts, { + value: current.distribution_info.storage_cost, + type: 'storage', + })} +
+ )} diff --git a/src/routes/settings/costModels/costModel/updateDistributionDialog.tsx b/src/routes/settings/costModels/costModel/updateDistributionDialog.tsx index 9f087123a..4db80b496 100644 --- a/src/routes/settings/costModels/costModel/updateDistributionDialog.tsx +++ b/src/routes/settings/costModels/costModel/updateDistributionDialog.tsx @@ -22,6 +22,7 @@ import { connect } from 'react-redux'; import { createMapStateToProps } from 'store/common'; import { costModelsActions, costModelsSelectors } from 'store/costModels'; +import { FeatureToggleSelectors } from '../../../../store/featureToggle'; import { styles } from './costCalc.styles'; interface UpdateDistributionDialogOwnProps extends WrappedComponentProps { @@ -31,6 +32,8 @@ interface UpdateDistributionDialogOwnProps extends WrappedComponentProps { interface UpdateDistributionDialogStateProps { error?: string; isLoading?: boolean; + isOcpCloudNetworkingToggleEnabled?: boolean; + isOcpProjectStorageToggleEnabled?: boolean; } interface UpdateDistributionDialogDispatchProps { @@ -40,7 +43,9 @@ interface UpdateDistributionDialogDispatchProps { interface UpdateDistributionDialogState { distribution?: string; + distributeNetwork?: boolean; distributePlatformUnallocated?: boolean; + distributeStorage?: boolean; distributeWorkerUnallocated?: boolean; } @@ -56,7 +61,9 @@ class UpdateDistributionDialogBase extends React.Component< super(props); this.state = { distribution: this.props.current.distribution_info.distribution_type, + distributeNetwork: this.props.current.distribution_info.network_cost === true, distributePlatformUnallocated: this.props.current.distribution_info.platform_cost === true, + distributeStorage: this.props.current.distribution_info.storage_cost === true, distributeWorkerUnallocated: this.props.current.distribution_info.worker_cost === true, }; } @@ -76,8 +83,27 @@ class UpdateDistributionDialogBase extends React.Component< this.setState({ distributeWorkerUnallocated: value === 'true' }); }; + private handleDistributeNetworkChange = event => { + const { value } = event.currentTarget; + this.setState({ distributeNetwork: value === 'true' }); + }; + + private handleDistributeStorageChange = event => { + const { value } = event.currentTarget; + this.setState({ distributeStorage: value === 'true' }); + }; + public render() { - const { error, current, intl, isLoading, onClose, updateCostModel } = this.props; + const { + error, + current, + intl, + isLoading, + isOcpCloudNetworkingToggleEnabled, + isOcpProjectStorageToggleEnabled, + onClose, + updateCostModel, + } = this.props; return ( + {isOcpCloudNetworkingToggleEnabled && ( + <> + + + {intl.formatMessage(messages.network)} + + + {intl.formatMessage(messages.networkDesc)} + + + +
+ + + + +
+
+ + )} + {isOcpProjectStorageToggleEnabled && ( + <> + + + {intl.formatMessage(messages.storage)} + + + {intl.formatMessage(messages.storageDesc)} + + + +
+ + + + +
+
+ + )}
); @@ -228,6 +328,8 @@ const mapStateToProps = createMapStateToProps { return { isLoading: costModelsSelectors.updateProcessing(state), + isOcpCloudNetworkingToggleEnabled: FeatureToggleSelectors.selectIsOcpCloudNetworkingToggleEnabled(state), + isOcpProjectStorageToggleEnabled: FeatureToggleSelectors.selectIsOcpProjectStorageToggleEnabled(state), error: costModelsSelectors.updateError(state), }; } diff --git a/src/routes/settings/costModels/costModelWizard/context.ts b/src/routes/settings/costModels/costModelWizard/context.ts index 8af17a7aa..501643864 100644 --- a/src/routes/settings/costModels/costModelWizard/context.ts +++ b/src/routes/settings/costModels/costModelWizard/context.ts @@ -15,7 +15,9 @@ export const defaultCostModelContext = { description: '', dirtyName: false, distribution: '', + distributeNetwork: true, distributePlatformUnallocated: true, + distributeStorage: true, distributeWorkerUnallocated: true, error: null, fetchSources: (type: string, query: any, page: number, perPage: number) => null, @@ -24,7 +26,9 @@ export const defaultCostModelContext = { isDiscount: false, handleMarkupDiscountChange: (...args: any[]) => null, handleDistributionChange: (...args: any[]) => null, + handleDistributeNetworkChange: (...args: any[]) => null, handleDistributePlatformUnallocatedChange: (...args: any[]) => null, + handleDistributeStorageChange: (...args: any[]) => null, handleDistributeWorkerUnallocatedChange: (...args: any[]) => null, handleSignChange: (...args: any[]) => null, loading: false, diff --git a/src/routes/settings/costModels/costModelWizard/costModelWizard.tsx b/src/routes/settings/costModels/costModelWizard/costModelWizard.tsx index 0b300c5b4..d5dfea849 100644 --- a/src/routes/settings/costModels/costModelWizard/costModelWizard.tsx +++ b/src/routes/settings/costModels/costModelWizard/costModelWizard.tsx @@ -109,7 +109,9 @@ const InternalWizardBase: React.FC = ({ currency, description, distribution, + distributeNetwork, distributePlatformUnallocated, + distributeStorage, distributeWorkerUnallocated, isDiscount, markup, @@ -125,7 +127,9 @@ const InternalWizardBase: React.FC = ({ description, distribution_info: { distribution_type: distribution, + network_cost: distributeNetwork, platform_cost: distributePlatformUnallocated, + storage_cost: distributeStorage, worker_cost: distributeWorkerUnallocated, }, rates: tiers, @@ -166,7 +170,9 @@ interface CostModelWizardState { description?: string; dirtyName?: boolean; distribution?: string; + distributeNetwork?: boolean; distributePlatformUnallocated?: boolean; + distributeStorage?: boolean; distributeWorkerUnallocated?: boolean; error?: any; filterName?: string; @@ -207,7 +213,9 @@ class CostModelWizardBase extends React.Component { + const { value } = event.currentTarget; + this.setState({ distributeNetwork: value === 'true' }); + }, handleDistributePlatformUnallocatedChange: event => { const { value } = event.currentTarget; this.setState({ distributePlatformUnallocated: value === 'true' }); }, + handleDistributeStorageChange: event => { + const { value } = event.currentTarget; + this.setState({ distributeStorage: value === 'true' }); + }, handleDistributeWorkerUnallocatedChange: event => { const { value } = event.currentTarget; this.setState({ distributeWorkerUnallocated: value === 'true' }); @@ -531,7 +549,9 @@ class CostModelWizardBase extends React.Component { public render() { - const { intl } = this.props; + const { isOcpCloudNetworkingToggleEnabled, isOcpProjectStorageToggleEnabled, intl } = this.props; return ( {({ handleDistributionChange, + handleDistributeNetworkChange, handleDistributePlatformUnallocatedChange, + handleDistributeStorageChange, handleDistributeWorkerUnallocatedChange, distribution, + distributeNetwork, distributePlatformUnallocated, + distributeStorage, distributeWorkerUnallocated, }) => { return ( @@ -57,7 +63,7 @@ class DistributionBase extends React.Component + {isOcpCloudNetworkingToggleEnabled && ( + <> + + + {intl.formatMessage(messages.network)} + + + {intl.formatMessage(messages.networkDesc)} + + + +
+ + + + +
+
+ + )} + {isOcpProjectStorageToggleEnabled && ( + <> + + + {intl.formatMessage(messages.storage)} + + + {intl.formatMessage(messages.storageDesc)} + + + +
+ + + + +
+
+ + )} ); }} @@ -148,9 +226,10 @@ class DistributionBase extends React.Component(() => { +const mapStateToProps = createMapStateToProps(state => { return { - // TBD... + isOcpCloudNetworkingToggleEnabled: FeatureToggleSelectors.selectIsOcpCloudNetworkingToggleEnabled(state), + isOcpProjectStorageToggleEnabled: FeatureToggleSelectors.selectIsOcpProjectStorageToggleEnabled(state), }; }); diff --git a/src/routes/settings/costModels/costModelWizard/review.tsx b/src/routes/settings/costModels/costModelWizard/review.tsx index 338fee686..bccbc52ac 100644 --- a/src/routes/settings/costModels/costModelWizard/review.tsx +++ b/src/routes/settings/costModels/costModelWizard/review.tsx @@ -28,6 +28,7 @@ import { connect } from 'react-redux'; import { RateTable } from 'routes/settings/costModels/components/rateTable'; import { WarningIcon } from 'routes/settings/costModels/components/warningIcon'; import { createMapStateToProps } from 'store/common'; +import { FeatureToggleSelectors } from 'store/featureToggle'; import { CostModelContext } from './context'; @@ -66,12 +67,17 @@ interface ReviewDetailsOwnProps extends WrappedComponentProps { } interface ReviewDetailsStateProps { - // TBD... + isOcpCloudNetworkingToggleEnabled?: boolean; + isOcpProjectStorageToggleEnabled?: boolean; } type ReviewDetailsProps = ReviewDetailsOwnProps & ReviewDetailsStateProps; -const ReviewDetailsBase: React.FC = ({ intl }) => ( +const ReviewDetailsBase: React.FC = ({ + intl, + isOcpCloudNetworkingToggleEnabled, + isOcpProjectStorageToggleEnabled, +}) => ( {({ checked, @@ -79,7 +85,9 @@ const ReviewDetailsBase: React.FC = ({ intl }) => ( currencyUnits, description, distribution, + distributeNetwork, distributePlatformUnallocated, + distributeStorage, distributeWorkerUnallocated, isDiscount, markup, @@ -155,17 +163,33 @@ const ReviewDetailsBase: React.FC = ({ intl }) => ( {intl.formatMessage(messages.distributionTypeDesc, { type: distribution })} - {intl.formatMessage(messages.distributeCosts, { + {intl.formatMessage(messages.distributeUnallocatedCapacity, { value: distributePlatformUnallocated, type: 'platform', })} - {intl.formatMessage(messages.distributeCosts, { + {intl.formatMessage(messages.distributeUnallocatedCapacity, { value: distributeWorkerUnallocated, type: 'worker', })} + {isOcpCloudNetworkingToggleEnabled && ( + + {intl.formatMessage(messages.distributeCosts, { + value: distributeNetwork, + type: 'network', + })} + + )} + {isOcpProjectStorageToggleEnabled && ( + + {intl.formatMessage(messages.distributeCosts, { + value: distributeStorage, + type: 'storage', + })} + + )} )} @@ -187,9 +211,10 @@ const ReviewDetailsBase: React.FC = ({ intl }) => ( ); -const mapStateToProps = createMapStateToProps(() => { +const mapStateToProps = createMapStateToProps(state => { return { - // TBD... + isOcpCloudNetworkingToggleEnabled: FeatureToggleSelectors.selectIsOcpCloudNetworkingToggleEnabled(state), + isOcpProjectStorageToggleEnabled: FeatureToggleSelectors.selectIsOcpProjectStorageToggleEnabled(state), }; }); From 720fa44f394add47ca73e53bd3b95b051831aa55 Mon Sep 17 00:00:00 2001 From: Dan Labrecque Date: Sun, 21 Apr 2024 01:22:51 -0400 Subject: [PATCH 2/2] Stubbed-out historical layout for network and storage features --- locales/data.json | 8 + locales/translations.json | 2 +- src/api/reports/ocpReports.ts | 1 + src/locales/messages.ts | 1 + .../historicalData/historicalDataBase.tsx | 65 ++++- .../historicalDataVolumeChart.tsx | 226 ++++++++++++++++++ .../details/ocpBreakdown/historicalData.tsx | 3 + .../costModel/updateDistributionDialog.tsx | 2 +- .../common/historicalDataCommon.ts | 2 + .../ocpHistoricalData.test.ts | 16 +- .../ocpHistoricalDataReducer.ts | 12 +- .../ocpHistoricalDataWidgets.ts | 16 ++ 12 files changed, 347 insertions(+), 7 deletions(-) create mode 100644 src/routes/details/components/historicalData/historicalDataVolumeChart.tsx diff --git a/locales/data.json b/locales/data.json index 8d404f1a2..7d9d3ce17 100644 --- a/locales/data.json +++ b/locales/data.json @@ -9799,6 +9799,14 @@ } ] }, + "network": { + "value": [ + { + "type": 0, + "value": "Network usage comparison" + } + ] + }, "other": { "value": [] }, diff --git a/locales/translations.json b/locales/translations.json index c81287ad1..a66dcbf17 100644 --- a/locales/translations.json +++ b/locales/translations.json @@ -339,7 +339,7 @@ "groupByValuesTitleCase": "{value, select, account {{count, plural, one {Account} other {Accounts}}} aws_category {{count, plural, one {Cost category} other {Cost categories}}} cluster {{count, plural, one {Cluster} other {Clusters}}} gcp_project {{count, plural, one {GCP project} other {GCP projects}}} node {{count, plural, one {Node} other {Node}}} org_unit_id {{count, plural, one {Organizational unit} other {Organizational units}}} payer_tenant_id {{count, plural, one {Account} other {Accounts}}} product_service {{count, plural, one {Service} other {Services}}} project {{count, plural, one {Project} other {Projects}}} region {{count, plural, one {Region} other {Regions}}} resource_location {{count, plural, one {Region} other {Regions}}} service {{count, plural, one {Service} other {Services}}} service_name {{count, plural, one {Service} other {Services}}} subscription_guid {{count, plural, one {Account} other {Accounts}}} tag {{count, plural, one {Tag} other {Tags}}} other {}}", "historicalChartCostLabel": "Cost ({units})", "historicalChartDayOfMonthLabel": "Day of Month", - "historicalChartTitle": "{value, select, cost {Cost comparison} cpu {CPU usage, request, and limit comparison} instance_type {Compute usage comparison}memory {Memory usage, request, and limit comparison} modal {{name} daily usage comparison} storage {Storage usage comparison} virtual_machine {Virtual machine usage comparison}other {}}", + "historicalChartTitle": "{value, select, cost {Cost comparison} cpu {CPU usage, request, and limit comparison} instance_type {Compute usage comparison}memory {Memory usage, request, and limit comparison} modal {{name} daily usage comparison} network {Network usage comparison} storage {Storage usage comparison} virtual_machine {Virtual machine usage comparison}other {}}", "historicalChartUsageLabel": "{value, select, instance_type {hrs} storage {gb-mo} other {}}", "ibm": "IBM Cloud", "ibmComputeTitle": "Compute instances usage", diff --git a/src/api/reports/ocpReports.ts b/src/api/reports/ocpReports.ts index c7eafab80..4360ad580 100644 --- a/src/api/reports/ocpReports.ts +++ b/src/api/reports/ocpReports.ts @@ -61,6 +61,7 @@ export const ReportTypePaths: Partial> = { [ReportType.cost]: 'reports/openshift/costs/', [ReportType.cpu]: 'reports/openshift/compute/', [ReportType.memory]: 'reports/openshift/memory/', + [ReportType.network]: 'reports/openshift/volumes/', // TBD: Use "network" when API is available [ReportType.volume]: 'reports/openshift/volumes/', }; diff --git a/src/locales/messages.ts b/src/locales/messages.ts index 872288913..c5975ac44 100644 --- a/src/locales/messages.ts +++ b/src/locales/messages.ts @@ -2222,6 +2222,7 @@ export default defineMessages({ 'instance_type {Compute usage comparison}' + 'memory {Memory usage, request, and limit comparison} ' + 'modal {{name} daily usage comparison} ' + + 'network {Network usage comparison} ' + 'storage {Storage usage comparison} ' + 'virtual_machine {Virtual machine usage comparison}' + 'other {}}', diff --git a/src/routes/details/components/historicalData/historicalDataBase.tsx b/src/routes/details/components/historicalData/historicalDataBase.tsx index 92242e8cb..7c5b683e0 100644 --- a/src/routes/details/components/historicalData/historicalDataBase.tsx +++ b/src/routes/details/components/historicalData/historicalDataBase.tsx @@ -10,6 +10,7 @@ import { HistoricalDataWidgetType } from 'store/breakdown/historicalData/common/ import { HistoricalDataCostChart } from './historicalDataCostChart'; import { HistoricalDataTrendChart } from './historicalDataTrendChart'; import { HistoricalDataUsageChart } from './historicalDataUsageChart'; +import { HistoricalDataVolumeChart } from './historicalDataVolumeChart'; interface HistoricalDataOwnProps { costDistribution?: string; @@ -18,6 +19,8 @@ interface HistoricalDataOwnProps { } export interface HistoricalDataStateProps { + isOcpCloudNetworkingToggleEnabled?: boolean; + isOcpProjectStorageToggleEnabled?: boolean; selectWidgets?: Record; widgets: number[]; } @@ -26,7 +29,9 @@ type HistoricalDataProps = HistoricalDataOwnProps & HistoricalDataStateProps & W class HistoricalDatasBase extends React.Component { private getTitleKey = (reportPathsType, reportType) => { - if (reportPathsType === ReportPathsType.azure) { + if (reportPathsType === ReportPathsType.ocp) { + return reportType === ReportType.volume ? 'storage' : reportType; + } else if (reportPathsType === ReportPathsType.azure) { return reportType === ReportType.instanceType ? 'virtual_machine' : reportType; } return reportType === ReportType.instanceType ? 'instance_type' : reportType; @@ -59,6 +64,60 @@ class HistoricalDatasBase extends React.Component { ); }; + // Returns network chart + private getNetworkChart = (widget: HistoricalDataWidget) => { + const { intl, isOcpCloudNetworkingToggleEnabled } = this.props; + + if (widget.reportPathsType === ReportPathsType.ocp && !isOcpCloudNetworkingToggleEnabled) { + return null; + } + return ( + + + + {intl.formatMessage(messages.historicalChartTitle, { + value: this.getTitleKey(widget.reportPathsType, widget.reportType), + })} + + + + + + + ); + }; + + // Returns volume chart + private getVolumeChart = (widget: HistoricalDataWidget) => { + const { intl, isOcpProjectStorageToggleEnabled } = this.props; + + if (widget.reportPathsType === ReportPathsType.ocp && !isOcpProjectStorageToggleEnabled) { + return null; + } + return ( + + + + {intl.formatMessage(messages.historicalChartTitle, { + value: this.getTitleKey(widget.reportPathsType, widget.reportType), + })} + + + + + + + ); + }; + // Returns trend chart private getTrendChart = (widget: HistoricalDataWidget) => { const { costType, currency, intl } = this.props; @@ -114,10 +173,14 @@ class HistoricalDatasBase extends React.Component { switch (widget.type) { case HistoricalDataWidgetType.cost: return this.getCostChart(widget); + case HistoricalDataWidgetType.network: + return this.getNetworkChart(widget); case HistoricalDataWidgetType.trend: return this.getTrendChart(widget); case HistoricalDataWidgetType.usage: return this.getUsageChart(widget); + case HistoricalDataWidgetType.volume: + return this.getVolumeChart(widget); default: return null; } diff --git a/src/routes/details/components/historicalData/historicalDataVolumeChart.tsx b/src/routes/details/components/historicalData/historicalDataVolumeChart.tsx new file mode 100644 index 000000000..2a4be18a5 --- /dev/null +++ b/src/routes/details/components/historicalData/historicalDataVolumeChart.tsx @@ -0,0 +1,226 @@ +import { Skeleton } from '@patternfly/react-core'; +import type { Query } from 'api/queries/query'; +import { getQuery, parseQuery } from 'api/queries/query'; +import type { Report, ReportPathsType } from 'api/reports/report'; +import { ReportType } from 'api/reports/report'; +import messages from 'locales/messages'; +import React from 'react'; +import type { WrappedComponentProps } from 'react-intl'; +import { injectIntl } from 'react-intl'; +import { connect } from 'react-redux'; +import { DatumType, transformReport } from 'routes/components/charts/common/chartDatum'; +import { HistoricalUsageChart } from 'routes/components/charts/historicalUsageChart'; +import { getGroupById, getGroupByOrgValue, getGroupByValue } from 'routes/utils/groupBy'; +import { getQueryState } from 'routes/utils/queryState'; +import { skeletonWidth } from 'routes/utils/skeleton'; +import { createMapStateToProps, FetchStatus } from 'store/common'; +import { reportActions, reportSelectors } from 'store/reports'; +import { formatUnits, unitsLookupKey } from 'utils/format'; +import { logicalAndPrefix, logicalOrPrefix, orgUnitIdKey, platformCategoryKey } from 'utils/props'; +import type { RouterComponentProps } from 'utils/router'; +import { withRouter } from 'utils/router'; + +import { chartStyles, styles } from './historicalChart.styles'; + +interface HistoricalDataVolumeChartOwnProps extends RouterComponentProps, WrappedComponentProps { + chartName?: string; + reportPathsType: ReportPathsType; + reportType: ReportType; +} + +interface HistoricalDataVolumeChartStateProps { + currentQuery?: Query; + currentQueryString?: string; + currentReport?: Report; + currentReportFetchStatus?: FetchStatus; + previousQuery?: Query; + previousQueryString?: string; + previousReport?: Report; + previousReportFetchStatus?: FetchStatus; +} + +interface HistoricalDataVolumeChartDispatchProps { + fetchReport?: typeof reportActions.fetchReport; +} + +type HistoricalDataVolumeChartProps = HistoricalDataVolumeChartOwnProps & + HistoricalDataVolumeChartStateProps & + HistoricalDataVolumeChartDispatchProps; + +class HistoricalDataVolumeChartBase extends React.Component { + public componentDidMount() { + this.updateReport(); + } + + public componentDidUpdate(prevProps: HistoricalDataVolumeChartProps) { + const { currentQueryString, previousQueryString } = this.props; + + if (prevProps.currentQueryString !== currentQueryString || prevProps.previousQueryString !== previousQueryString) { + this.updateReport(); + } + } + + private getSkeleton = () => { + return ( + <> + + + + ); + }; + + private updateReport = () => { + const { fetchReport, currentQueryString, previousQueryString, reportPathsType, reportType } = this.props; + fetchReport(reportPathsType, reportType, currentQueryString); + fetchReport(reportPathsType, reportType, previousQueryString); + }; + + public render() { + const { chartName, currentReport, currentReportFetchStatus, previousReport, previousReportFetchStatus, intl } = + this.props; + + // Current data + const currentLimitData = transformReport(currentReport, DatumType.rolling, 'date', 'limit'); + const currentRequestData = transformReport(currentReport, DatumType.rolling, 'date', 'request'); + const currentUsageData = transformReport(currentReport, DatumType.rolling, 'date', 'usage'); + + // Previous data + const previousLimitData = transformReport(previousReport, DatumType.rolling, 'date', 'limit'); + const previousRequestData = transformReport(previousReport, DatumType.rolling, 'date', 'request'); + const previousUsageData = transformReport(previousReport, DatumType.rolling, 'date', 'usage'); + + const usageUnits = currentReport?.meta?.total?.usage ? currentReport.meta.total.usage.units : ''; + + return ( +
+
+ {currentReportFetchStatus === FetchStatus.inProgress && + previousReportFetchStatus === FetchStatus.inProgress ? ( + this.getSkeleton() + ) : ( + + )} +
+
+ ); + } +} + +const mapStateToProps = createMapStateToProps( + (state, { reportPathsType, reportType, router }) => { + const queryFromRoute = parseQuery(router.location.search); + const queryState = getQueryState(router.location, 'details'); + + const groupByOrgValue = getGroupByOrgValue(queryFromRoute); + const groupBy = getGroupById(queryFromRoute); + const groupByValue = getGroupByValue(queryFromRoute); + + // instance-types and storage APIs must filter org units + const useFilter = reportType === ReportType.instanceType || reportType === ReportType.storage; + + const baseQuery: Query = { + filter_by: { + // Add filters here to apply logical OR/AND + ...(queryState?.filter_by && queryState.filter_by), + ...(queryFromRoute?.isPlatformCosts && { category: platformCategoryKey }), + ...(queryFromRoute?.filter?.account && { [`${logicalAndPrefix}account`]: queryFromRoute.filter.account }), + ...(groupByOrgValue && useFilter && { [orgUnitIdKey]: groupByOrgValue }), + // Workaround for https://issues.redhat.com/browse/COST-1189 + ...(queryState?.filter_by && + queryState.filter_by[orgUnitIdKey] && { + [`${logicalOrPrefix}${orgUnitIdKey}`]: queryState.filter_by[orgUnitIdKey], + [orgUnitIdKey]: undefined, + }), + }, + exclude: { + ...(queryState?.exclude && queryState.exclude), + }, + group_by: { + ...(groupByOrgValue && !useFilter && { [orgUnitIdKey]: groupByOrgValue }), + ...(groupBy && !groupByOrgValue && { [groupBy]: groupByValue }), + }, + }; + + // Current report + const currentQuery: Query = { + ...baseQuery, + filter: { + resolution: 'daily', + time_scope_units: 'month', + time_scope_value: -1, + }, + filter_by: { + ...baseQuery.filter_by, + // Omit filters associated with the current group_by -- see https://issues.redhat.com/browse/COST-1131 and https://issues.redhat.com/browse/COST-3642 + ...(groupBy && groupByValue !== '*' && { [groupBy]: undefined }), // Used by the "Platform" project + }, + }; + + const currentQueryString = getQuery(currentQuery); + const currentReport = reportSelectors.selectReport(state, reportPathsType, reportType, currentQueryString); + const currentReportFetchStatus = reportSelectors.selectReportFetchStatus( + state, + reportPathsType, + reportType, + currentQueryString + ); + + // Previous report + const previousQuery: Query = { + ...baseQuery, + filter: { + resolution: 'daily', + time_scope_units: 'month', + time_scope_value: -2, + }, + filter_by: { + ...baseQuery.filter_by, + // Omit filters associated with the current group_by -- see https://issues.redhat.com/browse/COST-1131 and https://issues.redhat.com/browse/COST-3642 + ...(groupBy && groupByValue !== '*' && { [groupBy]: undefined }), // Used by the "Platform" project + }, + }; + + const previousQueryString = getQuery(previousQuery); + const previousReport = reportSelectors.selectReport(state, reportPathsType, reportType, previousQueryString); + const previousReportFetchStatus = reportSelectors.selectReportFetchStatus( + state, + reportPathsType, + reportType, + previousQueryString + ); + + return { + currentQuery, + currentQueryString, + currentReport, + currentReportFetchStatus, + previousQuery, + previousQueryString, + previousReport, + previousReportFetchStatus, + }; + } +); + +const mapDispatchToProps: HistoricalDataVolumeChartDispatchProps = { + fetchReport: reportActions.fetchReport, +}; + +const HistoricalDataVolumeChart = injectIntl( + withRouter(connect(mapStateToProps, mapDispatchToProps)(HistoricalDataVolumeChartBase)) +); + +export { HistoricalDataVolumeChart }; diff --git a/src/routes/details/ocpBreakdown/historicalData.tsx b/src/routes/details/ocpBreakdown/historicalData.tsx index 4ec1caf74..0913d1b66 100644 --- a/src/routes/details/ocpBreakdown/historicalData.tsx +++ b/src/routes/details/ocpBreakdown/historicalData.tsx @@ -3,6 +3,7 @@ import type { HistoricalDataStateProps } from 'routes/details/components/histori import { HistoricalDataBase } from 'routes/details/components/historicalData'; import { ocpHistoricalDataSelectors } from 'store/breakdown/historicalData/ocpHistoricalData'; import { createMapStateToProps } from 'store/common'; +import { FeatureToggleSelectors } from 'store/featureToggle'; interface OcpHistoricalDataOwnProps { // TBD... @@ -11,6 +12,8 @@ interface OcpHistoricalDataOwnProps { // eslint-disable-next-line @typescript-eslint/no-unused-vars const mapStateToProps = createMapStateToProps((state, props) => { return { + isOcpCloudNetworkingToggleEnabled: FeatureToggleSelectors.selectIsOcpCloudNetworkingToggleEnabled(state), + isOcpProjectStorageToggleEnabled: FeatureToggleSelectors.selectIsOcpProjectStorageToggleEnabled(state), selectWidgets: ocpHistoricalDataSelectors.selectWidgets(state), widgets: ocpHistoricalDataSelectors.selectCurrentWidgets(state), }; diff --git a/src/routes/settings/costModels/costModel/updateDistributionDialog.tsx b/src/routes/settings/costModels/costModel/updateDistributionDialog.tsx index 4db80b496..1ef01e463 100644 --- a/src/routes/settings/costModels/costModel/updateDistributionDialog.tsx +++ b/src/routes/settings/costModels/costModel/updateDistributionDialog.tsx @@ -21,8 +21,8 @@ import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { createMapStateToProps } from 'store/common'; import { costModelsActions, costModelsSelectors } from 'store/costModels'; +import { FeatureToggleSelectors } from 'store/featureToggle'; -import { FeatureToggleSelectors } from '../../../../store/featureToggle'; import { styles } from './costCalc.styles'; interface UpdateDistributionDialogOwnProps extends WrappedComponentProps { diff --git a/src/store/breakdown/historicalData/common/historicalDataCommon.ts b/src/store/breakdown/historicalData/common/historicalDataCommon.ts index 98ad275fa..05ca74bd5 100644 --- a/src/store/breakdown/historicalData/common/historicalDataCommon.ts +++ b/src/store/breakdown/historicalData/common/historicalDataCommon.ts @@ -3,8 +3,10 @@ import type { ReportPathsType, ReportType } from 'api/reports/report'; // eslint-disable-next-line no-shadow export const enum HistoricalDataWidgetType { cost = 'cost', // This type displays historical cost chart + network = 'network', // This type displays historical network chart trend = 'trend', // This type displays historical trend chart usage = 'usage', // This type displays historical usage chart + volume = 'volume', // This type displays historical volume chart } export interface HistoricalDataWidget { diff --git a/src/store/breakdown/historicalData/ocpHistoricalData/ocpHistoricalData.test.ts b/src/store/breakdown/historicalData/ocpHistoricalData/ocpHistoricalData.test.ts index 67a8ef645..9414178e7 100644 --- a/src/store/breakdown/historicalData/ocpHistoricalData/ocpHistoricalData.test.ts +++ b/src/store/breakdown/historicalData/ocpHistoricalData/ocpHistoricalData.test.ts @@ -6,7 +6,13 @@ import { reportActions } from 'store/reports'; import { ocpHistoricalDataStateKey } from './ocpHistoricalDataCommon'; import { ocpHistoricalDataReducer } from './ocpHistoricalDataReducer'; import * as selectors from './ocpHistoricalDataSelectors'; -import { costWidget, cpuUsageWidget, memoryUsageWidget } from './ocpHistoricalDataWidgets'; +import { + costWidget, + cpuUsageWidget, + memoryUsageWidget, + networkUsageWidget, + volumeUsageWidget, +} from './ocpHistoricalDataWidgets'; const createOcpHistoricalDataStore = createMockStoreCreator({ [ocpHistoricalDataStateKey]: ocpHistoricalDataReducer, @@ -21,6 +27,12 @@ beforeEach(() => { test('default state', () => { const store = createOcpHistoricalDataStore(); const state = store.getState(); - expect(selectors.selectCurrentWidgets(state)).toEqual([costWidget.id, cpuUsageWidget.id, memoryUsageWidget.id]); + expect(selectors.selectCurrentWidgets(state)).toEqual([ + costWidget.id, + cpuUsageWidget.id, + memoryUsageWidget.id, + networkUsageWidget.id, + volumeUsageWidget.id, + ]); expect(selectors.selectWidget(state, costWidget.id)).toEqual(costWidget); }); diff --git a/src/store/breakdown/historicalData/ocpHistoricalData/ocpHistoricalDataReducer.ts b/src/store/breakdown/historicalData/ocpHistoricalData/ocpHistoricalDataReducer.ts index 19f738a85..a6e572cfa 100644 --- a/src/store/breakdown/historicalData/ocpHistoricalData/ocpHistoricalDataReducer.ts +++ b/src/store/breakdown/historicalData/ocpHistoricalData/ocpHistoricalDataReducer.ts @@ -1,5 +1,11 @@ import type { OcpHistoricalDataWidget } from './ocpHistoricalDataCommon'; -import { costWidget, cpuUsageWidget, memoryUsageWidget } from './ocpHistoricalDataWidgets'; +import { + costWidget, + cpuUsageWidget, + memoryUsageWidget, + networkUsageWidget, + volumeUsageWidget, +} from './ocpHistoricalDataWidgets'; export type OcpHistoricalDataState = Readonly<{ widgets: Record; @@ -7,11 +13,13 @@ export type OcpHistoricalDataState = Readonly<{ }>; export const defaultState: OcpHistoricalDataState = { - currentWidgets: [costWidget.id, cpuUsageWidget.id, memoryUsageWidget.id], + currentWidgets: [costWidget.id, cpuUsageWidget.id, memoryUsageWidget.id, networkUsageWidget.id, volumeUsageWidget.id], widgets: { [costWidget.id]: costWidget, [cpuUsageWidget.id]: cpuUsageWidget, [memoryUsageWidget.id]: memoryUsageWidget, + [networkUsageWidget.id]: networkUsageWidget, + [volumeUsageWidget.id]: volumeUsageWidget, }, }; diff --git a/src/store/breakdown/historicalData/ocpHistoricalData/ocpHistoricalDataWidgets.ts b/src/store/breakdown/historicalData/ocpHistoricalData/ocpHistoricalDataWidgets.ts index c8c9b9aa1..7ef8f2151 100644 --- a/src/store/breakdown/historicalData/ocpHistoricalData/ocpHistoricalDataWidgets.ts +++ b/src/store/breakdown/historicalData/ocpHistoricalData/ocpHistoricalDataWidgets.ts @@ -29,3 +29,19 @@ export const memoryUsageWidget: OcpHistoricalDataWidget = { reportType: ReportType.memory, type: HistoricalDataWidgetType.usage, }; + +export const networkUsageWidget: OcpHistoricalDataWidget = { + chartName: 'ocpNetworkChart', + id: getId(), + reportPathsType: ReportPathsType.ocp, + reportType: ReportType.network, + type: HistoricalDataWidgetType.network, +}; + +export const volumeUsageWidget: OcpHistoricalDataWidget = { + chartName: 'ocpVolumeChart', + id: getId(), + reportPathsType: ReportPathsType.ocp, + reportType: ReportType.volume, + type: HistoricalDataWidgetType.volume, +};