diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json index 9bd4d1e7c..a5d48cc81 100644 --- a/locales/en/plugin__odf-console.json +++ b/locales/en/plugin__odf-console.json @@ -409,8 +409,12 @@ "Utilization": "Utilization", "Expanding StorageCluster": "Expanding StorageCluster", "Upgrading Data Foundation's Operator": "Upgrading Data Foundation's Operator", - "Used capacity breakdown": "Used capacity breakdown", + "PersistentVolumeClaims": "PersistentVolumeClaims", "This card shows the used capacity for different Kubernetes resources. The figures shown represent the Usable storage, meaning that data replication is not taken into consideration.": "This card shows the used capacity for different Kubernetes resources. The figures shown represent the Usable storage, meaning that data replication is not taken into consideration.", + "Select a namespace:": "Select a namespace:", + "{{pvcNamespace}}": "{{pvcNamespace}}", + "PVCs Namespace Dropdown": "PVCs Namespace Dropdown", + "Only showing PVCs that are being mounted on an active pod": "Only showing PVCs that are being mounted on an active pod", "Internal": "Internal", "Raw capacity is the absolute total disk space available to the array subsystem.": "Raw capacity is the absolute total disk space available to the array subsystem.", "Troubleshoot": "Troubleshoot", @@ -957,6 +961,7 @@ "Only lowercase letters, numbers, non-consecutive periods, or hyphens": "Only lowercase letters, numbers, non-consecutive periods, or hyphens", "Cannot be used before": "Cannot be used before", "Not enough usage data": "Not enough usage data", + "Total requests: ": "Total requests: ", "used": "used", "available": "available", "Other": "Other", @@ -1041,7 +1046,6 @@ "PersistentVolume": "PersistentVolume", "PersistentVolumes": "PersistentVolumes", "PersistentVolumeClaim": "PersistentVolumeClaim", - "PersistentVolumeClaims": "PersistentVolumeClaims", "Namespaces": "Namespaces", "ConfigMap": "ConfigMap", "ConfigMaps": "ConfigMaps", diff --git a/packages/ocs/dashboards/persistent-internal/capacity-breakdown-card/capacity-breakdown-card.scss b/packages/ocs/dashboards/persistent-internal/capacity-breakdown-card/capacity-breakdown-card.scss index 1c1749375..3acf781df 100644 --- a/packages/ocs/dashboards/persistent-internal/capacity-breakdown-card/capacity-breakdown-card.scss +++ b/packages/ocs/dashboards/persistent-internal/capacity-breakdown-card/capacity-breakdown-card.scss @@ -5,14 +5,39 @@ } .ceph-capacity-breakdown-card__header { + border-bottom: var(--pf-global--BorderWidth--sm) solid var(--pf-global--BorderColor--100); display: flex; align-items: center; justify-content: space-between; } .ceph-capacity-breakdown-card-header__dropdown { - max-width: 12em; + max-width: 30em; @media screen and (min-width: $pf-global--breakpoint--lg) { min-width: 12em; } } + +.odf-capacity-breakdown-card-pvc-namespace { + &__header { + display: flex; + align-items: center; + justify-content: end; + padding: var(--pf-global--spacer--md) var(--pf-global--spacer--lg) 0; + } + &__dropdown > ul { + max-height: 300px; + @media screen and (min-width: $pf-global--breakpoint--lg) { + min-width: 12em; + } + max-width: 30em; + overflow: auto; + } + &__title { + padding-right: var(--pf-global--spacer--lg); + } +} + +.odf-capacity-breakdown-card-pvc-description { + font-size: var(--pf-global--FontSize--xs); +} diff --git a/packages/ocs/dashboards/persistent-internal/capacity-breakdown-card/capacity-breakdown-card.tsx b/packages/ocs/dashboards/persistent-internal/capacity-breakdown-card/capacity-breakdown-card.tsx index 7266fa2d3..d247c3446 100644 --- a/packages/ocs/dashboards/persistent-internal/capacity-breakdown-card/capacity-breakdown-card.tsx +++ b/packages/ocs/dashboards/persistent-internal/capacity-breakdown-card/capacity-breakdown-card.tsx @@ -1,12 +1,18 @@ import * as React from 'react'; import { BreakdownCardBody } from '@odf/shared/dashboards/breakdown-card/breakdown-body'; -import { getSelectOptions } from '@odf/shared/dashboards/breakdown-card/breakdown-dropdown'; +import { + getPVCSelectOptions, + getSelectOptions, +} from '@odf/shared/dashboards/breakdown-card/breakdown-dropdown'; import { FieldLevelHelp } from '@odf/shared/generic/FieldLevelHelp'; +import { Loading } from '@odf/shared/generic/status-box'; import { useCustomPrometheusPoll, usePrometheusBasePath, } from '@odf/shared/hooks/custom-prometheus-poll'; import { BreakdownCardFields } from '@odf/shared/queries'; +import { useK8sList } from '@odf/shared/hooks/useK8sList'; +import { NamespaceModel } from '@odf/shared/models'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; import { getInstantVectorStats, @@ -22,7 +28,7 @@ import { CardTitle, } from '@patternfly/react-core'; import { - breakdownQueryMapCEPH, + getBreakdownMetricsQuery, CEPH_CAPACITY_BREAKDOWN_QUERIES, StorageDashboardQuery, } from '../../../queries/ceph-storage'; @@ -49,8 +55,20 @@ const BreakdownCard: React.FC = () => { BreakdownCardFields.PROJECTS ); const [isOpenBreakdownSelect, setBreakdownSelect] = React.useState(false); + const [isNamespaceSelectOpen, setNamespaceSelect] = React.useState(false); + const [pvcNamespace, setPVCNamespace] = React.useState(''); - const { queries, model, metric } = breakdownQueryMapCEPH[metricType]; + const [allNamespace, allNamespaceLoaded, allNamespaceError] = + useK8sList(NamespaceModel); + React.useEffect( + () => setPVCNamespace(allNamespace?.sort()[0]?.metadata?.name || ''), + [allNamespace] + ); + + const { queries, model, metric } = React.useMemo( + () => getBreakdownMetricsQuery(metricType, pvcNamespace), + [metricType, pvcNamespace] + ); const [modelByUsed, modelUsedError, modelUsedLoading] = useCustomPrometheusPoll({ @@ -86,6 +104,11 @@ const BreakdownCard: React.FC = () => { setBreakdownSelect(!isOpenBreakdownSelect); }; + const handleNamespaceChange: SelectProps['onSelect'] = (_e, namespace) => { + setPVCNamespace(namespace as any); + setNamespaceSelect(!isNamespaceSelectOpen); + }; + const dropdownOptions = [ { name: t('Projects'), @@ -99,15 +122,19 @@ const BreakdownCard: React.FC = () => { name: t('Pods'), id: BreakdownCardFields.PODS, }, + { + name: t('PersistentVolumeClaims'), + id: PVCs, + }, ]; const breakdownSelectItems = getSelectOptions(dropdownOptions); return ( - - {t('Used capacity breakdown')} - + + {t('Requested capacity')} + {t( 'This card shows the used capacity for different Kubernetes resources. The figures shown represent the Usable storage, meaning that data replication is not taken into consideration.' )} @@ -125,10 +152,42 @@ const BreakdownCard: React.FC = () => { })} aria-label={t('Break By Dropdown')} isCheckboxSelectionBadgeHidden + id="breakdown-card-metrics-dropdown" > {breakdownSelectItems} + {metricType === PVCs && ( +
+ {allNamespaceLoaded && !allNamespaceError ? ( + <> +
+ {t('Select a namespace:')} +
+ + + + ) : ( + + )} +
+ )} { capacityUsed={cephUsed} metricModel={model} humanize={humanize} + isOfBlockAndFileType={true} /> + {metricType === PVCs && + !queriesLoadError && + top5MetricsStats.length > 0 && + cephUsed && ( + + {t('Only showing PVCs that are being mounted on an active pod')} + + )}
); }; diff --git a/packages/ocs/queries/ceph-storage.ts b/packages/ocs/queries/ceph-storage.ts index ec3e38ffb..985d36de0 100644 --- a/packages/ocs/queries/ceph-storage.ts +++ b/packages/ocs/queries/ceph-storage.ts @@ -122,6 +122,24 @@ export const breakdownQueryMapCEPH: BreakdownCardQueryMap = { }, }; +export const getBreakdownMetricsQuery = ( + metricType: string, + namespace?: string +): MetricsQueryProps => { + if (metricType === PVCs && namespace) { + const queries = { + [StorageDashboardQuery.PVC_NAMESPACES_BY_USED]: `sum by (namespace, persistentvolumeclaim) (kubelet_volume_stats_used_bytes{namespace='${namespace}'} * on (namespace, persistentvolumeclaim) group_left(storageclass, provisioner) (kube_persistentvolumeclaim_info * on (storageclass) group_left(provisioner) kube_storageclass_info {provisioner=~"(.*rbd.csi.ceph.com)|(.*cephfs.csi.ceph.com)|(ceph.rook.io/block)"}))`, + [StorageDashboardQuery.PVC_NAMESPACES_TOTAL_USED]: `sum(sum by (namespace, persistentvolumeclaim) (kubelet_volume_stats_used_bytes{namespace='${namespace}'} * on (namespace, persistentvolumeclaim) group_left(storageclass, provisioner) (kube_persistentvolumeclaim_info * on (storageclass) group_left(provisioner) kube_storageclass_info {provisioner=~"(.*rbd.csi.ceph.com)|(.*cephfs.csi.ceph.com)|(ceph.rook.io/block)"})))`, + }; + return { + model: PersistentVolumeClaimModel, + metric: 'persistentvolumeclaim', + queries, + }; + } + return breakdownQueryMapCEPH[metricType]; +}; + export const CAPACITY_INFO_QUERIES = { [StorageDashboardQuery.RAW_CAPACITY_TOTAL]: 'ceph_cluster_total_bytes', [StorageDashboardQuery.RAW_CAPACITY_USED]: @@ -265,3 +283,44 @@ export const breakdownIndependentQueryMap: BreakdownCardQueryMap = { }, }, }; + +type projectQueryProps = { + metric: 'namespace'; + queries: { + [StorageDashboardQuery.PROJECTS_BY_USED]: string; + [StorageDashboardQuery.PROJECTS_TOTAL_USED]: string; + [StorageDashboardQuery.CEPH_CAPACITY_USED]: string; + }; +}; +type storageClassQueryProps = { + metric: 'storageclass'; + queries: { + [StorageDashboardQuery.STORAGE_CLASSES_BY_USED]: string; + [StorageDashboardQuery.STORAGE_CLASSES_TOTAL_USED]: string; + [StorageDashboardQuery.CEPH_CAPACITY_USED]: string; + }; +}; +type podsQueryProps = { + metric: 'pod'; + queries: { + [StorageDashboardQuery.PODS_BY_USED]: string; + [StorageDashboardQuery.PODS_TOTAL_USED]: string; + [StorageDashboardQuery.CEPH_CAPACITY_USED]: string; + }; +}; +type pvcQueryProps = { + metric: 'persistentvolumeclaim'; + queries: { + [StorageDashboardQuery.PVC_NAMESPACES_BY_USED]: string; + [StorageDashboardQuery.PVC_NAMESPACES_TOTAL_USED]: string; + }; +}; + +type MetricsQueryProps = { + model: K8sModel; +} & ( + | projectQueryProps + | podsQueryProps + | storageClassQueryProps + | pvcQueryProps +); diff --git a/packages/shared/dashboards/breakdown-card/breakdown-body.tsx b/packages/shared/dashboards/breakdown-card/breakdown-body.tsx index 28590f915..ec73825d7 100644 --- a/packages/shared/dashboards/breakdown-card/breakdown-body.tsx +++ b/packages/shared/dashboards/breakdown-card/breakdown-body.tsx @@ -19,6 +19,7 @@ export const BreakdownCardBody: React.FC = ({ hasLoadError, ocsVersion = '', labelPadding, + isOfBlockAndFileType, }) => { const { t } = useCustomTranslation(); @@ -59,8 +60,10 @@ export const BreakdownCardBody: React.FC = ({ @@ -68,7 +71,7 @@ export const BreakdownCardBody: React.FC = ({ {capacityAvailable && ( )} @@ -97,4 +100,5 @@ export type BreakdownBodyProps = { humanize: Humanize; ocsVersion?: string; labelPadding?: LabelPadding; + isOfBlockAndFileType?: boolean; }; diff --git a/packages/shared/dashboards/breakdown-card/breakdown-capacity.tsx b/packages/shared/dashboards/breakdown-card/breakdown-capacity.tsx index e72cd98af..a12d1c136 100644 --- a/packages/shared/dashboards/breakdown-card/breakdown-capacity.tsx +++ b/packages/shared/dashboards/breakdown-card/breakdown-capacity.tsx @@ -4,8 +4,10 @@ import './breakdown-card.scss'; export const TotalCapacityBody: React.FC = ({ capacity, + prefix, suffix, className, + makeCapacityInBold, }) => { return (

= ({ className )} > - {capacity} {suffix} + {prefix} {makeCapacityInBold ? {capacity} : capacity}{' '} + {suffix}

); }; type TotalCapacityBodyProps = { capacity: string; - suffix: string; + prefix?: string; + suffix?: string; className?: string; + makeCapacityInBold?: boolean; }; diff --git a/packages/shared/dashboards/breakdown-card/breakdown-dropdown.tsx b/packages/shared/dashboards/breakdown-card/breakdown-dropdown.tsx index 8e0856cac..2121c9796 100644 --- a/packages/shared/dashboards/breakdown-card/breakdown-dropdown.tsx +++ b/packages/shared/dashboards/breakdown-card/breakdown-dropdown.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; import { SelectOption, SelectGroup, @@ -20,6 +21,18 @@ export const getSelectOptions = ( )); +export const getPVCSelectOptions = ( + selectItems: K8sResourceCommon[] +): React.ReactElement[] => + selectItems.map((namespace) => { + const { name } = namespace.metadata; + return ( + + {name} + + ); + }); + export const getGroupedSelectOptions = ( groupedSelectItems: GroupedSelectItems ): React.ReactElement[] =>