diff --git a/frontend/packages/console-shared/src/components/dashboard/resource-quota-card/AppliedClusterResourceQuotaItem.tsx b/frontend/packages/console-shared/src/components/dashboard/resource-quota-card/AppliedClusterResourceQuotaItem.tsx new file mode 100644 index 000000000000..c5dc082b9289 --- /dev/null +++ b/frontend/packages/console-shared/src/components/dashboard/resource-quota-card/AppliedClusterResourceQuotaItem.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { + getQuotaResourceTypes, + QuotaScopesInline, + QuotaGaugeCharts, +} from '@console/internal/components/resource-quota'; +import { ResourceLink } from '@console/internal/components/utils/resource-link'; +import { AppliedClusterResourceQuotaModel } from '@console/internal/models'; +import { referenceForModel, K8sResourceKind } from '@console/internal/module/k8s'; + +import './resource-quota-card.scss'; + +const AppliedClusterResourceQuotaItem: React.FC = ({ + resourceQuota, + namespace, +}) => { + const resourceTypes = getQuotaResourceTypes(resourceQuota); + const scopes = _.get(resourceQuota, 'spec.scopes'); + return ( + <> +
+ + {scopes && } +
+ + + ); +}; + +export default AppliedClusterResourceQuotaItem; + +type AppliedClusterResourceQuotaItemProps = { + resourceQuota: K8sResourceKind; + namespace: string; +}; diff --git a/frontend/public/components/dashboard/project-dashboard/inventory-card.tsx b/frontend/public/components/dashboard/project-dashboard/inventory-card.tsx index 9c4d30cf1fa2..e9ac4930a3f2 100644 --- a/frontend/public/components/dashboard/project-dashboard/inventory-card.tsx +++ b/frontend/public/components/dashboard/project-dashboard/inventory-card.tsx @@ -7,7 +7,6 @@ import DashboardCardBody from '@console/shared/src/components/dashboard/dashboar import DashboardCardHeader from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardHeader'; import DashboardCardTitle from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardTitle'; import { - AppliedClusterResourceQuotaModel, PodModel, DeploymentModel, DeploymentConfigModel, @@ -164,7 +163,6 @@ export const InventoryCard = () => { {canListSecrets && } - {itemExtensions.map((item) => ( ({ kind: ResourceQuotaModel.kind, @@ -21,6 +23,13 @@ const getResourceQuota = (namespace: string) => ({ prop: 'resourceQuotas', }); +const getAppliedClusterResourceQuota = (namespace: string) => ({ + kind: referenceForModel(AppliedClusterResourceQuotaModel), + namespace, + isList: true, + prop: 'appliedClusterResourceQuotas', +}); + export const ResourceQuotaCard = withDashboardResources( ({ watchK8sResource, stopWatchK8sResource, resources }: DashboardItemProps) => { const { obj } = React.useContext(ProjectDashboardContext); @@ -29,10 +38,24 @@ export const ResourceQuotaCard = withDashboardResources( watchK8sResource(resourceQuota); return () => stopWatchK8sResource(resourceQuota); }, [obj.metadata.name, watchK8sResource, stopWatchK8sResource]); + React.useEffect(() => { + const appliedClusterResourceQuota = getAppliedClusterResourceQuota(obj.metadata.name); + watchK8sResource(appliedClusterResourceQuota); + return () => stopWatchK8sResource(appliedClusterResourceQuota); + }, [obj.metadata.name, watchK8sResource, stopWatchK8sResource]); const quotas = _.get(resources.resourceQuotas, 'data', []) as FirehoseResult['data']; - const loaded = _.get(resources.resourceQuotas, 'loaded'); - const error = _.get(resources.resourceQuotas, 'loadError'); + const clusterQuotas = _.get( + resources.appliedClusterResourceQuotas, + 'data', + [], + ) as FirehoseResult['data']; + const loaded = + _.get(resources.resourceQuotas, 'loaded') && + _.get(resources.appliedClusterResourceQuotas, 'loaded'); + const error = + _.get(resources.resourceQuotas, 'loadError') || + _.get(resources.appliedClusterResourceQuotas, 'loadError'); const { t } = useTranslation(); return ( @@ -47,6 +70,16 @@ export const ResourceQuotaCard = withDashboardResources( .map((rq) => ( ))} + {clusterQuotas && + clusterQuotas + .filter((rq) => hasComputeResources(getQuotaResourceTypes(rq))) + .map((rq) => ( + + ))} diff --git a/frontend/public/components/default-resource.tsx b/frontend/public/components/default-resource.tsx index 394bcea366c6..83a5efb7c48e 100644 --- a/frontend/public/components/default-resource.tsx +++ b/frontend/public/components/default-resource.tsx @@ -8,7 +8,6 @@ import { Conditions } from './conditions'; import { DetailsPage, ListPage, Table, TableData, TableProps, RowFunctionArgs } from './factory'; import { referenceFor, - referenceForModel, kindForReference, K8sResourceKind, modelFor, @@ -24,7 +23,6 @@ import { SectionHeading, Timestamp, } from './utils'; -import { AppliedClusterResourceQuotaModel } from '../models'; const { common } = Kebab.factory; @@ -72,12 +70,10 @@ export const DetailsForKind = ( ); }; -export const DefaultList: React.FC = ( - props, -) => { +export const DefaultList: React.FC = (props) => { const { t } = useTranslation(); - const { kinds, namespace } = props; + const { kinds } = props; const TableHeader = () => { return [ @@ -136,36 +132,6 @@ export const DefaultList: React.FC> = ({ obj, customData }) => { - const kind = referenceFor(obj) || customData.kind; - const menuActions = [...Kebab.getExtensionsActionsForKind(kindObj(kind)), ...common]; - - return ( - <> - - - - - {customData.namespace ? ( - - ) : ( - t('public~None') - )} - - - - - - - - - ); - }; - const getAriaLabel = (item) => { const model = modelFor(item); // API discovery happens asynchronously. Avoid runtime errors if the model hasn't loaded. @@ -182,26 +148,7 @@ export const DefaultList: React.FC ({ - kind: kinds[0], - namespace, - }), - [kinds, namespace], - ); - - const appliedClusterQuotaReference = referenceForModel(AppliedClusterResourceQuotaModel); - const isACRQ = kinds[0] === appliedClusterQuotaReference; - return isACRQ ? ( - - ) : ( + return (
; }; - DefaultDetailsPage.displayName = 'DefaultDetailsPage'; diff --git a/frontend/public/components/resource-pages.ts b/frontend/public/components/resource-pages.ts index 1ec5e0762885..8a3057422bd7 100644 --- a/frontend/public/components/resource-pages.ts +++ b/frontend/public/components/resource-pages.ts @@ -525,6 +525,11 @@ export const baseListPages = ImmutableMap() (m) => m.ResourceQuotasPage, ), ) + .set(referenceForModel(AppliedClusterResourceQuotaModel), () => + import('./resource-quota' /* webpackChunkName: "resource-quota" */).then( + (m) => m.AppliedClusterResourceQuotasPage, + ), + ) .set(referenceForModel(LimitRangeModel), () => import('./limit-range' /* webpackChunkName: "limit-range" */).then((m) => m.LimitRangeListPage), ) diff --git a/frontend/public/components/resource-quota.jsx b/frontend/public/components/resource-quota.jsx index a090c3b93a57..6f63bf131f0d 100644 --- a/frontend/public/components/resource-quota.jsx +++ b/frontend/public/components/resource-quota.jsx @@ -22,6 +22,7 @@ import { ResourceSummary, convertToBaseValue, FieldLevelHelp, + useAccessReview, } from './utils'; import { connectToFlags } from '../reducers/connectToFlags'; import { flagPending } from '../reducers/features'; @@ -83,10 +84,10 @@ const getResourceUsage = (quota, resourceType, namespace, isACRQ = false) => { // It has cluster-scoped data as well as namespaced data // "used" is the data for the current namespace and "totalUsed" is the cluster-scoped data if (isACRQ) { - used = _.get(quota, ['status', 'namespaces']); + const allNamespaceData = _.get(quota, ['status', 'namespaces']); if (namespace !== undefined) { - used = used.filter((ns) => ns.namespace === namespace); - used = _.get(used[0], ['status', 'used', resourceType]); + const currentNamespaceData = allNamespaceData.filter((ns) => ns.namespace === namespace); + used = _.get(currentNamespaceData[0], ['status', 'used', resourceType]); } totalUsed = _.get(quota, ['status', 'total', 'used', resourceType]); } else { @@ -173,18 +174,30 @@ const NoQuotaGuage = ({ title, className }) => { ); }; -export const QuotaGaugeCharts = ({ quota, resourceTypes, chartClassName = null }) => { +export const QuotaGaugeCharts = ({ + quota, + resourceTypes, + chartClassName = null, + namespace = undefined, +}) => { + const kind = quotaKind(quota); + const isACRQ = kind === appliedClusterQuotaReference; const resourceTypesSet = new Set(resourceTypes); const cpuRequestUsagePercent = getResourceUsage( quota, resourceTypesSet.has('requests.cpu') ? 'requests.cpu' : 'cpu', + namespace, + isACRQ, ).percent; - const cpuLimitUsagePercent = getResourceUsage(quota, 'limits.cpu').percent; + const cpuLimitUsagePercent = getResourceUsage(quota, 'limits.cpu', namespace, isACRQ).percent; const memoryRequestUsagePercent = getResourceUsage( quota, resourceTypesSet.has('requests.memory') ? 'requests.memory' : 'memory', + namespace, + isACRQ, ).percent; - const memoryLimitUsagePercent = getResourceUsage(quota, 'limits.memory').percent; + const memoryLimitUsagePercent = getResourceUsage(quota, 'limits.memory', namespace, isACRQ) + .percent; const { t } = useTranslation(); return (
@@ -328,15 +341,31 @@ const Details = ({ obj: rq, match }) => { default: text = t('public~ResourceQuota details'); } + const canListCRQ = useAccessReview({ + group: ClusterResourceQuotaModel.apiGroup, + resource: ClusterResourceQuotaModel.plural, + verb: 'list', + }); return ( <>
- {showChartRow && } + {showChartRow && ( + + )}
- + + {canListCRQ && ( + <> +
{t('public~ClusterResourceQuota')}
+
+ +
+ + )} +
{scopes && (
@@ -495,17 +524,20 @@ export const ResourceQuotasPage = connectToFlags(FLAGS.OPENSHIFT)( return ; } if (flags[FLAGS.OPENSHIFT]) { - resources.push({ - kind: referenceForModel(ClusterResourceQuotaModel), - namespaced: false, - optional: true, - }); - resources.push({ - kind: referenceForModel(AppliedClusterResourceQuotaModel), - namespaced: true, - namespace, - optional: true, - }); + if (namespace === undefined) { + resources.push({ + kind: referenceForModel(ClusterResourceQuotaModel), + namespaced: false, + optional: true, + }); + } else { + resources.push({ + kind: referenceForModel(AppliedClusterResourceQuotaModel), + namespaced: true, + namespace, + optional: true, + }); + } rowFilters = [ { @@ -554,6 +586,37 @@ export const ResourceQuotasPage = connectToFlags(FLAGS.OPENSHIFT)( }, ); +export const AppliedClusterResourceQuotasPage = connectToFlags(FLAGS.OPENSHIFT)( + ({ namespace, flags, mock, showTitle }) => { + const { t } = useTranslation(); + const resources = [ + { + kind: referenceForModel(AppliedClusterResourceQuotaModel), + namespaced: true, + namespace, + optional: true, + }, + ]; + + if (flagPending(flags[FLAGS.OPENSHIFT])) { + return ; + } + + return ( + + ); + }, +); + export const ResourceQuotasDetailsPage = (props) => { return (