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]) => (
+
+ ))}
+
+ )}