diff --git a/src/pages/report/CostEstimationModal.tsx b/src/pages/report/CostEstimationModal.tsx new file mode 100644 index 00000000..a7f25ecd --- /dev/null +++ b/src/pages/report/CostEstimationModal.tsx @@ -0,0 +1,441 @@ +import React, { useCallback, useEffect, useState } from 'react'; + +import { + Button, + Content, + Divider, + Form, + FormGroup, + FormSelect, + FormSelectOption, + Grid, + GridItem, + Popover, + Split, + SplitItem, + Stack, + StackItem, + TextInput, + Title, +} from '@patternfly/react-core'; +import { Modal, ModalVariant } from '@patternfly/react-core/deprecated'; +import { HelpIcon } from '@patternfly/react-icons'; + +interface CostEstimationModalProps { + vCPUs: number; + isOpen: boolean; + onClose: () => void; +} + +type CPURatioOption = '1:1' | '1:2' | '1:4' | '1:6' | '1:8' | '1:10'; +type OpenShiftPlatform = 'OpenShift Container Platform' | 'OpenShift Virtualization Engine'; +type SLAOption = '24/7' | '8x5'; + +// Helper function to calculate effective cores needed +const calculateEffectiveCores = ( + vCPUs: number, + cpuRatio: CPURatioOption +): number => { + if (vCPUs === 0) { + return 0; + } + const ratioDenominator = parseInt(cpuRatio.split(':')[1], 10); + return vCPUs / ratioDenominator; +}; + +// Helper function to calculate number of subscriptions/servers needed +const calculateSubscriptionsNeeded = ( + vCPUs: number, + cpuRatio: CPURatioOption, + nodeCpuCores: number, + nodeSockets: number +): number => { + if (vCPUs === 0) { + return 0; + } + + const cores = calculateEffectiveCores(vCPUs, cpuRatio); + const coresPerNode = nodeSockets * nodeCpuCores; + return Math.ceil(cores / coresPerNode); +}; + +// Helper function to calculate nodes required (decimal) +const calculateNodesRequired = ( + vCPUs: number, + cpuRatio: CPURatioOption, + nodeCpuCores: number, + nodeSockets: number +): number => { + if (vCPUs === 0) { + return 0; + } + const cores = calculateEffectiveCores(vCPUs, cpuRatio); + const coresPerNode = nodeSockets * nodeCpuCores; + return cores / coresPerNode; +}; + +// Helper function to calculate node utilization percentages +const calculateNodeUtilization = ( + vCPUs: number, + cpuRatio: CPURatioOption, + nodeCpuCores: number, + nodeSockets: number +): number[] => { + const cores = calculateEffectiveCores(vCPUs, cpuRatio); + const coresPerNode = nodeSockets * nodeCpuCores; + const nodesRequired = Math.ceil(cores / coresPerNode); + + const utilizations: number[] = []; + for (let i = 0; i < nodesRequired; i++) { + const coresUsed = Math.min(cores - i * coresPerNode, coresPerNode); + const utilization = (coresUsed / coresPerNode) * 100; + utilizations.push(utilization); + } + + return utilizations; +}; + +const CostEstimationModal: React.FC = ({ vCPUs: initialVCPUs, isOpen, onClose }) => { + const [vCPUs, setVCPUs] = useState(initialVCPUs); + const [cpuRatio, setCpuRatio] = useState('1:4'); + const [nodeCpuCores, setNodeCpuCores] = useState(32); + const [nodeSockets, setNodeSockets] = useState(2); + const [openShiftPlatform, setOpenShiftPlatform] = useState('OpenShift Virtualization Engine'); + const [sla, setSLA] = useState('24/7'); + const [estimatedCost, setEstimatedCost] = useState(0); + const [subscriptionsNeeded, setSubscriptionsNeeded] = useState(0); + const [effectiveCores, setEffectiveCores] = useState(0); + const [nodesRequired, setNodesRequired] = useState(0); + const [nodeUtilizations, setNodeUtilizations] = useState([]); + + // Calculate all values whenever any field changes + const calculateAll = useCallback((): void => { + const subscriptions = calculateSubscriptionsNeeded(vCPUs, cpuRatio, nodeCpuCores, nodeSockets); + const cores = calculateEffectiveCores(vCPUs, cpuRatio); + const nodes = calculateNodesRequired(vCPUs, cpuRatio, nodeCpuCores, nodeSockets); + const utilizations = calculateNodeUtilization(vCPUs, cpuRatio, nodeCpuCores, nodeSockets); + + setSubscriptionsNeeded(subscriptions); + setEffectiveCores(cores); + setNodesRequired(nodes); + setNodeUtilizations(utilizations); + + if (subscriptions === 0) { + setEstimatedCost(0); + return; + } + + let monthlyCost = 0; + if (openShiftPlatform === 'OpenShift Container Platform') { + monthlyCost = subscriptions * 900; + } else { + monthlyCost = subscriptions * 200; + } + if (sla === '24/7') { + monthlyCost = monthlyCost * 1.5; + } + + const annualCost = monthlyCost * 12; + setEstimatedCost(annualCost); + }, [vCPUs, cpuRatio, nodeCpuCores, nodeSockets, openShiftPlatform, sla]); + + useEffect(() => { + setVCPUs(initialVCPUs); + }, [initialVCPUs]); + + useEffect(() => { + calculateAll(); + }, [calculateAll]); + + const handleClose = (): void => { + // Reset form + setVCPUs(initialVCPUs); + setCpuRatio('1:4'); + setNodeCpuCores(32); + setNodeSockets(2); + setOpenShiftPlatform('OpenShift Virtualization Engine'); + setSLA('24/7'); + setEstimatedCost(0); + setSubscriptionsNeeded(0); + setEffectiveCores(0); + setNodesRequired(0); + setNodeUtilizations([]); + onClose(); + }; + + const coresPerNode = nodeSockets * nodeCpuCores; + const nodesRequiredRounded = Math.ceil(nodesRequired); + const perVCPUCost = vCPUs > 0 ? estimatedCost / vCPUs : 0; + + const actions = [ + , + ]; + + return ( + + + {/* Left Column - Configuration */} + + + + Configuration + + + + Workload Volume +
+ + + + } + > + setVCPUs(parseInt(value) || 0)} + min={0} + /> + + + + + + } + > + setCpuRatio(value as CPURatioOption)} + id="cpu-ratio" + name="cpu-ratio" + > + + + + + + + + +
+
+ + + Node Hardware +
+ + setNodeCpuCores(Number(value))} + id="node-cpu-cores" + name="node-cpu-cores" + > + + + + + + + + + + + + setNodeSockets(Number(value))} + id="node-sockets" + name="node-sockets" + > + + + + + + + + (Total {coresPerNode} cores per node) + +
+
+ + + Software & Support +
+ + setOpenShiftPlatform(value as OpenShiftPlatform)} + id="openshift-platform" + name="openshift-platform" + > + + + + + + + setSLA(value as SLAOption)} + id="sla" + name="sla" + > + + + + +
+
+
+
+ + {/* Right Column - Estimation & Insights */} + + + + Estimation & Insights + + + + Estimated Annual Cost + + ${estimatedCost.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + + + ~${perVCPUCost.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} per vCPU/year + + + + + + + + + How it's calculated: + + + + Total vCPUs: + + {vCPUs.toLocaleString('en-US')} + + + + + Effective Cores Needed: + + {Math.ceil(effectiveCores).toLocaleString('en-US')} + + + + + Capacity per Node: + + {coresPerNode} cores + + + + + Nodes Required: + + + {nodesRequired.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} (Rounded to {nodesRequiredRounded}) + + + + + + + + Node Utilization +
+ {nodeUtilizations.map((utilization, index) => ( +
+
+
+
+ + Node {index + 1} + + + {utilization === 100 ? 'Full' : `${Math.round(utilization)}% Full`} + +
+ ))} +
+ + You need {subscriptionsNeeded} Socket-pair subscription{subscriptionsNeeded !== 1 ? 's' : ''} + + + + + + + ); +}; + +export default CostEstimationModal; diff --git a/src/pages/report/Report.tsx b/src/pages/report/Report.tsx index d44bd4b9..96f3b42f 100644 --- a/src/pages/report/Report.tsx +++ b/src/pages/report/Report.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Link, useParams } from 'react-router-dom'; import { useMount } from 'react-use'; @@ -28,6 +28,9 @@ import { openAssistedInstaller } from '../assessment/utils/functions'; import { parseLatestSnapshot } from '../assessment/utils/snapshotParser'; import { Dashboard } from './assessment-report/Dashboard'; +import CostEstimationModal from './CostEstimationModal'; + +; export type SnapshotLike = { // New preferred structure @@ -36,6 +39,7 @@ export type SnapshotLike = { // Backward-compatible fields infra?: Infra; vms?: VMs; + cpuCores?: number; inventory?: { infra?: Infra; // legacy vms?: VMs; // legacy @@ -57,6 +61,16 @@ const Inner: React.FC = () => { const { id } = useParams<{ id: string }>(); const discoverySourcesContext = useDiscoverySources(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleOpenModal = (): void => { + setIsModalOpen(true); + }; + + const handleCloseModal = (): void => { + setIsModalOpen(false); + } + useMount(async () => { if ( !discoverySourcesContext.assessments || @@ -203,6 +217,9 @@ const Inner: React.FC = () => { + ) : null}
@@ -219,6 +236,12 @@ const Inner: React.FC = () => { )} + + ); };