Skip to content

Commit 2ed51bb

Browse files
callmevladikSergK
authored andcommitted
feat: Add deployment flow RQs (#503)
1 parent 367853d commit 2ed51bb

File tree

4 files changed

+199
-45
lines changed

4 files changed

+199
-45
lines changed
Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
11
import { K8s } from '@kinvolk/headlamp-plugin/lib';
2+
import { streamResults } from '../../../common/streamResults';
3+
import { ResourceQuotaKubeObjectConfig } from './config';
4+
import { RESOURCE_QUOTA_LABEL_TENANT } from './labels';
5+
import { StreamListProps } from './types';
26

3-
export class ResourceQuotaKubeObject extends K8s.ResourceClasses.ResourceQuota {}
7+
const {
8+
name: { pluralForm },
9+
version,
10+
} = ResourceQuotaKubeObjectConfig;
11+
12+
export class ResourceQuotaKubeObject extends K8s.ResourceClasses.ResourceQuota {
13+
static streamList({
14+
namespace,
15+
tenantNamespace,
16+
dataHandler,
17+
errorHandler,
18+
}: StreamListProps): () => void {
19+
console.log(tenantNamespace);
20+
21+
const url = `/api/${version}/namespaces/${namespace}/${pluralForm}`;
22+
return streamResults(url, dataHandler, errorHandler, {
23+
labelSelector: `${RESOURCE_QUOTA_LABEL_TENANT}=edp-workload-${tenantNamespace}`,
24+
});
25+
}
26+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
import { KubeObjectInterface } from '@kinvolk/headlamp-plugin/lib/lib/k8s/cluster';
22

33
export interface ResourceQuotaKubeObjectInterface extends KubeObjectInterface {}
4+
5+
export interface StreamListProps {
6+
namespace: string;
7+
tenantNamespace: string;
8+
dataHandler: (data: ResourceQuotaKubeObjectInterface[]) => void;
9+
errorHandler: (err: Error) => void;
10+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { PercentageCircle } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
2+
import { Box, Stack, Typography, useTheme } from '@mui/material';
3+
import React from 'react';
4+
import { QuotaDetails } from '../../types';
5+
import { getColorByLoadPercentage } from '../../utils';
6+
7+
export const RQItem = ({ entity, details }: { entity: string; details: QuotaDetails }) => {
8+
const theme = useTheme();
9+
10+
const { hard, used } = details;
11+
const loadPercentage = Math.floor((used / hard) * 100);
12+
const color = getColorByLoadPercentage(theme, loadPercentage);
13+
14+
return (
15+
<Box sx={{ flex: '1 1 0', minWidth: theme.typography.pxToRem(100) }}>
16+
<Stack alignItems="center" spacing={1}>
17+
<Typography color="primary.dark" variant="subtitle2">
18+
{entity}
19+
</Typography>
20+
<Box sx={{ width: '40px', height: '40px' }}>
21+
<PercentageCircle
22+
data={[
23+
{
24+
name: 'OK',
25+
value: loadPercentage,
26+
fill: color,
27+
},
28+
]}
29+
total={100}
30+
size={50}
31+
thickness={6}
32+
/>
33+
</Box>
34+
<Typography color="primary.dark" variant="caption">
35+
{details?.['used_initial']} / {details?.['hard_initial']}
36+
</Typography>
37+
</Stack>
38+
</Box>
39+
);
40+
};

src/widgets/ResourceQuotas/index.tsx

Lines changed: 128 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,123 @@
1-
import { PercentageCircle } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
2-
import { Box, IconButton, Popover, Stack, Tooltip, Typography, useTheme } from '@mui/material';
1+
import { ApiError } from '@kinvolk/headlamp-plugin/lib/lib/k8s/apiProxy';
2+
import { Box, IconButton, Popover, Stack, Tooltip, useTheme } from '@mui/material';
33
import React from 'react';
4+
import { BorderedSection } from '../../components/BorderedSection';
5+
import { LoadingWrapper } from '../../components/LoadingWrapper';
6+
import { DEFAULT_CLUSTER } from '../../constants/clusters';
47
import { ResourceQuotaKubeObject } from '../../k8s/groups/default/ResourceQuota';
58
import { RESOURCE_QUOTA_LABEL_TENANT } from '../../k8s/groups/default/ResourceQuota/labels';
9+
import { ResourceQuotaKubeObjectInterface } from '../../k8s/groups/default/ResourceQuota/types';
10+
import { StageKubeObject } from '../../k8s/groups/EDP/Stage';
11+
import { StageKubeObjectInterface } from '../../k8s/groups/EDP/Stage/types';
612
import { getDefaultNamespace } from '../../utils/getDefaultNamespace';
713
import { CircleProgress } from './components/CircleProgress';
14+
import { RQItem } from './components/RQItem';
15+
import { ParsedQuotas, QuotaDetails } from './types';
816
import { getColorByLoadPercentage, parseResourceQuota } from './utils';
917

1018
export const ResourceQuotas = () => {
11-
const [items] = ResourceQuotaKubeObject.useList({
12-
labelSelector: `${RESOURCE_QUOTA_LABEL_TENANT}=${getDefaultNamespace()}`,
19+
const defaultNamespace = getDefaultNamespace();
20+
21+
const [globalRQs, setGlobalRQs] = React.useState<{
22+
quotas: ParsedQuotas;
23+
highestUsedQuota: QuotaDetails | null;
24+
}>(null);
25+
const [globalRQsError, setGlobalRQsError] = React.useState<Error | ApiError>(null);
26+
27+
const handleSetGlobalRQs = React.useCallback((items: ResourceQuotaKubeObjectInterface[]) => {
28+
if (items?.length === 0) {
29+
setGlobalRQs({
30+
quotas: {},
31+
highestUsedQuota: null,
32+
});
33+
return;
34+
}
35+
36+
const useAnnotations = Object.keys(items[0]?.metadata?.annotations || {}).some((key) =>
37+
key.includes('quota.capsule.clastix.io')
38+
);
39+
40+
setGlobalRQs(parseResourceQuota(items, useAnnotations));
41+
}, []);
42+
43+
ResourceQuotaKubeObject.useApiList(handleSetGlobalRQs, setGlobalRQsError, {
44+
namespace: defaultNamespace,
45+
labelSelector: `${RESOURCE_QUOTA_LABEL_TENANT}=${defaultNamespace}`,
1346
});
1447

15-
const { quotas, highestUsedQuota } = React.useMemo(() => {
16-
if (items === null || items?.length === 0) {
17-
return { quotas: null, highestUsedQuota: null };
48+
const [firstInClusterStage, setFirstInClusterStage] =
49+
React.useState<StageKubeObjectInterface>(null);
50+
const [stagesError, setStagesError] = React.useState<Error | ApiError>(null);
51+
52+
const stageIsLoading = firstInClusterStage === null && !stagesError;
53+
54+
const handleGetFirstInClusterStage = React.useCallback((stages: StageKubeObjectInterface[]) => {
55+
const firstFind = stages.find((stage) => stage.spec.clusterName === DEFAULT_CLUSTER);
56+
setFirstInClusterStage(firstFind);
57+
}, []);
58+
59+
StageKubeObject.useApiList(handleGetFirstInClusterStage, setStagesError, {
60+
namespace: defaultNamespace,
61+
});
62+
63+
const [stageRQs, setStageRQs] = React.useState<{
64+
quotas: ParsedQuotas;
65+
highestUsedQuota: QuotaDetails | null;
66+
}>(null);
67+
const [stageRQsError, setStageRQsError] = React.useState<Error | ApiError>(null);
68+
69+
const handleSetStageRQs = React.useCallback((items: ResourceQuotaKubeObjectInterface[]) => {
70+
if (items?.length === 0) {
71+
setStageRQs({
72+
quotas: {},
73+
highestUsedQuota: null,
74+
});
75+
return;
1876
}
1977

2078
const useAnnotations = Object.keys(items[0]?.metadata?.annotations || {}).some((key) =>
2179
key.includes('quota.capsule.clastix.io')
2280
);
2381

24-
return parseResourceQuota(items, useAnnotations);
25-
}, [items]);
82+
setStageRQs(parseResourceQuota(items, useAnnotations));
83+
}, []);
84+
85+
React.useEffect(() => {
86+
if (stageIsLoading) {
87+
return;
88+
}
89+
90+
const cancelStream = ResourceQuotaKubeObject.streamList({
91+
namespace: firstInClusterStage?.spec.namespace,
92+
tenantNamespace: defaultNamespace,
93+
dataHandler: handleSetStageRQs,
94+
errorHandler: setStageRQsError,
95+
});
96+
97+
return () => cancelStream();
98+
}, [defaultNamespace, firstInClusterStage?.spec.namespace, handleSetStageRQs, stageIsLoading]);
99+
100+
const highestUsedQuota = React.useMemo(() => {
101+
if (globalRQs === null || stageRQs === null) {
102+
return null;
103+
}
104+
105+
if (globalRQs.highestUsedQuota === null && stageRQs.highestUsedQuota === null) {
106+
return null;
107+
}
108+
109+
if (globalRQs.highestUsedQuota === null) {
110+
return stageRQs.highestUsedQuota;
111+
}
112+
113+
if (stageRQs.highestUsedQuota === null) {
114+
return globalRQs.highestUsedQuota;
115+
}
116+
117+
return globalRQs.highestUsedQuota.usedPercentage > stageRQs.highestUsedQuota.usedPercentage
118+
? globalRQs.highestUsedQuota
119+
: stageRQs.highestUsedQuota;
120+
}, [globalRQs, stageRQs]);
26121

27122
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
28123

@@ -39,7 +134,10 @@ export const ResourceQuotas = () => {
39134
const open = Boolean(anchorEl);
40135
const id = open ? 'simple-popover' : undefined;
41136

42-
if (quotas === null) {
137+
const globalRQsDataIsLoading = globalRQs === null && !globalRQsError;
138+
const stageRQsDataIsLoading = stageRQs === null && !stageRQsError;
139+
140+
if (globalRQsDataIsLoading || stageRQsDataIsLoading) {
43141
return null;
44142
}
45143

@@ -70,40 +168,26 @@ export const ResourceQuotas = () => {
70168
horizontal: 'right',
71169
}}
72170
>
73-
<Box sx={{ py: theme.typography.pxToRem(20), px: theme.typography.pxToRem(30) }}>
74-
<Stack direction="row" spacing={5}>
75-
{Object.entries(quotas).map(([entity, details]) => {
76-
const { hard, used } = details;
77-
const loadPercentage = Math.floor((used / hard) * 100);
78-
const color = getColorByLoadPercentage(theme, loadPercentage);
79-
80-
return (
81-
<Box sx={{ flex: '1 1 0', minWidth: theme.typography.pxToRem(100) }}>
82-
<Stack alignItems="center" spacing={1}>
83-
<Typography color="primary.dark" variant="subtitle2">
84-
{entity}
85-
</Typography>
86-
<Box sx={{ width: '40px', height: '40px' }}>
87-
<PercentageCircle
88-
data={[
89-
{
90-
name: 'OK',
91-
value: loadPercentage,
92-
fill: color,
93-
},
94-
]}
95-
total={100}
96-
size={50}
97-
thickness={6}
98-
/>
99-
</Box>
100-
<Typography color="primary.dark" variant="caption">
101-
{details?.['used_initial']} / {details?.['hard_initial']}
102-
</Typography>
103-
</Stack>
104-
</Box>
105-
);
106-
})}
171+
<Box sx={{ py: theme.typography.pxToRem(40), px: theme.typography.pxToRem(40) }}>
172+
<Stack spacing={5}>
173+
<LoadingWrapper isLoading={globalRQsDataIsLoading}>
174+
<BorderedSection title="Global Resource Quotas">
175+
<Stack direction="row" spacing={5}>
176+
{Object.entries(globalRQs.quotas).map(([entity, details]) => (
177+
<RQItem key={`global-${entity}`} entity={entity} details={details} />
178+
))}
179+
</Stack>
180+
</BorderedSection>
181+
</LoadingWrapper>
182+
<LoadingWrapper isLoading={stageRQsDataIsLoading}>
183+
<BorderedSection title="Deployment Flow Resource Quotas">
184+
<Stack direction="row" spacing={5}>
185+
{Object.entries(stageRQs.quotas).map(([entity, details]) => (
186+
<RQItem key={`stage-${entity}`} entity={entity} details={details} />
187+
))}
188+
</Stack>
189+
</BorderedSection>
190+
</LoadingWrapper>
107191
</Stack>
108192
</Box>
109193
</Popover>

0 commit comments

Comments
 (0)