Skip to content

Commit

Permalink
feat: Set ns from token, add avatar, add namespaces metric (#509)
Browse files Browse the repository at this point in the history
  • Loading branch information
callmevladik authored and SergK committed Dec 5, 2024
1 parent 2b95202 commit f925316
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 30 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/components/EmptyList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const EmptyList = ({
handleClick,
isSearch = false,
icon,
iconSize = 128,
}: EmptyListProps) => {
const theme = useTheme();
return (
Expand Down Expand Up @@ -41,8 +42,8 @@ export const EmptyList = ({
) : (
<Icon
icon={isSearch ? ICONS.SEARCH : ICONS.WARNING}
width={theme.typography.pxToRem(128)}
height={theme.typography.pxToRem(128)}
width={theme.typography.pxToRem(iconSize)}
height={theme.typography.pxToRem(iconSize)}
color="#A2A7B7"
/>
)}
Expand Down
1 change: 1 addition & 0 deletions src/components/EmptyList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface EmptyListProps {
handleClick?: () => void;
isSearch?: boolean;
icon?: React.ReactNode;
iconSize?: number;
}
9 changes: 9 additions & 0 deletions src/k8s/groups/Capsule/Tenant/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const TenantKubeObjectConfig = {
kind: 'Tenant',
name: {
singularForm: 'tenant',
pluralForm: 'tenants',
},
group: 'capsule.clastix.io',
version: 'v1beta2',
} as const;
27 changes: 27 additions & 0 deletions src/k8s/groups/Capsule/Tenant/index.ts
Original file line number Diff line number Diff line change
@@ -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<TenantKubeObjectInterface>(
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;
}
}
62 changes: 62 additions & 0 deletions src/k8s/groups/Capsule/Tenant/types.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
owners: Owner[];
resourceQuotas: {
items: ResourceQuota[];
scope: string;
};
}

export interface TenantKubeObjectInterface extends KubeObjectInterface {
status: TenantStatus;
spec: TenantSpec;
}
5 changes: 3 additions & 2 deletions src/widgets/ResourceQuotas/components/RQItem/index.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -14,8 +15,8 @@ export const RQItem = ({ entity, details }: { entity: string; details: QuotaDeta
return (
<Box sx={{ flex: '1 1 0', minWidth: theme.typography.pxToRem(100) }}>
<Stack alignItems="center" spacing={1}>
<Typography color="primary.dark" variant="subtitle2">
{entity}
<Typography color="primary.dark" variant="subtitle2" whiteSpace="nowrap">
{entityMapping?.[entity] || entity}
</Typography>
<Box sx={{ width: '40px', height: '40px' }}>
<PercentageCircle
Expand Down
8 changes: 8 additions & 0 deletions src/widgets/ResourceQuotas/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const entityMapping = {
'requests.cpu': 'CPU Requests',
'requests.memory': 'Memory Requests',
'limits.cpu': 'CPU Limits',
'limits.memory': 'Memory Limits',
pods: 'Pods',
namespaces: 'Namespaces',
} as const;
93 changes: 68 additions & 25 deletions src/widgets/ResourceQuotas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { ApiError } from '@kinvolk/headlamp-plugin/lib/lib/k8s/apiProxy';
import { Box, IconButton, Popover, Stack, Tooltip, useTheme } from '@mui/material';
import React from 'react';
import { BorderedSection } from '../../components/BorderedSection';
import { EmptyList } from '../../components/EmptyList';
import { LoadingWrapper } from '../../components/LoadingWrapper';
import { DEFAULT_CLUSTER } from '../../constants/clusters';
import { TenantKubeObject } from '../../k8s/groups/Capsule/Tenant';
import { ResourceQuotaKubeObject } from '../../k8s/groups/default/ResourceQuota';
import { RESOURCE_QUOTA_LABEL_TENANT } from '../../k8s/groups/default/ResourceQuota/labels';
import { ResourceQuotaKubeObjectInterface } from '../../k8s/groups/default/ResourceQuota/types';
Expand Down Expand Up @@ -64,6 +66,7 @@ export const ResourceQuotas = () => {
quotas: ParsedQuotas;
highestUsedQuota: QuotaDetails | null;
}>(null);

const [stageRQsError, setStageRQsError] = React.useState<Error | ApiError>(null);

const handleSetStageRQs = React.useCallback((items: ResourceQuotaKubeObjectInterface[]) => {
Expand All @@ -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;
Expand All @@ -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 | HTMLElement>(null);

Expand Down Expand Up @@ -145,7 +171,7 @@ export const ResourceQuotas = () => {

return (
<>
<Tooltip title="Resource Quotas">
<Tooltip title="Platform Resource Usage">
<IconButton onClick={handleClick} size="large">
<CircleProgress
loadPercentage={highestUsedQuota.usedPercentage}
Expand All @@ -171,21 +197,38 @@ export const ResourceQuotas = () => {
<Box sx={{ py: theme.typography.pxToRem(40), px: theme.typography.pxToRem(40) }}>
<Stack spacing={5}>
<LoadingWrapper isLoading={globalRQsDataIsLoading}>
<BorderedSection title="Global Resource Quotas">
<BorderedSection title="Platform Resource Usage">
<Stack direction="row" spacing={5}>
{Object.entries(globalRQs.quotas).map(([entity, details]) => (
<RQItem key={`global-${entity}`} entity={entity} details={details} />
))}
{Object.keys(globalRQs.quotas).length === 0 ? (
<EmptyList
customText="Failed to retrieve resource information from the platform. Check your platform configuration."
iconSize={64}
/>
) : (
Object.entries(globalRQs.quotas).map(([entity, details]) => (
<RQItem key={`global-${entity}`} entity={entity} details={details} />
))
)}
</Stack>
</BorderedSection>
</LoadingWrapper>
<LoadingWrapper isLoading={stageRQsDataIsLoading}>
<BorderedSection title="Deployment Flow Resource Quotas">
<Stack direction="row" spacing={5}>
{Object.entries(stageRQs.quotas).map(([entity, details]) => (
<RQItem key={`stage-${entity}`} entity={entity} details={details} />
))}
</Stack>
<BorderedSection title="Deployment Flows Resource Usage">
{Object.keys(stageRQs.quotas).length === 0 ? (
<EmptyList
customText="Resource information is not available yet. It may take some time for the data to be generated."
iconSize={64}
/>
) : (
<Stack direction="row" spacing={5}>
{Object.entries(stageRQs.quotas).map(([entity, details]) => (
<RQItem key={`stage-${entity}`} entity={entity} details={details} />
))}
{Object.entries(namespacesQuota.quotas).map(([entity, details]) => (
<RQItem key={`stage-${entity}`} entity={entity} details={details} />
))}
</Stack>
)}
</BorderedSection>
</LoadingWrapper>
</Stack>
Expand Down

0 comments on commit f925316

Please sign in to comment.