Skip to content

Commit

Permalink
feat:db add quota (#4055)
Browse files Browse the repository at this point in the history
* feat:db add quota

Signed-off-by: jingyang <[email protected]>

* add cronjob key

Signed-off-by: jingyang <[email protected]>

---------

Signed-off-by: jingyang <[email protected]>
  • Loading branch information
zjy365 authored Oct 10, 2023
1 parent 6ec63e0 commit 7bc4773
Show file tree
Hide file tree
Showing 21 changed files with 387 additions and 50 deletions.
19 changes: 19 additions & 0 deletions frontend/providers/cronjob/src/constants/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// applanuchpad
export const pauseKey = 'deploy.cloud.sealos.io/pause';
export const deployManagerKey = 'cloud.sealos.io/app-deploy-manager';
export const noGpuSliderKey = 'NoGpu';
export const maxReplicasKey = 'deploy.cloud.sealos.io/maxReplicas';
export const minReplicasKey = 'deploy.cloud.sealos.io/minReplicas';
export const appDeployKey = 'cloud.sealos.io/app-deploy-manager';
export const publicDomainKey = `cloud.sealos.io/app-deploy-manager-domain`;
export const gpuNodeSelectorKey = 'nvidia.com/gpu.product';
export const gpuResourceKey = 'nvidia.com/gpu';
// template
export const templateDeployKey = 'cloud.sealos.io/deploy-on-sealos';
// db
export const kubeblocksTypeKey = 'clusterdefinition.kubeblocks.io/name';
// labels
export const componentLabel = 'app.kubernetes.io/component';
// cronjob
export const cronJobTypeKey = 'cronjob-type';
export const cronJobKey = 'cloud.sealos.io/cronjob';
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cronJobKey } from '@/constants/keys';
import { authSession } from '@/services/backend/auth';
import { getK8s } from '@/services/backend/kubernetes';
import { jsonRes } from '@/services/backend/response';
Expand All @@ -9,7 +10,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const { namespace, k8sBatch } = await getK8s({
kubeconfig: await authSession(req)
});
const response = await k8sBatch.listNamespacedCronJob(namespace);
const response = await k8sBatch.listNamespacedCronJob(
namespace,
undefined,
undefined,
undefined,
undefined,
cronJobKey
);

jsonRes(res, { data: response?.body?.items });
} catch (err: any) {
Expand Down
4 changes: 3 additions & 1 deletion frontend/providers/cronjob/src/utils/json2Yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CronJobEditType } from '@/types/job';
import { getUserTimeZone, str2Num } from '@/utils/tools';
import yaml from 'js-yaml';
import { getUserServiceAccount } from './user';
import { cronJobKey } from '@/constants/keys';

export const json2CronJob = (data: CronJobEditType) => {
const serviceAccount = getUserServiceAccount();
Expand All @@ -12,7 +13,8 @@ export const json2CronJob = (data: CronJobEditType) => {
annotations: {},
labels: {
'cronjob-type': data.jobType,
'cronjob-launchpad-name': data.launchpadName
'cronjob-launchpad-name': data.launchpadName,
[cronJobKey]: data.jobName
}
};

Expand Down
12 changes: 11 additions & 1 deletion frontend/providers/dbprovider/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,15 @@
"Running Time": "Running Time",
"InnoDB Buffer Pool": "InnoDB Buffer Pool",
"Database Usage": "Database Usage",
"Disk": "Disk"
"Disk": "Disk",
"app": {
"Resource Quota": "Quota",
"The applied CPU exceeds the quota": "The applied CPU exceeds the quota",
"The applied memory exceeds the quota": "The applied memory exceeds the quota",
"The applied GPU exceeds the quota": "The applied GPU exceeds the quota",
"The applied storage exceeds the quota": "The applied storage exceeds the quota",
"The container exposed port cannot be empty": "The container exposed port cannot be empty",
"The minimum exposed port is 1": "The minimum exposed port is 1",
"The maximum number of exposed ports is 65535": "The maximum number of exposed ports is 65535"
}
}
12 changes: 11 additions & 1 deletion frontend/providers/dbprovider/public/locales/zh/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,15 @@
"Running Time": "运行时长",
"InnoDB Buffer Pool": "InnoDB 缓冲池",
"Database Usage": "数据库用量",
"Disk": "磁盘空间"
"Disk": "磁盘空间",
"app": {
"Resource Quota": "资源配额",
"The applied CPU exceeds the quota": "申请的 CPU 超出限制,请联系管理员",
"The applied memory exceeds the quota": "申请的 '内存' 超出限制,请联系管理员",
"The applied GPU exceeds the quota": "申请的 GPU 超出限制,请联系管理员",
"The applied storage exceeds the quota": "申请的 '存储' 超出限制,请联系管理员",
"The container exposed port cannot be empty": "容器暴露端口不能为空",
"The minimum exposed port is 1": "暴露端口最小为 1",
"The maximum number of exposed ports is 65535": "暴露端口最大为65535"
}
}
13 changes: 10 additions & 3 deletions frontend/providers/dbprovider/src/api/platform.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { GET, POST, DELETE } from '@/services/request';
import type { Response as resourcePriceResponse } from '@/pages/api/platform/resourcePrice';
import type { Response as DBVersionMapType } from '@/pages/api/platform/getVersion';
import { Response as EnvResponse } from '@/pages/api/getEnv';
import type { Response as DBVersionMapType } from '@/pages/api/platform/getVersion';
import type { Response as resourcePriceResponse } from '@/pages/api/platform/resourcePrice';
import { GET } from '@/services/request';
import type { UserQuotaItemType } from '@/types/user';

export const getResourcePrice = () => GET<resourcePriceResponse>('/api/platform/resourcePrice');
export const getAppEnv = () => GET<EnvResponse>('/api/getEnv');
export const getDBVersionMap = () => GET<DBVersionMapType>('/api/platform/getVersion');

export const getUserQuota = () =>
GET<{
balance: number;
quota: UserQuotaItemType[];
}>('/api/platform/getQuota');
24 changes: 24 additions & 0 deletions frontend/providers/dbprovider/src/pages/api/platform/getQuota.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { getK8s } from '@/services/backend/kubernetes';
import { jsonRes } from '@/services/backend/response';
import { authSession } from '@/services/backend/auth';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
// source price
const { getUserQuota, getUserBalance } = await getK8s({
kubeconfig: await authSession(req)
});

const [quota, balance] = await Promise.all([getUserQuota(), getUserBalance()]);

jsonRes(res, {
data: {
quota,
balance
}
});
} catch (error) {
jsonRes(res, { code: 500, message: 'get price error' });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { useTranslation } from 'next-i18next';
import PriceBox from './PriceBox';
import { INSTALL_ACCOUNT } from '@/store/static';
import Tip from '@/components/Tip';

import QuotaBox from './QuotaBox';
import { obj2Query } from '@/api/tools';
import { throttle } from 'lodash';
import { InfoOutlineIcon } from '@chakra-ui/icons';
Expand Down Expand Up @@ -196,7 +196,9 @@ const Form = ({
</Box>
))}
</Box>

<Box mt={3} borderRadius={'sm'} overflow={'hidden'} backgroundColor={'white'}>
<QuotaBox />
</Box>
{INSTALL_ACCOUNT && (
<Box mt={3} borderRadius={'sm'} overflow={'hidden'} backgroundColor={'white'} p={3}>
<PriceBox
Expand Down Expand Up @@ -351,7 +353,9 @@ const Form = ({
step={1}
position={'relative'}
value={getValues('storage')}
onChange={(e) => e && setValue('storage', +e)}
onChange={(e) => {
e !== '' ? setValue('storage', +e) : setValue('storage', minStorage);
}}
>
<NumberInputField
{...register('storage', {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { useMemo } from 'react';
import { Box, Flex, useTheme, Progress, css } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
import MyTooltip from '@/components/MyTooltip';
import { useUserStore } from '@/store/user';

const sourceMap = {
cpu: {
color: '#33BABB',
unit: 'Core'
},
memory: {
color: '#36ADEF',
unit: 'Gi'
},
storage: {
color: '#8172D8',
unit: 'GB'
},
gpu: {
color: '#89CD11',
unit: 'Card'
}
};

const QuotaBox = () => {
const theme = useTheme();
const { t } = useTranslation();
const { userQuota, loadUserQuota } = useUserStore();
useQuery(['getUserQuota'], loadUserQuota);

const quotaList = useMemo(() => {
if (!userQuota) return [];
return userQuota
.map((item) => ({
...item,
tip: `${t('Total')}: ${`${item.limit} ${sourceMap[item.type]?.unit}`}
${t('common.Used')}: ${`${item.used} ${sourceMap[item.type]?.unit}`}
${t('common.Surplus')}: ${`${item.limit - item.used} ${sourceMap[item.type]?.unit}`}
`,
color: sourceMap[item.type]?.color
}))
.filter((item) => item.limit > 0);
}, [userQuota, t]);

return userQuota.length === 0 ? null : (
<Box>
<Box py={3} px={4} borderBottom={theme.borders.base}>
<strong>{t('app.Resource Quota')}</strong>
</Box>
<Box py={3} px={4}>
{quotaList.map((item) => (
<MyTooltip key={item.type} label={item.tip} placement={'top-end'} lineHeight={1.7}>
<Flex alignItems={'center'} _notFirst={{ mt: 3 }}>
<Box flex={'0 0 60px'}>{t(item.type)}</Box>
<Progress
flex={'1 0 0'}
borderRadius={'sm'}
size="sm"
value={item.used}
max={item.limit}
css={css({
'& > div': {
bg: item.color
}
})}
/>
</Flex>
</MyTooltip>
))}
</Box>
</Box>
);
};

export default QuotaBox;
63 changes: 42 additions & 21 deletions frontend/providers/dbprovider/src/pages/db/edit/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useCallback, useMemo } from 'react';
import React, { useState, useCallback, useMemo, useRef } from 'react';
import { useRouter } from 'next/router';
import { Flex, Box } from '@chakra-ui/react';
import type { YamlItemType } from '@/types';
Expand All @@ -23,6 +23,7 @@ import { DBVersionMap } from '@/store/static';
import Header from './components/Header';
import Form from './components/Form';
import Yaml from './components/Yaml';
import { useUserStore } from '@/store/user';
const ErrorModal = dynamic(() => import('./components/ErrorModal'));

const defaultEdit = {
Expand All @@ -40,6 +41,8 @@ const EditApp = ({ dbName, tabType }: { dbName?: string; tabType?: 'form' | 'yam
const { toast } = useToast();
const { Loading, setIsLoading } = useLoading();
const { loadDBDetail } = useDBStore();
const oldDBEditData = useRef<DBEditType>();
const { checkQuotaAllow, balance } = useUserStore();
const { title, applyBtnText, applyMessage, applySuccess, applyError } = editModeMap(!!dbName);
const { openConfirm, ConfirmChild } = useConfirm({
content: t(applyMessage)
Expand All @@ -61,24 +64,28 @@ const EditApp = ({ dbName, tabType }: { dbName?: string; tabType?: 'form' | 'yam
defaultValues: defaultEdit
});

const generateYamlList = (data: DBEditType) => {
return [
{
filename: 'cluster.yaml',
value: json2CreateCluster(data)
},
...(isEdit
? []
: [
{
filename: 'account.yaml',
value: json2Account(data)
}
])
];
};

// eslint-disable-next-line react-hooks/exhaustive-deps
const formOnchangeDebounce = useCallback(
debounce((data: DBEditType) => {
try {
setYamlList([
{
filename: 'cluster.yaml',
value: json2CreateCluster(data)
},
...(isEdit
? []
: [
{
filename: 'account.yaml',
value: json2Account(data)
}
])
]);
setYamlList(generateYamlList(data));
} catch (error) {
console.log(error);
}
Expand All @@ -91,16 +98,26 @@ const EditApp = ({ dbName, tabType }: { dbName?: string; tabType?: 'form' | 'yam
setForceUpdate(!forceUpdate);
});

const submitSuccess = useCallback(async () => {
const submitSuccess = async (formData: DBEditType) => {
setIsLoading(true);
try {
!isEdit && (await applyYamlList([limitRangeYaml], 'create'));
} catch (err) {}
try {
const data = yamlList.map((item) => item.value);

await applyYamlList(data, isEdit ? 'replace' : 'create');
const yamlList = generateYamlList(formData).map((item) => item.value);
// quote check
const quoteCheckRes = checkQuotaAllow(formData, oldDBEditData.current);
if (quoteCheckRes) {
setIsLoading(false);
return toast({
status: 'warning',
title: t(quoteCheckRes),
duration: 5000,
isClosable: true
});
}

await applyYamlList(yamlList, isEdit ? 'replace' : 'create');
toast({
title: t(applySuccess),
status: 'success'
Expand All @@ -111,7 +128,8 @@ const EditApp = ({ dbName, tabType }: { dbName?: string; tabType?: 'form' | 'yam
setErrorMessage(JSON.stringify(error));
}
setIsLoading(false);
}, [applySuccess, formHook, isEdit, router, setIsLoading, t, toast, yamlList]);
};

const submitError = useCallback(() => {
// deep search message
const deepSearch = (obj: any): string => {
Expand Down Expand Up @@ -152,6 +170,7 @@ const EditApp = ({ dbName, tabType }: { dbName?: string; tabType?: 'form' | 'yam
{
onSuccess(res) {
if (!res) return;
oldDBEditData.current = res;
formHook.reset(adaptDBForm(res));
setMinStorage(res.storage);
},
Expand Down Expand Up @@ -181,7 +200,9 @@ const EditApp = ({ dbName, tabType }: { dbName?: string; tabType?: 'form' | 'yam
title={title}
yamlList={yamlList}
applyBtnText={applyBtnText}
applyCb={() => formHook.handleSubmit(openConfirm(submitSuccess), submitError)()}
applyCb={() =>
formHook.handleSubmit((data) => openConfirm(submitSuccess(data)), submitError)()
}
/>

<Box flex={'1 0 0'} h={0} w={'100%'} pb={4}>
Expand Down
Loading

0 comments on commit 7bc4773

Please sign in to comment.