diff --git a/Dockerfile b/Dockerfile index 025bed4b..076b4238 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM epamedp/headlamp:0.22.37 +FROM epamedp/headlamp:0.22.38 COPY --chown=100:101 assets/ /headlamp/frontend COPY --chown=100:101 dist/main.js /headlamp/plugins/edp/main.js diff --git a/src/components/EmptyList/index.tsx b/src/components/EmptyList/index.tsx index e8f37457..7866759d 100644 --- a/src/components/EmptyList/index.tsx +++ b/src/components/EmptyList/index.tsx @@ -13,6 +13,7 @@ export const EmptyList = ({ handleClick, isSearch = false, icon, + iconSize = 128, }: EmptyListProps) => { const theme = useTheme(); return ( @@ -41,8 +42,8 @@ export const EmptyList = ({ ) : ( )} diff --git a/src/components/EmptyList/types.ts b/src/components/EmptyList/types.ts index fff74495..9f887b6e 100644 --- a/src/components/EmptyList/types.ts +++ b/src/components/EmptyList/types.ts @@ -7,4 +7,5 @@ export interface EmptyListProps { handleClick?: () => void; isSearch?: boolean; icon?: React.ReactNode; + iconSize?: number; } diff --git a/src/k8s/groups/Capsule/Tenant/config.ts b/src/k8s/groups/Capsule/Tenant/config.ts new file mode 100644 index 00000000..71866742 --- /dev/null +++ b/src/k8s/groups/Capsule/Tenant/config.ts @@ -0,0 +1,9 @@ +export const TenantKubeObjectConfig = { + kind: 'Tenant', + name: { + singularForm: 'tenant', + pluralForm: 'tenants', + }, + group: 'capsule.clastix.io', + version: 'v1beta2', +} as const; diff --git a/src/k8s/groups/Capsule/Tenant/index.ts b/src/k8s/groups/Capsule/Tenant/index.ts new file mode 100644 index 00000000..a28a76ce --- /dev/null +++ b/src/k8s/groups/Capsule/Tenant/index.ts @@ -0,0 +1,27 @@ +import { ApiProxy, K8s } from '@kinvolk/headlamp-plugin/lib'; +import { TenantKubeObjectConfig } from './config'; +import { TenantKubeObjectInterface } from './types'; + +const { + name: { singularForm, pluralForm }, + group, + version, +} = TenantKubeObjectConfig; + +export class TenantKubeObject extends K8s.cluster.makeKubeObject( + singularForm +) { + static apiEndpoint = ApiProxy.apiFactoryWithNamespace(group, version, pluralForm); + + static get className(): string { + return singularForm; + } + + get spec(): any { + return this.jsonData!.spec; + } + + get status(): any { + return this.jsonData!.status; + } +} diff --git a/src/k8s/groups/Capsule/Tenant/types.ts b/src/k8s/groups/Capsule/Tenant/types.ts new file mode 100644 index 00000000..b04f352b --- /dev/null +++ b/src/k8s/groups/Capsule/Tenant/types.ts @@ -0,0 +1,62 @@ +import { KubeObjectInterface } from '@kinvolk/headlamp-plugin/lib/lib/k8s/cluster'; + +export interface TenantStatus { + namespaces: string[]; + size: number; + state: string; +} + +export interface IngressOptions { + hostnameCollisionScope: string; +} + +export interface Limit { + default: { + cpu: string; + memory: string; + }; + defaultRequest: { + cpu: string; + memory: string; + }; + type: string; +} + +export interface LimitRange { + limits: Limit[]; +} + +export interface NamespaceOptions { + quota: number; +} + +export interface Owner { + clusterRoles: string[]; + kind: string; + name: string; +} + +export interface ResourceQuota { + hard: { + [key: string]: string; + }; +} + +export interface TenantSpec { + ingressOptions: IngressOptions; + limitRanges: { + items: LimitRange[]; + }; + namespaceOptions: NamespaceOptions; + networkPolicies: Record; + owners: Owner[]; + resourceQuotas: { + items: ResourceQuota[]; + scope: string; + }; +} + +export interface TenantKubeObjectInterface extends KubeObjectInterface { + status: TenantStatus; + spec: TenantSpec; +} diff --git a/src/widgets/ResourceQuotas/components/RQItem/index.tsx b/src/widgets/ResourceQuotas/components/RQItem/index.tsx index 8e39505d..b053c7f2 100644 --- a/src/widgets/ResourceQuotas/components/RQItem/index.tsx +++ b/src/widgets/ResourceQuotas/components/RQItem/index.tsx @@ -1,6 +1,7 @@ import { PercentageCircle } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import { Box, Stack, Typography, useTheme } from '@mui/material'; import React from 'react'; +import { entityMapping } from '../../constants'; import { QuotaDetails } from '../../types'; import { getColorByLoadPercentage } from '../../utils'; @@ -14,8 +15,8 @@ export const RQItem = ({ entity, details }: { entity: string; details: QuotaDeta return ( - - {entity} + + {entityMapping?.[entity] || entity} { quotas: ParsedQuotas; highestUsedQuota: QuotaDetails | null; }>(null); + const [stageRQsError, setStageRQsError] = React.useState(null); const handleSetStageRQs = React.useCallback((items: ResourceQuotaKubeObjectInterface[]) => { @@ -82,6 +85,33 @@ export const ResourceQuotas = () => { setStageRQs(parseResourceQuota(items, useAnnotations)); }, []); + const [namespacesQuota, setNamespacesQuota] = React.useState<{ + quotas: ParsedQuotas; + highestUsedQuota: QuotaDetails | null; + }>(null); + + TenantKubeObject.useApiGet((data) => { + const namespacesHard = data.spec.namespaceOptions.quota; + const namespacesUsed = data.status.size; + + const usedPercentage = (namespacesUsed / namespacesHard) * 100; + + setNamespacesQuota({ + quotas: { + namespaces: { + hard: namespacesHard, + hard_initial: namespacesHard, + used: namespacesUsed, + used_initial: namespacesUsed, + usedPercentage: usedPercentage, + }, + }, + highestUsedQuota: { + usedPercentage: usedPercentage, + }, + }); + }, `edp-workload-${defaultNamespace}`); + React.useEffect(() => { if (stageIsLoading) { return; @@ -98,26 +128,22 @@ export const ResourceQuotas = () => { }, [defaultNamespace, firstInClusterStage?.spec.namespace, handleSetStageRQs, stageIsLoading]); const highestUsedQuota = React.useMemo(() => { - if (globalRQs === null || stageRQs === null) { + if (globalRQs === null || stageRQs === null || namespacesQuota === null) { return null; } - if (globalRQs.highestUsedQuota === null && stageRQs.highestUsedQuota === null) { - return null; - } - - if (globalRQs.highestUsedQuota === null) { - return stageRQs.highestUsedQuota; - } + const quotas = [ + globalRQs.highestUsedQuota, + stageRQs.highestUsedQuota, + namespacesQuota.highestUsedQuota, + ].filter(Boolean); - if (stageRQs.highestUsedQuota === null) { - return globalRQs.highestUsedQuota; + if (quotas.length === 0) { + return null; } - return globalRQs.highestUsedQuota.usedPercentage > stageRQs.highestUsedQuota.usedPercentage - ? globalRQs.highestUsedQuota - : stageRQs.highestUsedQuota; - }, [globalRQs, stageRQs]); + return quotas.reduce((max, quota) => (quota.usedPercentage > max.usedPercentage ? quota : max)); + }, [globalRQs, stageRQs, namespacesQuota]); const [anchorEl, setAnchorEl] = React.useState(null); @@ -145,7 +171,7 @@ export const ResourceQuotas = () => { return ( <> - + { - + - {Object.entries(globalRQs.quotas).map(([entity, details]) => ( - - ))} + {Object.keys(globalRQs.quotas).length === 0 ? ( + + ) : ( + Object.entries(globalRQs.quotas).map(([entity, details]) => ( + + )) + )} - - - {Object.entries(stageRQs.quotas).map(([entity, details]) => ( - - ))} - + + {Object.keys(stageRQs.quotas).length === 0 ? ( + + ) : ( + + {Object.entries(stageRQs.quotas).map(([entity, details]) => ( + + ))} + {Object.entries(namespacesQuota.quotas).map(([entity, details]) => ( + + ))} + + )}