diff --git a/frontend/src/pages/distributedWorkloads/global/projectMetrics/sections/RequestedResources.tsx b/frontend/src/pages/distributedWorkloads/global/projectMetrics/sections/RequestedResources.tsx index 1a2d1b6ea2..770bb26260 100644 --- a/frontend/src/pages/distributedWorkloads/global/projectMetrics/sections/RequestedResources.tsx +++ b/frontend/src/pages/distributedWorkloads/global/projectMetrics/sections/RequestedResources.tsx @@ -1,145 +1,14 @@ import * as React from 'react'; -import { CardBody, Gallery, GalleryItem, capitalize } from '@patternfly/react-core'; -import { ChartBullet, ChartLegend } from '@patternfly/react-charts'; -import { - chart_color_blue_300 as chartColorBlue300, - chart_color_blue_100 as chartColorBlue100, - chart_color_black_100 as chartColorBlack100, - chart_color_orange_300 as chartColorOrange300, -} from '@patternfly/react-tokens'; +import { CardBody, Gallery, GalleryItem } from '@patternfly/react-core'; import { DistributedWorkloadsContext } from '~/concepts/distributedWorkloads/DistributedWorkloadsContext'; import { getQueueRequestedResources, getTotalSharedQuota, } from '~/concepts/distributedWorkloads/utils'; -import { bytesAsPreciseGiB, roundNumber } from '~/utilities/number'; +import { bytesAsPreciseGiB } from '~/utilities/number'; import EmptyStateErrorMessage from '~/components/EmptyStateErrorMessage'; import { LoadingState } from '~/pages/distributedWorkloads/components/LoadingState'; - -type RequestedResourcesBulletChartProps = { - metricLabel: string; - unitLabel: string; - numRequestedByThisProject: number; - numRequestedByAllProjects: number; - numTotalSharedQuota: number; -}; - -const RequestedResourcesBulletChart: React.FC = ({ - metricLabel, - unitLabel, - numRequestedByThisProject, - numRequestedByAllProjects, - numTotalSharedQuota, -}) => { - const { projectDisplayName } = React.useContext(DistributedWorkloadsContext); - - // Cap things at 110% total quota for display, but show real values in tooltips - const maxDomain = numTotalSharedQuota * 1.1; - // Warn at 150% total quota - const warningThreshold = numTotalSharedQuota * 1.5; - - type CappedBulletChartDataItem = { - name: string; - color: string; - tooltip?: string; // Falls back to `name: preciseValue` if omitted - hideValueInLegend?: boolean; - preciseValue: number; - legendValue: number; - tooltipValue: number; - cappedValue: number; - }; - const getDataItem = ( - args: Omit, - ): CappedBulletChartDataItem => ({ - ...args, - legendValue: roundNumber(args.preciseValue), - tooltipValue: roundNumber(args.preciseValue, 3), - cappedValue: roundNumber(Math.min(args.preciseValue, maxDomain)), - }); - - const requestedByThisProjectData = getDataItem({ - name: `Requested by ${projectDisplayName}`, - color: chartColorBlue300.value, - preciseValue: numRequestedByThisProject, - }); - const requestedByAllProjectsData = getDataItem({ - name: 'Requested by all projects', - color: chartColorBlue100.value, - preciseValue: numRequestedByAllProjects, - }); - const totalSharedQuotaData = getDataItem({ - name: 'Total shared quota', - color: chartColorBlack100.value, - preciseValue: numTotalSharedQuota, - }); - const warningThresholdData = getDataItem({ - name: 'Warning threshold (over 150%)', - color: chartColorOrange300.value, - tooltip: 'Requested resources have surpassed 150%', - hideValueInLegend: true, - preciseValue: warningThreshold, - }); - - const segmentedMeasureData = [requestedByThisProjectData, requestedByAllProjectsData]; - const qualitativeRangeData = [totalSharedQuotaData]; - - const hasWarning = segmentedMeasureData.some( - ({ preciseValue }) => preciseValue > warningThreshold, - ); - const warningMeasureData = hasWarning ? [warningThresholdData] : []; - - const allData = [...segmentedMeasureData, ...qualitativeRangeData, ...warningMeasureData]; - return ( - { - const matchingDataItem = allData.find(({ name }) => name === datum.name); - const { tooltip, name, tooltipValue } = matchingDataItem || {}; - return tooltip || `${name}: ${tooltipValue} ${unitLabel}`; - }} - primarySegmentedMeasureData={segmentedMeasureData.map(({ name, cappedValue }) => ({ - name, - y: cappedValue, - }))} - qualitativeRangeData={qualitativeRangeData.map(({ name, cappedValue }) => ({ - name, - y: cappedValue, - }))} - comparativeWarningMeasureData={warningMeasureData.map(({ name, cappedValue }) => ({ - name, - y: cappedValue, - }))} - maxDomain={{ y: roundNumber(maxDomain) }} - titlePosition="top-left" - legendPosition="bottom-left" - legendOrientation="vertical" - legendComponent={ - ({ - name: hideValueInLegend ? name : `${name}: ${legendValue}`, - }))} - colorScale={allData.map(({ color }) => color)} - gutter={30} - itemsPerRow={3} - rowGutter={0} - /> - } - constrainToVisibleArea - height={250} - width={600} - padding={{ - bottom: 100, // Adjusted to accommodate legend - left: 50, - right: 50, - top: 100, // Adjusted to accommodate labels - }} - /> - ); -}; +import { RequestedResourcesBulletChart } from './RequestedResourcesBulletChart'; export const RequestedResources: React.FC = () => { const { localQueues, clusterQueue } = React.useContext(DistributedWorkloadsContext); @@ -161,7 +30,7 @@ export const RequestedResources: React.FC = () => { return ( - + = ({ + metricLabel, + unitLabel, + numRequestedByThisProject, + numRequestedByAllProjects, + numTotalSharedQuota, +}) => { + const { projectDisplayName } = React.useContext(DistributedWorkloadsContext); + const [width, setWidth] = React.useState(250); + const chartHeight = 250; + const containerRef = React.useRef(null); + const updateWidth = () => { + if (containerRef.current) { + setWidth(containerRef.current.clientWidth); + } + }; + React.useEffect(() => { + if (!containerRef.current) { + return; + } + const resizeObserver = new ResizeObserver(() => { + updateWidth(); + }); + resizeObserver.observe(containerRef.current); + + return () => resizeObserver.disconnect(); + }, []); + + // Cap things at 110% total quota for display, but show real values in tooltips + const maxDomain = numTotalSharedQuota * 1.1; + // Warn at 150% total quota + const warningThreshold = numTotalSharedQuota * 1.5; + + type CappedBulletChartDataItem = { + name: string; + color: string; + tooltip?: string; // Falls back to `name: preciseValue` if omitted + hideValueInLegend?: boolean; + preciseValue: number; + legendValue: number; + tooltipValue: number; + cappedValue: number; + }; + const getDataItem = ( + args: Omit, + ): CappedBulletChartDataItem => ({ + ...args, + legendValue: roundNumber(args.preciseValue), + tooltipValue: roundNumber(args.preciseValue, 3), + cappedValue: roundNumber(Math.min(args.preciseValue, maxDomain)), + }); + + const requestedByThisProjectData = getDataItem({ + name: `Requested by ${projectDisplayName}`, + color: chartColorBlue300.value, + preciseValue: numRequestedByThisProject, + }); + const requestedByAllProjectsData = getDataItem({ + name: 'Requested by all projects', + color: chartColorBlue100.value, + preciseValue: numRequestedByAllProjects, + }); + const totalSharedQuotaData = getDataItem({ + name: 'Total shared quota', + color: chartColorBlack100.value, + preciseValue: numTotalSharedQuota, + }); + const warningThresholdData = getDataItem({ + name: 'Warning threshold (over 150%)', + color: chartColorOrange300.value, + tooltip: 'Requested resources have surpassed 150%', + hideValueInLegend: true, + preciseValue: warningThreshold, + }); + + const segmentedMeasureData = [requestedByThisProjectData, requestedByAllProjectsData]; + const qualitativeRangeData = [totalSharedQuotaData]; + + const hasWarning = segmentedMeasureData.some( + ({ preciseValue }) => preciseValue > warningThreshold, + ); + const warningMeasureData = hasWarning ? [warningThresholdData] : []; + + const allData = [...segmentedMeasureData, ...qualitativeRangeData, ...warningMeasureData]; + return ( +
+ + { + const matchingDataItem = allData.find(({ name }) => name === datum.name); + const { tooltip, name, tooltipValue } = matchingDataItem || {}; + return tooltip || `${name}: ${tooltipValue} ${unitLabel}`; + }} + primarySegmentedMeasureData={segmentedMeasureData.map(({ name, cappedValue }) => ({ + name, + y: cappedValue, + }))} + qualitativeRangeData={qualitativeRangeData.map(({ name, cappedValue }) => ({ + name, + y: cappedValue, + }))} + comparativeWarningMeasureData={warningMeasureData.map(({ name, cappedValue }) => ({ + name, + y: cappedValue, + }))} + maxDomain={{ y: roundNumber(maxDomain) }} + titlePosition="top-left" + legendPosition="bottom-left" + legendOrientation="vertical" + legendComponent={ + ({ + name: hideValueInLegend ? name : `${name}: ${legendValue}`, + }))} + colorScale={allData.map(({ color }) => color)} + gutter={30} + itemsPerRow={3} + rowGutter={0} + /> + } + constrainToVisibleArea + width={width} + padding={{ + bottom: 100, // Adjusted to accommodate legend + left: 50, + right: 50, + top: 100, // Adjusted to accommodate labels + }} + /> + +
+ ); +}; diff --git a/frontend/src/pages/distributedWorkloads/global/projectMetrics/sections/TopResourceConsumingWorkloads.tsx b/frontend/src/pages/distributedWorkloads/global/projectMetrics/sections/TopResourceConsumingWorkloads.tsx index 6ff7fc4d8d..64679df56b 100644 --- a/frontend/src/pages/distributedWorkloads/global/projectMetrics/sections/TopResourceConsumingWorkloads.tsx +++ b/frontend/src/pages/distributedWorkloads/global/projectMetrics/sections/TopResourceConsumingWorkloads.tsx @@ -1,6 +1,12 @@ import * as React from 'react'; import { Card, CardBody, CardTitle, Gallery, GalleryItem } from '@patternfly/react-core'; -import { ChartLegend, ChartLabel, ChartDonut, ChartThemeColor } from '@patternfly/react-charts'; +import { + ChartLegend, + ChartLabel, + ChartDonut, + ChartThemeColor, + ChartTooltip, +} from '@patternfly/react-charts'; import { DistributedWorkloadsContext } from '~/concepts/distributedWorkloads/DistributedWorkloadsContext'; import { TopWorkloadUsageType, @@ -27,55 +33,83 @@ const TopResourceConsumingWorkloadsChart: React.FC num || 0, }) => { + const [extraWidth, setExtraWidth] = React.useState(0); + const chartBaseWidth = 375; + const legendBaseWidth = 260; + const chartHeight = 150; + const containerRef = React.useRef(null); + const updateWidth = () => { + if (containerRef.current) { + setExtraWidth(Math.max(chartBaseWidth, containerRef.current.clientWidth) - chartBaseWidth); + } + }; + React.useEffect(() => { + if (!containerRef.current) { + return; + } + const resizeObserver = new ResizeObserver(() => { + updateWidth(); + }); + resizeObserver.observe(containerRef.current); + + return () => resizeObserver.disconnect(); + }, []); const { topWorkloads, otherUsage, totalUsage } = data; + return ( - ({ - x: getWorkloadName(workload), - y: roundNumber(convertUnits(usage), 3), +
+ } + height={chartHeight} + ariaTitle={`${metricLabel} chart`} + data={ + topWorkloads.length + ? [ + ...topWorkloads.map(({ workload, usage }) => ({ + x: getWorkloadName(workload), + y: roundNumber(convertUnits(usage), 3), + })), + ...(otherUsage + ? [{ x: 'Other', y: roundNumber(convertUnits(otherUsage), 3) }] + : []), + ] + : [{ x: `No workload is consuming ${unitLabel}`, y: 1 }] + } + labels={ + topWorkloads.length + ? ({ datum }) => `${datum.x}: ${datum.y} ${unitLabel}` + : ({ datum }) => datum.x + } + legendComponent={ + ({ + name: truncateString(getWorkloadName(workload), 13 + extraWidth / 15), })), - ...(otherUsage ? [{ x: 'Other', y: roundNumber(convertUnits(otherUsage), 3) }] : []), - ] - : [{ x: `No workload is consuming ${unitLabel}`, y: 1 }] - } - height={150} - labels={ - topWorkloads.length - ? ({ datum }) => `${datum.x}: ${datum.y} ${unitLabel}` - : ({ datum }) => datum.x - } - legendComponent={ - ({ - name: truncateString(getWorkloadName(workload), 16), - })), - ...(otherUsage ? [{ name: 'Other' }] : []), - ]} - gutter={5} - labelComponent={} - itemsPerRow={Math.ceil(topWorkloads.length / 2)} - /> - } - legendOrientation="vertical" - legendPosition="right" - name={`topResourceConsuming${metricLabel}`} - padding={{ - bottom: 0, - left: 0, - right: 260, // Adjusted to accommodate legend - top: 0, - }} - subTitle={unitLabel} - title={String(roundNumber(convertUnits(totalUsage)))} - themeColor={topWorkloads.length ? ChartThemeColor.multi : ChartThemeColor.gray} - width={375} - /> + ...(otherUsage ? [{ name: 'Other' }] : []), + ]} + gutter={15} + labelComponent={} + itemsPerRow={Math.ceil(topWorkloads.length / 2)} + /> + } + legendOrientation="vertical" + legendPosition="right" + name={`topResourceConsuming${metricLabel}`} + padding={{ + bottom: 0, + left: 0, + right: legendBaseWidth + extraWidth, + top: 0, + }} + subTitle={unitLabel} + title={String(roundNumber(convertUnits(totalUsage)))} + themeColor={topWorkloads.length ? ChartThemeColor.multi : ChartThemeColor.gray} + width={chartBaseWidth + extraWidth} + /> +
); }; @@ -118,7 +152,7 @@ export const TopResourceConsumingWorkloads: React.FC = () => { } return ( - + CPU diff --git a/frontend/src/pages/distributedWorkloads/global/workloadStatus/DWStatusOverviewDonutChart.tsx b/frontend/src/pages/distributedWorkloads/global/workloadStatus/DWStatusOverviewDonutChart.tsx index 8ff5b60bb8..0a291ae9fc 100644 --- a/frontend/src/pages/distributedWorkloads/global/workloadStatus/DWStatusOverviewDonutChart.tsx +++ b/frontend/src/pages/distributedWorkloads/global/workloadStatus/DWStatusOverviewDonutChart.tsx @@ -39,44 +39,46 @@ export const DWStatusOverviewDonutChart: React.FC = () => { ); } return ( - ({ - x: statusType, - y: statusCounts[statusType], - }))} - labels={({ datum }) => `${datum.x}: ${datum.y}`} - colorScale={statusTypesIncludedInChart.map( - (statusType) => WorkloadStatusColorAndIcon[statusType].chartColor, - )} - legendComponent={ - ({ - name: `${statusType}: ${statusCounts[statusType]}`, - }))} - colorScale={statusTypesIncludedInLegend.map( - (statusType) => WorkloadStatusColorAndIcon[statusType].chartColor, - )} - gutter={15} - itemsPerRow={Math.ceil(statusTypesIncludedInLegend.length / 2)} - rowGutter={0} - /> - } - legendOrientation="vertical" - legendPosition="right" - name="status-overview" - padding={{ - bottom: 0, - left: 0, - right: 280, // Adjusted to accommodate legend - top: 0, - }} - subTitle="Distributed Workloads" - title={String(workloads.data.length)} - width={450} - height={150} - /> +
+ ({ + x: statusType, + y: statusCounts[statusType], + }))} + labels={({ datum }) => `${datum.x}: ${datum.y}`} + colorScale={statusTypesIncludedInChart.map( + (statusType) => WorkloadStatusColorAndIcon[statusType].chartColor, + )} + legendComponent={ + ({ + name: `${statusType}: ${statusCounts[statusType]}`, + }))} + colorScale={statusTypesIncludedInLegend.map( + (statusType) => WorkloadStatusColorAndIcon[statusType].chartColor, + )} + gutter={15} + itemsPerRow={Math.ceil(statusTypesIncludedInLegend.length / 2)} + rowGutter={0} + /> + } + legendOrientation="vertical" + legendPosition="right" + name="status-overview" + padding={{ + bottom: 0, + left: 0, + right: 280, // Adjusted to accommodate legend + top: 0, + }} + subTitle="Distributed Workloads" + title={String(workloads.data.length)} + width={530} + height={200} + /> +
); };