Skip to content

Commit

Permalink
feat: create Resource Quotas views (#3336)
Browse files Browse the repository at this point in the history
* add resource quotas views

* add tests

* change list to ts, fix deps

* add readonly

* adjust test

* adjust small list and spec

* cleanup

* review adjustments
  • Loading branch information
OliwiaGowor authored Sep 12, 2024
1 parent ae319ec commit 6cc72c6
Show file tree
Hide file tree
Showing 16 changed files with 443 additions and 32 deletions.
11 changes: 11 additions & 0 deletions public/i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,13 @@ command-palette:
gateways: Gateways
horizontalpodautoscalers: Horizontal Pod Autoscalers
kyma: Modules
limitranges: Limit Ranges
networkpolicies: Network Policies
oauth2clients: OAuth2 Clients
persistentvolumeclaims: Persistent Volume Claims
persistentvolumes: Persistent Volumes
replicasets: Replica Sets
resourcequotas: Resource Quotas
rolebindings: Role Bindings
serviceaccounts: Service Accounts
serviceentries: Service Entries
Expand Down Expand Up @@ -1004,13 +1006,22 @@ resource-graph:
save-as-dot: DOT format
title: Resource Graph
resource-quotas:
description: A <0>Resource Quota</0> provides constraints that limit aggregate resource consumption per namespace.
headers:
limits:
memory: Memory Limits
cpu: CPU Limits
requests:
memory: Memory Requests
cpu: CPU Requests
scopes: Scopes
scope-selectors: Scope Selectors
limits-usage: Limits and Usage
resource: Resource
hard: Hard
used: Used
operator: Operator
values: Values
title: Resource Quotas
name_singular: Resource Quota
role-bindings:
Expand Down
1 change: 0 additions & 1 deletion src/resources/LimitRanges/LimitRangeSpecification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,6 @@ export default function LimitRangeSpecification({
) : (
<UI5Panel
title={t('limit-ranges.headers.limits')}
headerActions={null}
className={'limit-range-spec'}
>
{transLimits.map((limit: { type: string; props: FlatLimitProps[] }) => {
Expand Down
6 changes: 2 additions & 4 deletions src/resources/Namespaces/NamespaceDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ResourceDetails } from 'shared/components/ResourceDetails/ResourceDetai
import { EventsList } from 'shared/components/EventsList';
import { EVENT_MESSAGE_TYPE } from 'hooks/useMessageList';
import { LimitRangesList } from 'resources/LimitRanges/LimitRangesList';
import { ResourceQuotaList as ResourceQuotaListComponent } from 'resources/ResourceQuotas/ResourceQuotaList';
import { ResourceQuotasList as ResourceQuotaListComponent } from 'resources/ResourceQuotas/ResourceQuotasList';
import { showYamlUploadDialogState } from 'state/showYamlUploadDialogAtom';

import { NamespaceStatus } from './NamespaceStatus';
Expand All @@ -32,19 +32,17 @@ export function NamespaceDetails(props) {
namespace: props.resourceName,
isCompact: true,
showTitle: true,
disableCreate: true,
};

const LimitrangesList = <LimitRangesList {...limitRangesParams} />;

const resourceQuotasParams = {
hasDetailsView: false,
hasDetailsView: true,
resourceUrl: `/api/v1/namespaces/${props.resourceName}/resourcequotas`,
resourceType: 'ResourceQuotas',
namespace: props.resourceName,
isCompact: true,
showTitle: true,
disableCreate: true,
};

const ResourceQuotasList = (
Expand Down
12 changes: 8 additions & 4 deletions src/resources/ResourceQuotas/ResourceQuotaCreate.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useRecoilValue } from 'recoil';
import * as _ from 'lodash';
Expand All @@ -8,7 +8,7 @@ import { activeNamespaceIdState } from 'state/activeNamespaceIdAtom';

import { createResourceQuotaTemplate } from './templates';

export function ResourceQuotaCreate({
export default function ResourceQuotaCreate({
formElementRef,
onChange,
setCustomValid,
Expand All @@ -17,6 +17,7 @@ export function ResourceQuotaCreate({

...props
}) {
const { t } = useTranslation();
const namespaceId = useRecoilValue(activeNamespaceIdState);
const [resourceQuota, setResourceQuota] = useState(
_.cloneDeep(initialResourceQuota) ||
Expand All @@ -26,7 +27,10 @@ export function ResourceQuotaCreate({
initialResourceQuota ||
createResourceQuotaTemplate({ namespaceName: namespaceId }),
);
const { t } = useTranslation();

const [initialUnchangedResource] = useState(
_.cloneDeep(initialResourceQuota),
);

return (
<ResourceForm
Expand All @@ -35,13 +39,13 @@ export function ResourceQuotaCreate({
singularName={t('resource-quotas.name_singular')}
resource={resourceQuota}
initialResource={initialResource}
initialUnchangedResource={initialUnchangedResource}
setResource={setResourceQuota}
onChange={onChange}
formElementRef={formElementRef}
createUrl={resourceUrl}
setCustomValid={setCustomValid}
onlyYaml
afterCreatedFn={() => {}}
/>
);
}
16 changes: 16 additions & 0 deletions src/resources/ResourceQuotas/ResourceQuotaDetails.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.resource-quota-limits {
&.compact {
margin: 0 0 1rem 0 !important;
padding-right: 0.5rem;
}
text-transform: capitalize;
}

.resource-quota-spec-subheader {
border-block-end: 0.0625rem solid var(--sapGroup_TitleBorderColor);
padding-bottom: 0.5rem;
}

ui5-table-cell:has(> ui5-panel.resource-quota-limits.compact) {
display: inline !important;
}
100 changes: 100 additions & 0 deletions src/resources/ResourceQuotas/ResourceQuotaDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { ResourceDetails } from 'shared/components/ResourceDetails/ResourceDetails';
import { ResourceDescription } from '.';
import ResourceQuotaCreate from './ResourceQuotaCreate';
import ResourceQuotaLimits from './ResourceQuotaLimits';
import { UI5Panel } from 'shared/components/UI5Panel/UI5Panel';
import { LayoutPanelRow } from 'shared/components/LayoutPanelRow/LayoutPanelRow';
import { useTranslation } from 'react-i18next';
import { Tokens } from 'shared/components/Tokens';
import { Text, Title } from '@ui5/webcomponents-react';
import { spacing } from '@ui5/webcomponents-react-base';

export type ResourceQuotaProps = {
kind: string;
apiVersion: string;
metadata: {
name: string;
namespace: string;
};
spec: {
scopes?: string[];
hard: {
[key: string]: string;
};
scopeSelector?: {
matchExpressions: {
scopeName: string;
operator: string;
values?: string[];
}[];
};
};
status: {
hard: {
[key: string]: string;
};
used: {
[key: string]: string;
};
};
};

export default function ResourceQuotaDetails(props: any) {
const { t } = useTranslation();

const customComponents = [
(resource: ResourceQuotaProps) => {
return (
<>
{(resource.spec.scopes || resource.spec.scopeSelector) && (
<UI5Panel title={t('common.headers.specification')}>
{resource.spec?.scopes && (
<LayoutPanelRow
name={t('resource-quotas.headers.scopes')}
value={<Tokens tokens={resource.spec.scopes} />}
/>
)}
{resource.spec?.scopeSelector && (
<UI5Panel title={t('resource-quotas.headers.scope-selectors')}>
{resource.spec.scopeSelector?.matchExpressions?.map(scope => (
<>
<Title
level="H6"
className="resource-quota-spec-subheader"
style={spacing.sapUiSmallMargin}
>
{scope.scopeName}
</Title>
<LayoutPanelRow
name={t('resource-quotas.headers.operator')}
value={<Text>{scope.operator}</Text>}
/>
{scope.values && (
<LayoutPanelRow
name={t('resource-quotas.headers.values')}
value={<Tokens tokens={scope.values} />}
/>
)}
</>
))}
</UI5Panel>
)}
</UI5Panel>
)}
</>
);
},
(resource: ResourceQuotaProps) => (
<ResourceQuotaLimits resource={resource} />
),
];

return (
<ResourceDetails
description={ResourceDescription}
createResourceForm={ResourceQuotaCreate}
customComponents={customComponents}
{...props}
/>
);
}
68 changes: 68 additions & 0 deletions src/resources/ResourceQuotas/ResourceQuotaLimits.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { GenericList } from 'shared/components/GenericList/GenericList';
import { EMPTY_TEXT_PLACEHOLDER } from 'shared/constants';
import './ResourceQuotaDetails.scss';
import { ResourceQuotaProps } from './ResourceQuotaDetails';

type ResourceTableEntry = {
resource: string;
used: string;
hard: string;
};

export default function ResourceQuotaLimits({
resource,
isCompact = false,
}: {
resource: ResourceQuotaProps;
isCompact?: boolean;
}) {
const { t } = useTranslation();

const headerRenderer = () => [
t('resource-quotas.headers.resource'),
t('resource-quotas.headers.hard'),
t('resource-quotas.headers.used'),
];

const parsedResourceQuota = useMemo(() => {
const result: ResourceTableEntry[] = [];

const hardResources = resource.spec.hard;
const usedResources = resource.status.used;

for (const resource in hardResources) {
if (hardResources.hasOwnProperty(resource)) {
result.push({
resource,
hard: hardResources[resource],
used: usedResources[resource] || '0',
});
}
}

return result;
}, [resource.spec.hard, resource.status.used]);

const rowRenderer = ({ resource, used, hard }: ResourceTableEntry) => {
return [
resource || EMPTY_TEXT_PLACEHOLDER,
hard || EMPTY_TEXT_PLACEHOLDER,
used || EMPTY_TEXT_PLACEHOLDER,
];
};

return (
<GenericList
title={!isCompact ? t('resource-quotas.headers.limits-usage') : null}
entries={parsedResourceQuota || []}
headerRenderer={headerRenderer}
rowRenderer={rowRenderer}
searchSettings={{
showSearchField: false,
}}
className={`resource-quota-limits ${isCompact ? 'compact' : ''}`}
/>
);
}
Original file line number Diff line number Diff line change
@@ -1,49 +1,51 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ResourcesList } from 'shared/components/ResourcesList/ResourcesList';
import { ResourceDescription, docsURL, i18nDescriptionKey } from '.';
import { EMPTY_TEXT_PLACEHOLDER } from 'shared/constants';
import { useTranslation } from 'react-i18next';
import { ResourceQuotaCreate } from './ResourceQuotaCreate';
import ResourceQuotaCreate from './ResourceQuotaCreate';
import { ResourceQuotaProps } from './ResourceQuotaDetails';

export function ResourceQuotaList(props) {
export default function ResourceQuotaList(props: any) {
const { t } = useTranslation();

const customColumns = [
{
header: t('resource-quotas.headers.limits.cpu'),
value: quota =>
value: (quota: ResourceQuotaProps) =>
quota.spec?.hard?.['limits.cpu'] || EMPTY_TEXT_PLACEHOLDER,
},
{
header: t('resource-quotas.headers.limits.memory'),
value: quota =>
value: (quota: ResourceQuotaProps) =>
quota.spec?.hard?.['limits.memory'] || EMPTY_TEXT_PLACEHOLDER,
},
{
header: t('resource-quotas.headers.requests.cpu'),
value: quota =>
value: (quota: ResourceQuotaProps) =>
quota.spec?.hard?.['requests.cpu'] ||
quota.spec?.hard?.cpu ||
EMPTY_TEXT_PLACEHOLDER,
},
{
header: t('resource-quotas.headers.requests.memory'),
value: quota =>
value: (quota: ResourceQuotaProps) =>
quota.spec?.hard?.['requests.memory'] ||
quota.spec?.hard?.memory ||
EMPTY_TEXT_PLACEHOLDER,
},
];

return (
<ResourcesList
disableHiding={true}
simpleEmptyListMessage={true}
displayArrow={false}
resourceTitle={t('resource-quotas.title')}
customColumns={customColumns}
resourceTitle={t('limit-ranges.title')}
description={ResourceDescription}
{...props}
createResourceForm={ResourceQuotaCreate}
emptyListProps={{
subtitleText: i18nDescriptionKey,
url: docsURL,
}}
customColumns={customColumns}
/>
);
}

export default ResourceQuotaList;
Loading

0 comments on commit 6cc72c6

Please sign in to comment.