Skip to content

Commit

Permalink
feat(cost center): support multiNs (labring#3908)
Browse files Browse the repository at this point in the history
* feat(cost center): support multiNs

* style: modify code
  • Loading branch information
xudaotutou authored Sep 13, 2023
1 parent 500fbac commit 3f883f1
Show file tree
Hide file tree
Showing 13 changed files with 204 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"Unit": "Unit",
"Price": "Price"
},
"All Namespace": "All Team ID",
"Bonus": "Bonus",
"Select Amount": "Select Amount",
"Recent Transactions": "Recent Transactions",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"Unit": "单位",
"Price": "价格"
},
"All Namespace": "所有 Team ID",
"Bonus": "",
"Recent Transactions": "最近交易",
"Cost Trend": "成本趋势",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,28 @@ export function BillingTable({ data }: { data: BillingItem[] }) {
<Table variant="simple">
<Thead>
<Tr>
{[...TableHeaders, ...(gpuEnabled ? ['Gpu'] : []), 'Total Amount'].map((item) => (
<Th
key={item}
bg={'#F1F4F6'}
_before={{
content: `""`,
display: 'block',
borderTopLeftRadius: '10px',
borderTopRightRadius: '10px',
background: '#F1F4F6'
}}
>
<Flex display={'flex'} alignItems={'center'}>
<Text mr="4px">{t(item)}</Text>
{['CPU', 'Gpu', 'Memory', 'Storage', 'Network', 'Total Amount'].includes(
item
) && <CurrencySymbol type={currency} />}
</Flex>
</Th>
))}
{[...TableHeaders, ...(gpuEnabled ? ['Gpu'] : []), 'Total Amount', 'Namespace'].map(
(item) => (
<Th
key={item}
bg={'#F1F4F6'}
_before={{
content: `""`,
display: 'block',
borderTopLeftRadius: '10px',
borderTopRightRadius: '10px',
background: '#F1F4F6'
}}
>
<Flex display={'flex'} alignItems={'center'}>
<Text mr="4px">{t(item)}</Text>
{['CPU', 'Gpu', 'Memory', 'Storage', 'Network', 'Total Amount'].includes(item) && (
<CurrencySymbol type={currency} />
)}
</Flex>
</Th>
)
)}
</Tr>
</Thead>
<Tbody>
Expand All @@ -58,13 +60,13 @@ export function BillingTable({ data }: { data: BillingItem[] }) {
minW={'max-content'}
{...([1, 2].includes(item.type)
? {
bg: '#E6F6F6',
color: '#00A9A6'
}
bg: '#E6F6F6',
color: '#00A9A6'
}
: {
bg: '#EBF7FD',
color: '#0884DD'
})}
bg: '#EBF7FD',
color: '#0884DD'
})}
borderRadius="24px"
align={'center'}
justify={'space-evenly'}
Expand All @@ -78,10 +80,10 @@ export function BillingTable({ data }: { data: BillingItem[] }) {
{item.type === 0
? t('Deduction')
: item.type === 1
? t('Charge')
: item.type === 2
? t('Recipient')
: t('Transfer')}
? t('Charge')
: item.type === 2
? t('Recipient')
: t('Transfer')}
</Text>
</Flex>
</Flex>
Expand All @@ -94,6 +96,7 @@ export function BillingTable({ data }: { data: BillingItem[] }) {
<Td>{!item.type ? <span>{formatMoney(item.costs?.gpu || 0)}</span> : '-'}</Td>
)}
<Td>{<span>{formatMoney(item.amount)}</span>}</Td>
<Td>{<span>{item.namespace}</span>}</Td>
</Tr>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse

// do payment
const paymentName = crypto.randomUUID();
const namespace = kc.getContexts()[0].namespace || GetUserDefaultNameSpace(kubeUser.name);
const namespace = GetUserDefaultNameSpace(kubeUser.name);
const form: PaymentForm = {
namespace,
paymentName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse
const k8s_username = kc.getUsers()[0].name;
// do payment
const paymentName = crypto.randomUUID();
const namespace = kc.getContexts()[0].namespace || GetUserDefaultNameSpace(k8s_username);
const namespace = GetUserDefaultNameSpace(k8s_username);
const form: PaymentForm = {
namespace,
paymentName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { authSession } from '@/service/backend/auth';
import { CRDMeta, GetCRD } from '@/service/backend/kubernetes';
import { jsonRes } from '@/service/backend/response';
import type { NextApiRequest, NextApiResponse } from 'next';
import { ApplyYaml } from '@/service/backend/kubernetes';
import * as yaml from 'js-yaml';
import crypto from 'crypto';
import * as k8s from '@kubernetes/client-node';
type Result = {
status: {
details: string;
namespaceList: string[];
status: string;
};
};
const getNSList = (kc: k8s.KubeConfig, meta: CRDMeta, name: string) =>
new Promise<string[]>((resolve, reject) => {
let time = 5;
const wrap = () =>
GetCRD(kc, meta, name)
.then((res) => {
const body = res.body as Result;
if (body?.status?.status?.toLowerCase() === 'completed') {
resolve(body.status.namespaceList);
} else {
return Promise.reject([]);
}
})
.catch((_) => {
if (time >= 0) {
time--;
setTimeout(wrap, 1000);
} else {
reject([]);
}
});
wrap();
});

export default async function handler(req: NextApiRequest, resp: NextApiResponse) {
try {
const kc = await authSession(req.headers);
// get user account payment amount
const user = kc.getCurrentUser();
if (user === null) {
return jsonRes(resp, { code: 403, message: 'user null' });
}
const namespace = 'ns-' + user.name;
// 用react query 管理缓存
const hash = crypto.createHash('sha256').update('' + new Date().getTime());
const name = hash.digest('hex');
const crdSchema = {
apiVersion: `account.sealos.io/v1`,
kind: 'NamespaceBillingHistory',
metadata: {
name,
namespace
},
spec: {
type: -1
}
};

const meta: CRDMeta = {
group: 'account.sealos.io',
version: 'v1',
namespace,
plural: 'namespacebillinghistories'
};
await ApplyYaml(kc, yaml.dump(crdSchema));
const nsList = await getNSList(kc, meta, name);
return jsonRes<{ nsList: string[] }>(resp, {
code: 200,
data: {
nsList
}
});
} catch (error) {
console.log(error);
jsonRes(resp, { code: 500, message: 'get namespaceList error' });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse
if (user === null) {
return jsonRes(resp, { code: 403, message: 'user null' });
}
const namespace = kc.getContexts()[0].namespace || 'ns-' + user.name;
const namespace = 'ns-' + user.name;
const body = req.body;
let spec: BillingSpec = body.spec;
if (!spec) {
Expand Down
6 changes: 4 additions & 2 deletions frontend/providers/costcenter/src/pages/api/getQuota.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { authSession } from '@/service/backend/auth';
import { GetUserDefaultNameSpace } from '@/service/backend/kubernetes';
import { jsonRes } from '@/service/backend/response';
import * as k8s from '@kubernetes/client-node';
import type { NextApiRequest, NextApiResponse } from 'next';
Expand All @@ -11,15 +12,16 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse
if (user === null) {
return jsonRes(resp, { code: 403, message: 'user null' });
}
const namespace = 'ns-' + user.name;
// namespace要可切换
const namespace = GetUserDefaultNameSpace(user.name);
const quota = await getUserQuota(kc, namespace);
return jsonRes(resp, {
code: 200,
data: { quota }
});
} catch (error) {
console.log(error);
jsonRes(resp, { code: 500, message: 'get price error' });
jsonRes(resp, { code: 500, message: 'get quota error' });
}
}
export type UserQuotaItemType = {
Expand Down
7 changes: 1 addition & 6 deletions frontend/providers/costcenter/src/pages/api/price/bonus.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { authSession } from '@/service/backend/auth';
import {
CRDMeta,
GetCRD,
GetConfigMap,
GetUserDefaultNameSpace
} from '@/service/backend/kubernetes';
import { GetConfigMap } from '@/service/backend/kubernetes';
import { jsonRes } from '@/service/backend/response';
import { enableRecharge } from '@/service/enabled';
import type { NextApiRequest, NextApiResponse } from 'next';
Expand Down
1 change: 0 additions & 1 deletion frontend/providers/costcenter/src/pages/api/price/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse
namespace,
plural: 'pricequeries'
};
console.log('price');
try {
await ApplyYaml(kc, yaml.dump(crdSchema));
await new Promise<void>((resolve) => setTimeout(() => resolve(), 1000));
Expand Down
79 changes: 76 additions & 3 deletions frontend/providers/costcenter/src/pages/billing/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';
import { getCookie } from '@/utils/cookieUtils';
import NotFound from '@/components/notFound';
import { ApiResp } from '@/types';

function Billing() {
const { t, i18n } = useTranslation();
Expand All @@ -43,19 +44,28 @@ function Billing() {
const [totalPage, setTotalPage] = useState(1);
const [currentPage, setcurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [namespaceIdx, setNamespaceIdx] = useState(0);
const [totalItem, setTotalItem] = useState(10);
const { data: nsListData } = useQuery({
queryFn() {
return request<any, ApiResp<{ nsList: string[] }>>('/api/billing/getNamespaceList');
},
queryKey: ['nsList']
});

const namespaceList: string[] = [t('All Namespace'), ...(nsListData?.data?.nsList || [])];
const { data, isFetching, isSuccess } = useQuery(
['billing', { currentPage, startTime, endTime, orderID, selectType }],
['billing', { currentPage, startTime, endTime, orderID, selectType, namespaceIdx }],
() => {
let spec = {} as BillingSpec;
spec = {
const spec = {
page: currentPage,
pageSize: pageSize,
type: selectType,
startTime: formatISO(startTime, { representation: 'complete' }),
// startTime,
endTime: formatISO(endTime, { representation: 'complete' }),
// endTime,
namespace: namespaceIdx > 0 ? namespaceList[namespaceIdx] : '',
orderID
};
return request<any, { data: BillingData }, { spec: BillingSpec }>('/api/billing', {
Expand Down Expand Up @@ -88,6 +98,69 @@ function Billing() {
<Flex mr="24px" align={'center'}>
<Img src={receipt_icon.src} w={'24px'} h={'24px'} mr={'18px'} dropShadow={'#24282C'}></Img>
<Heading size="lg">{t('SideBar.BillingDetails')}</Heading>
<Flex align={'center'} ml="28px">
<Popover>
<PopoverTrigger>
<Button
w="110px"
h="32px"
fontStyle="normal"
fontWeight="400"
fontSize="12px"
lineHeight="140%"
border={'1px solid #DEE0E2'}
bg={'#F6F8F9'}
_expanded={{
background: '#F8FAFB',
border: `1px solid #36ADEF`
}}
isDisabled={isFetching}
_hover={{
background: '#F8FAFB',
border: `1px solid #36ADEF`
}}
borderRadius={'2px'}
>
{namespaceList[namespaceIdx]}
</Button>
</PopoverTrigger>
<PopoverContent
p={'6px'}
boxSizing="border-box"
w={'110px'}
shadow={'0px 0px 1px 0px #798D9F40, 0px 2px 4px 0px #A1A7B340'}
border={'none'}
>
{namespaceList.map((v, idx) => (
<Button
key={v}
{...(idx === namespaceIdx
? {
color: '#0884DD',
bg: '#F4F6F8'
}
: {
color: '#5A646E',
bg: '#FDFDFE'
})}
h="30px"
fontFamily="PingFang SC"
fontSize="12px"
fontWeight="400"
lineHeight="18px"
p={'0'}
isDisabled={isFetching}
onClick={() => {
setcurrentPage(1);
setNamespaceIdx(idx);
}}
>
{v}
</Button>
))}
</PopoverContent>
</Popover>
</Flex>{' '}
</Flex>
<Flex mt="24px" alignItems={'center'} flexWrap={'wrap'}>
<Flex align={'center'} mb={'24px'}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export type CRDMeta = {
plural: string; // type
};

export async function GetCRD<T = any>(kc: k8s.KubeConfig, meta: CRDMeta, name: string) {
export async function GetCRD(kc: k8s.KubeConfig, meta: CRDMeta, name: string) {
return kc.makeApiClient(k8s.CustomObjectsApi).getNamespacedCustomObject(
meta.group,
meta.version,
Expand Down
2 changes: 2 additions & 0 deletions frontend/providers/costcenter/src/types/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type BillingSpec =
endTime: string;
type: 0 | 1 | -1 | 2 | 3; //0为扣费,1为充值;用于billing数据查找:如为-1则查找type为0和1的数据,如果给定type值则查找type为给定值的数据
owner?: string; //用于billing数据中查找的owner字段值
namespace?: string;
}
| {
orderID: string; //如果给定orderId,则查找该id的值,该值为唯一值,因此当orderId给定时忽略其他查找限定值
Expand All @@ -24,6 +25,7 @@ export type BillingItem<T = Costs> = {
order_id: string;
owner: string;
time: string;
namespace: string;
type: 0 | -1 | 1 | 2 | 3;
};
export type BillingData<T = Costs> = {
Expand Down

0 comments on commit 3f883f1

Please sign in to comment.