refactor(costcenter): service api (#4475)

This commit is contained in:
xudaotutou
2024-01-09 22:56:01 +08:00
committed by GitHub
parent 24417328a1
commit e6bf29b3c5
17 changed files with 397 additions and 288 deletions
@@ -1,5 +1,5 @@
import request from '@/service/request';
import { ApiResp } from '@/types';
import useOverviewStore from '@/stores/overview';
import { Button, Flex, Popover, PopoverContent, PopoverTrigger } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
@@ -12,15 +12,29 @@ export default function NamespaceMenu({
isDisabled: boolean;
setNamespace: (x: string) => void;
}) {
const startTime = useOverviewStore((s) => s.startTime);
const endTime = useOverviewStore((s) => s.endTime);
const [namespaceIdx, setNamespaceIdx] = useState(0);
const { data: nsListData } = useQuery({
queryFn() {
return request<any, ApiResp<{ nsList: string[] }>>('/api/billing/getNamespaceList');
return request.post('/api/billing/getNamespaceList', {
startTime,
endTime
});
},
queryKey: ['nsList']
queryKey: [
'nsList',
{
startTime,
endTime
}
]
});
const { t } = useTranslation();
const namespaceList: string[] = [t('All Namespace'), ...(nsListData?.data?.nsList || [])];
const namespaceList: string[] = [
t('All Namespace'),
...((nsListData?.data?.list as string[]) || [])
];
return (
<Flex align={'center'} ml="28px">
<Popover>
@@ -76,7 +90,7 @@ export default function NamespaceMenu({
isDisabled={isDisabled}
onClick={() => {
setNamespaceIdx(idx);
setNamespace(idx === 0 ? '' : namespaceList[namespaceIdx + 1]);
setNamespace(idx === 0 ? '' : v);
}}
>
{v}
@@ -5,31 +5,38 @@ import up_icon from '@/assert/ic_round-trending-up.svg';
import { useMemo } from 'react';
import { displayMoney, formatMoney } from '@/utils/format';
import { useTranslation } from 'next-i18next';
import useBillingData from '@/hooks/useBillingData';
import CurrencySymbol from '../CurrencySymbol';
import useEnvStore from '@/stores/env';
import useBillingData from '@/hooks/useBillingData';
import request from '@/service/request';
import { useQuery } from '@tanstack/react-query';
import useOverviewStore from '@/stores/overview';
// const getBuget = ()=>request.post('api/billing/buget',)
export function Buget() {
const { t } = useTranslation();
const { data } = useBillingData();
const startTime = useOverviewStore((state) => state.startTime);
const endTime = useOverviewStore((state) => state.endTime);
const { data, isSuccess } = useQuery({
queryKey: ['billing', 'buget', { startTime, endTime }],
queryFn: () => {
return request.post<{ amount: number }[]>('/api/billing/buget', {
startTime,
endTime
});
}
});
const currency = useEnvStore((s) => s.currency);
const [_out, _in] = useMemo(
() =>
(data?.data?.status.item || []).reduce<[number, number]>(
(pre, cur) => {
if (cur.type === 0) {
pre[0] += cur.amount;
} else if (cur.type === 1) {
pre[1] += cur.amount;
}
return pre;
},
[0, 0]
),
[data]
);
const list = [
{ title: 'Deduction', src: down_icon.src, value: formatMoney(_out) },
{ title: 'Charge', src: up_icon.src, value: formatMoney(_in) }
{
title: 'Deduction',
src: down_icon.src,
value: formatMoney(isSuccess ? data?.data[0].amount || 0 : 0)
},
{
title: 'Charge',
src: up_icon.src,
value: formatMoney(isSuccess ? data?.data[1].amount || 0 : 0)
}
];
return (
<Flex direction={'column'} mb={'34px'}>
@@ -11,12 +11,10 @@ import {
import { LineChart } from 'echarts/charts';
import { UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import { INITAL_SOURCE } from '@/constants/billing';
import { BillingItem } from '@/types/billing';
import { parseISO, format, isSameDay, startOfDay } from 'date-fns';
import { formatMoney } from '@/utils/format';
import { format } from 'date-fns';
import { useTranslation } from 'next-i18next';
import useOverviewStore from '@/stores/overview';
import { displayMoney, formatMoney } from '@/utils/format';
echarts.use([
GridComponent,
@@ -29,43 +27,32 @@ echarts.use([
UniversalTransition
]);
export default function Trend({ data }: { data: BillingItem[] }) {
export default function Trend({ data }: { data: [number, string][] }) {
const { t } = useTranslation();
const startTime = useOverviewStore((s) => s.startTime);
const endTime = useOverviewStore((s) => s.endTime);
const sourceValue =
data
.filter((v) => v.type === 0)
.map<[Date, number, number, number, number, number]>((item) => [
parseISO(item.time),
item.costs?.cpu || 0,
item.costs?.memory || 0,
item.costs?.storage || 0,
item.costs?.network || 0,
item.amount
])
// 归并为当天的开始
.reduce<[Date, number, number, number, number, number][]>((pre, cur) => {
if (pre.length !== 0) {
const precost = pre[pre.length - 1];
if (isSameDay(precost[0], cur[0])) {
cur.forEach((v, i) => i > 0 && ((precost[i] as number) += v as number));
return pre;
}
const source = [
['date', 'amount'],
...data
.reduce<[number, number][]>((pre, cur) => {
const len = pre.length;
const time = cur[0];
let val = parseInt(cur[1]);
if (len === 0) return [[time, val]];
if (pre[len - 1][0] === time) {
// multi namespace
pre[len - 1][1] += val;
} else {
pre.push([time, val]);
}
cur[0] = startOfDay(cur[0]);
pre.push(cur);
return pre;
}, [])
.map((x) => {
return x.map((v, i) => (i !== 0 ? formatMoney(v as number) : v));
}) || [];
const source = [...INITAL_SOURCE, ...sourceValue.reverse()];
}, [] as [number, number][])
.map(([date, val]) => [date * 1000, val / 1000000])
];
const option = {
xAxis: {
type: 'time',
minInterval: 24 * 60 * 60 * 1000, // 最小刻度为一天
// minInterval: 24 * 60 * 60 * 1000, // 最小刻度为一天
// maxInterval: 30 * 24 * 60 * 60 * 1000, // 最大刻度为一周
min: startTime,
max: endTime,
@@ -82,7 +69,6 @@ export default function Trend({ data }: { data: BillingItem[] }) {
color: 'rgba(107, 112, 120, 1)'
}
},
yAxis: {
name: '元',
type: 'value',
@@ -109,7 +95,7 @@ export default function Trend({ data }: { data: BillingItem[] }) {
}
},
dataset: {
dimensions: INITAL_SOURCE[0],
dimensions: ['date', 'amount'],
source
},
grid: {
@@ -129,11 +115,9 @@ export default function Trend({ data }: { data: BillingItem[] }) {
padding: '0px',
formatter: function (params: any) {
// 如果不允许使用 `innerHTML`,可以使用 DOM API 来创建和添加元素。例如:
const { data, value } = params[0];
const date = format(data[0], 'yyyy-MM-dd');
const totalCost = value[5];
const totalCost = value[1];
// 创建外层 div 元素
const resDom = document.createElement('div');
resDom.style.background = '#FFFFFF';
@@ -10,6 +10,14 @@ import { useBreakpointValue } from '@chakra-ui/react';
import { BillingData } from '@/types/billing';
import { useTranslation } from 'next-i18next';
import useEnvStore from '@/stores/env';
import {
ComposeOption,
DatasetComponentOption,
LabelLayoutOptionCallback,
LegendComponentOption,
PieSeriesOption,
TooltipComponentOption
} from 'echarts';
echarts.use([
TooltipComponent,
@@ -22,7 +30,7 @@ echarts.use([
export default function CostChart({ data }: { data: BillingData['status']['deductionAmount'] }) {
const { t } = useTranslation();
const { cpu = 0, memory = 0, storage = 0, gpu = 0, network = 0 } = data;
const { cpu = 0, memory = 0, storage = 0, gpu = 0, network = 0, port = 0 } = data;
const gpuEnabled = useEnvStore((state) => state.gpuEnabled);
const radius = useBreakpointValue({
xl: ['45%', '70%'],
@@ -43,18 +51,17 @@ export default function CostChart({ data }: { data: BillingData['status']['deduc
['memory', formatMoney(memory).toFixed(2)],
['storage', formatMoney(storage).toFixed(2)],
['network', formatMoney(network).toFixed(2)],
['port', formatMoney(port).toFixed(2)],
...(gpuEnabled ? [['gpu', formatMoney(gpu).toFixed(2)]] : [])
],
[cpu, memory, storage, gpu, gpuEnabled]
);
const amount = formatMoney(cpu + memory + storage + gpu);
const amount = formatMoney(cpu + memory + storage + gpu + port + network);
const publicOption = {
name: 'Cost Form',
radius: radius || ['45%', '70%'],
avoidLabelOverlap: false,
center: ['50%', '60%'],
// selectedMode: 'single',
left: 'left',
emptyCircleStyle: {
borderCap: 'ronud'
@@ -71,21 +78,10 @@ export default function CostChart({ data }: { data: BillingData['status']['deduc
legend: {
top: '10%'
},
color: ['#24282C', '#485058', '#7B838B', '#BDC1C5'],
color: ['#24282C', '#485058', '#7B838B', '#BDC1C5', '#9CA2A8', '#DEE0E2'],
series: [
{
type: 'pie',
// label: {
// show:true,
// formatter: '{b}\n¥{@cost}\n({d}%)',
// lineHeight: 15,
// },
// label: {
// show: false,
// // position: 'center'
// },
emphasis: {
label: {
show: false
@@ -95,54 +91,16 @@ export default function CostChart({ data }: { data: BillingData['status']['deduc
show: false,
fontSize: 14
},
labelLine: {
show: false
},
// label: {
// formatter: '{a|{a}}{abg|}\n{hr|}\n {b|{b}:}{c} {per|{d}%} ',
// backgroundColor: '#F6F8FC',
// borderColor: '#8C8D8E',
// borderWidth: 1,
// borderRadius: 4,
// rich: {
// a: {
// color: '#6E7079',
// lineHeight: 22,
// align: 'center'
// },
// hr: {
// borderColor: '#8C8D8E',
// width: '100%',
// borderWidth: 1,
// height: 0
// },
// b: {
// color: '#4C5058',
// fontSize: 14,
// fontWeight: 'bold',
// lineHeight: 33
// },
// per: {
// color: '#fff',
// backgroundColor: '#4C5058',
// padding: [3, 4],
// borderRadius: 4
// }
// }
// },
// emphasis: {
// label:{
// show:true
// }
// },
encode: {
itemName: 'name',
value: 'cost'
},
itemStyle: {
borderWidth: 1,
borderColor: '#fff',
borderColor: 'rgba(0,0,0,0)',
left: 0
},
...publicOption
@@ -172,12 +130,8 @@ export default function CostChart({ data }: { data: BillingData['status']['deduc
itemName: 'name',
value: 'cost'
}
// itemStyle: {
// borderWidth: 0,
// }
}
]
// ]
};
return (
<ReactEChartsCore
@@ -189,7 +143,6 @@ export default function CostChart({ data }: { data: BillingData['status']['deduc
aspectRatio,
width: '100%',
flex: 1
// pointerEvents: 'none'
}}
/>
);
@@ -1,24 +1,38 @@
import { Flex, Text } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { memo } from 'react';
import Notfound from '@/components/notFound';
import useBillingData from '@/hooks/useBillingData';
import { useQuery } from '@tanstack/react-query';
import useOverviewStore from '@/stores/overview';
import request from '@/service/request';
import { PropertiesCost } from '@/types';
const Chart = dynamic(() => import('./components/pieChart'), {
ssr: false
});
export const Cost = memo(function Cost() {
export const Cost = function Cost() {
const { t } = useTranslation();
const { data, isInitialLoading } = useBillingData();
const _deduction = data?.data?.status.deductionAmount;
const deductionAmount = {
cpu: _deduction?.cpu || 0,
memory: _deduction?.memory || 0,
storage: _deduction?.storage || 0,
network: _deduction?.network || 0,
gpu: _deduction?.gpu || 0,
port: _deduction?.port || 0
};
const startTime = useOverviewStore((state) => state.startTime);
const endTime = useOverviewStore((state) => state.endTime);
const { data, isInitialLoading } = useQuery({
queryKey: ['billing', 'properties', 'costs', { startTime, endTime }],
queryFn: () => {
return request.post<PropertiesCost>('/api/billing/propertiesUsedAmount', {
startTime,
endTime
});
},
select(data) {
const _data = data.data.amount;
return {
cpu: _data.cpu || 0,
memory: _data.memory || 0,
storage: _data.storage || 0,
network: _data.network || 0,
gpu: _data.gpu || 0,
port: _data['services.nodeports'] || 0
};
}
});
return (
<Flex direction={'column'} flex={1}>
<Flex alignItems={'center'} justify={'space-between'}>
@@ -26,13 +40,13 @@ export const Cost = memo(function Cost() {
{t('Cost Distribution')}
</Text>
</Flex>
{isInitialLoading || !_deduction ? (
{isInitialLoading || !data ? (
<Flex justify={'center'} align={'center'} flex={1}>
<Notfound></Notfound>
</Flex>
) : (
<Chart data={deductionAmount}></Chart>
<Chart data={data}></Chart>
)}
</Flex>
);
});
};
@@ -4,25 +4,37 @@ import dynamic from 'next/dynamic';
import chart7 from '@/assert/Chart7.svg';
import { memo } from 'react';
import Notfound from '@/components/notFound';
import useBillingData from '@/hooks/useBillingData';
import useOverviewStore from '@/stores/overview';
import { useQuery } from '@tanstack/react-query';
import request from '@/service/request';
const LineChart = dynamic(() => import('./components/lineChart'), { ssr: false });
export const Trend = memo(function Trend() {
const { t } = useTranslation();
const { data, isInitialLoading } = useBillingData();
const startTime = useOverviewStore((state) => state.startTime);
const endTime = useOverviewStore((state) => state.endTime);
const { data, isInitialLoading } = useQuery({
queryKey: ['billing', 'trend', { startTime, endTime }],
queryFn: () => {
return request.post<{ costs: [number, string][] }>('/api/billing/costs', {
startTime,
endTime
});
}
});
return (
<Box>
<Flex justify={'space-between'} align={'center'}>
<Heading size={'sm'}> {t('Cost Trend')}</Heading>
</Flex>
<Flex height="310px" width={'100%'} justify={'center'} align={'center'} direction={'column'}>
{isInitialLoading || !data?.data?.status.item?.length ? (
{isInitialLoading || !data ? (
<>
<Img src={chart7.src}></Img>
<Notfound></Notfound>
</>
) : (
<LineChart data={data?.data?.status.item || []}></LineChart>
<LineChart data={data?.data?.costs || []}></LineChart>
)}
</Flex>
</Box>
@@ -8,7 +8,7 @@ import { ChakraProvider } from '@chakra-ui/react';
// import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { AppProps } from 'next/app';
import Router from 'next/router';
import Router, { useRouter } from 'next/router';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import 'react-day-picker/dist/style.css';
@@ -31,17 +31,6 @@ const queryClient = new QueryClient({
}
}
});
// if (typeof window !== 'undefined') {
// const syncStoragePersister = createSyncStoragePersister({
// storage: window.localStorage,
// retry: removeOldestQuery
// });
// persistQueryClient({
// persister: syncStoragePersister,
// queryClient: queryClient
// });
// }
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
@@ -49,14 +38,20 @@ Router.events.on('routeChangeError', () => NProgress.done());
const App = ({ Component, pageProps }: AppProps) => {
const state = useEnvStore();
const router = useRouter();
useEffect(() => {
const changeI18n = (data: any) => {
setCookie('NEXT_LOCALE', data.currentLanguage, {
expires: 30,
sameSite: 'None',
secure: true
});
i18n?.changeLanguage(data.currentLanguage);
const changeI18n = (data: { currentLanguage: string }) => {
// setCookie('NEXT_LOCALE', data.currentLanguage, {
// expires: 30,
// sameSite: 'None',
// secure: true
// });
console.log(data);
router.replace(router.basePath, router.asPath, { locale: data.currentLanguage });
// router.replace(router.asPath,router.asPath, {
// locale: data.currentLanguage
// })
// i18n?.changeLanguage(data.currentLanguage);
};
(async () => {
@@ -66,7 +61,9 @@ const App = ({ Component, pageProps }: AppProps) => {
currentLanguage: lang.lng
});
} catch (error) {
changeI18n('zh');
changeI18n({
currentLanguage: 'zh'
});
}
})();
(async () => {
@@ -0,0 +1,61 @@
import { authSession } from '@/service/backend/auth';
import { jsonRes } from '@/service/backend/response';
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, resp: NextApiResponse) {
try {
const kc = await authSession(req.headers);
const user = kc.getCurrentUser();
if (user === null) {
return jsonRes(resp, { code: 403, message: 'user null' });
}
const { endTime, startTime } = req.body as {
endTime?: Date;
startTime?: Date;
};
if (!endTime)
return jsonRes(resp, {
code: 400,
message: 'endTime is invalid'
});
if (!startTime)
return jsonRes(resp, {
code: 400,
message: 'endTime is invalid'
});
const consumptionUrl = process.env.BILLING_URI + '/account/v1alpha1/costs/consumption';
const rechagreUrl = process.env.BILLING_URI + '/account/v1alpha1/costs/recharge';
const results = await Promise.all([
(
await fetch(consumptionUrl, {
method: 'POST',
body: JSON.stringify({
endTime,
kubeConfig: kc.exportConfig(),
owner: user.name,
startTime
})
})
).json(),
(
await fetch(rechagreUrl, {
method: 'POST',
body: JSON.stringify({
endTime,
kubeConfig: kc.exportConfig(),
owner: user.name,
startTime
})
})
).json()
]);
return jsonRes(resp, {
code: 200,
data: results
});
} catch (error) {
console.log(error);
jsonRes(resp, { code: 500, message: 'get buget error' });
}
}
@@ -0,0 +1,46 @@
import { authSession } from '@/service/backend/auth';
import { jsonRes } from '@/service/backend/response';
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, resp: NextApiResponse) {
try {
const kc = await authSession(req.headers);
const user = kc.getCurrentUser();
if (user === null) {
return jsonRes(resp, { code: 403, message: 'user null' });
}
const { endTime, startTime } = req.body as {
endTime?: Date;
startTime?: Date;
};
if (!endTime)
return jsonRes(resp, {
code: 400,
message: 'endTime is invalid'
});
if (!startTime)
return jsonRes(resp, {
code: 400,
message: 'endTime is invalid'
});
const url = process.env.BILLING_URI + '/account/v1alpha1/costs';
const res = await (
await fetch(url, {
method: 'POST',
body: JSON.stringify({
endTime,
kubeConfig: kc.exportConfig(),
owner: user.name,
startTime
})
})
).json();
return jsonRes(resp, {
code: 200,
data: res.data,
message: res.message
});
} catch (error) {
console.log(error);
jsonRes(resp, { code: 500, message: 'get billing cost error' });
}
}
@@ -1,42 +1,6 @@
import { authSession } from '@/service/backend/auth';
import { CRDMeta, GetCRD, GetUserDefaultNameSpace } 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);
@@ -44,51 +8,39 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse
if (user === null) {
return jsonRes(resp, { code: 403, message: 'user null' });
}
// 要和kc保持一致
const namespace = kc.getContexts()[0].namespace || GetUserDefaultNameSpace(user.name);
const name = new Date().getTime() + 'namespacequery';
const crdSchema = {
apiVersion: `account.sealos.io/v1`,
kind: 'BillingInfoQuery',
metadata: {
name
},
spec: {
queryType: 'NamespacesHistory'
}
const { endTime, startTime } = req.body as {
endTime?: Date;
startTime?: Date;
};
const meta: CRDMeta = {
group: 'account.sealos.io',
version: 'v1',
namespace,
plural: 'billinginfoqueries'
};
try {
await ApplyYaml(kc, yaml.dump(crdSchema));
} catch (err) {
console.log('error', err);
}
const result = await new Promise<string>((resolve, reject) => {
let retry = 3;
const wrap = () =>
GetCRD(kc, meta, name)
.then((res) => {
const body = res.body as { status: any };
const { result, status } = body.status as Record<string, string>;
if (status.toLocaleLowerCase() === 'completed') resolve(result as string);
else return Promise.reject();
})
.catch((err) => {
if (retry-- >= 0) wrap();
else reject(err);
});
wrap();
});
if (!endTime)
return jsonRes(resp, {
code: 400,
message: 'endTime is invalid'
});
if (!startTime)
return jsonRes(resp, {
code: 400,
message: 'endTime is invalid'
});
const url = process.env.BILLING_URI + '/account/v1alpha1/namespaces';
console.log(url);
const res = await (
await fetch(url, {
method: 'POST',
body: JSON.stringify({
endTime,
kubeConfig: kc.exportConfig(),
owner: user.name,
startTime,
type: 0
})
})
).json();
console.log(res);
return jsonRes(resp, {
code: 200,
data: {
nsList: JSON.parse(result)
}
data: res.data,
message: res.message
});
} catch (error) {
console.log(error);
@@ -0,0 +1,46 @@
import { authSession } from '@/service/backend/auth';
import { jsonRes } from '@/service/backend/response';
import { PropertiesCost } from '@/types';
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, resp: NextApiResponse) {
try {
const kc = await authSession(req.headers);
const user = kc.getCurrentUser();
if (user === null) {
return jsonRes(resp, { code: 403, message: 'user null' });
}
const { endTime, startTime } = req.body as {
endTime?: Date;
startTime?: Date;
};
if (!endTime)
return jsonRes(resp, {
code: 400,
message: 'endTime is invalid'
});
if (!startTime)
return jsonRes(resp, {
code: 400,
message: 'endTime is invalid'
});
const url = process.env.BILLING_URI + '/account/v1alpha1/costs/properties';
const res = (await (
await fetch(url, {
method: 'POST',
body: JSON.stringify({
endTime,
kubeConfig: kc.exportConfig(),
owner: user.name,
startTime
})
})
).json()) as PropertiesCost;
return jsonRes(resp, {
code: 200,
data: res
});
} catch (error) {
console.log(error);
jsonRes(resp, { code: 500, message: 'get billing cost error' });
}
}
@@ -0,0 +1,30 @@
import { authSession } from '@/service/backend/auth';
import { jsonRes } from '@/service/backend/response';
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, resp: NextApiResponse) {
try {
const kc = await authSession(req.headers);
const user = kc.getCurrentUser();
if (user === null) {
return jsonRes(resp, { code: 403, message: 'user null' });
}
const url = process.env.BILLING_URI + '/account/v1alpha1/properties';
const res = await (
await fetch(url, {
method: 'POST',
body: JSON.stringify({
kubeConfig: kc.exportConfig(),
owner: user.name
})
})
).json();
return jsonRes(resp, {
code: 200,
data: res.data,
message: res.message
});
} catch (error) {
console.log(error);
jsonRes(resp, { code: 500, message: 'get billing cost error' });
}
}
@@ -21,7 +21,6 @@ import SelectRange from '@/components/billing/selectDateRange';
import useOverviewStore from '@/stores/overview';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';
import { getCookie } from '@/utils/cookieUtils';
import NotFound from '@/components/notFound';
import NamespaceMenu from '@/components/billing/NamespaceMenu';
import TypeMenu from '@/components/billing/TypeMenu';
@@ -273,11 +272,7 @@ function TransferTabPanel({ namespace }: { namespace: string }) {
);
}
function Billing() {
const { t, i18n } = useTranslation();
const cookie = getCookie('NEXT_LOCALE');
useEffect(() => {
i18n.changeLanguage(cookie);
}, [cookie, i18n]);
const { t } = useTranslation();
const [namespace, setNamespace] = useState('');
return (
<Flex flexDirection="column" w="100%" h="100%" bg={'white'} p="24px" overflow={'auto'}>
@@ -315,11 +310,10 @@ function Billing() {
}
export default Billing;
export async function getServerSideProps(content: any) {
const locale = content?.req?.cookies?.NEXT_LOCALE || 'zh';
export async function getServerSideProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, undefined, null, content.locales))
...(await serverSideTranslations(locale, undefined, null, ['zh', 'en']))
}
};
}
@@ -14,6 +14,9 @@ import NotFound from '@/components/notFound';
import { useRouter } from 'next/router';
import useOverviewStore from '@/stores/overview';
import { CommonBillingTable } from '@/components/billing/billingTable';
import { QueryClient } from '@tanstack/react-query';
import request from '@/service/request';
const getProperties = () => request.post('/api/billing/propertiesUsedAmount');
export const RechargeContext = createContext<{ rechargeRef: MutableRefObject<any> | null }>({
rechargeRef: null
@@ -123,11 +126,10 @@ function CostOverview() {
);
}
export async function getServerSideProps(content: any) {
const locale = content?.req?.cookies?.NEXT_LOCALE || 'zh';
export async function getServerSideProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, undefined, null, content.locales))
...(await serverSideTranslations(locale, undefined, null, ['zh', 'en']))
}
};
}
@@ -23,14 +23,12 @@ import {
import letter_icon from '@/assert/format_letter_spacing_standard_black.svg';
import { QueryClient, dehydrate, useQuery } from '@tanstack/react-query';
import request from '@/service/request';
import { ValuationBillingRecord } from '@/types/valuation';
import { valuationMap } from '@/constants/payment';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { i18n, useTranslation } from 'next-i18next';
import { useTranslation } from 'next-i18next';
import { ApiResp } from '@/types/api';
import nvidaIcon from '@/assert/bi_nvidia.svg';
import { getCookie } from '@/utils/cookieUtils';
import { CYCLE } from '@/constants/valuation';
import PredictCard from '@/components/valuation/predictCard';
import useEnvStore from '@/stores/env';
@@ -42,7 +40,7 @@ import { MemoryIcon } from '@/components/icons/MemoryIcon';
import { NetworkIcon } from '@/components/icons/NetworkIcon';
import { StorageIcon } from '@/components/icons/StorageIcon';
import { PortIcon } from '@sealos/ui';
import OuterLink from '@/components/outerLink';
import { ValuationStandard } from '@/types/valuation';
type CardItem = {
title: string;
@@ -53,9 +51,6 @@ type CardItem = {
icon: typeof Icon;
};
const getValuation = () =>
request<any, ApiResp<{ billingRecords: ValuationBillingRecord[] }>>('/api/price');
function CycleMenu({
cycleIdx,
setCycleIdx,
@@ -129,31 +124,27 @@ function CycleMenu({
</Flex>
);
}
const getValuation = () =>
request<any, ApiResp<{ properties: ValuationStandard[] }>>('/api/properties');
function Valuation() {
const { t, i18n } = useTranslation();
const cookie = getCookie('NEXT_LOCALE');
const { t } = useTranslation();
const gpuEnabled = useEnvStore((state) => state.gpuEnabled);
const [cycleIdx, setCycleIdx] = useState(0);
const currency = useEnvStore((s) => s.currency);
useEffect(() => {
i18n.changeLanguage(cookie);
}, [cookie, i18n]);
const { data: _data } = useQuery(['valuation'], getValuation);
const data =
_data?.data?.billingRecords
?.filter((x) => !x.resourceType.startsWith('gpu-'))
_data?.data?.properties
?.filter((x) => !x.name.startsWith('gpu-'))
?.flatMap<CardItem>((x) => {
const props = valuationMap.get(x.resourceType);
const props = valuationMap.get(x.name);
if (!props) return [];
let icon;
let title = x.resourceType;
if (x.resourceType === 'cpu') icon = CpuIcon;
else if (x.resourceType === 'memory') icon = MemoryIcon;
else if (x.resourceType === 'network') icon = NetworkIcon;
else if (x.resourceType === 'storage') icon = StorageIcon;
else if (x.resourceType === 'services.nodeports') {
let title = x.name;
if (x.name === 'cpu') icon = CpuIcon;
else if (x.name === 'memory') icon = MemoryIcon;
else if (x.name === 'network') icon = NetworkIcon;
else if (x.name === 'storage') icon = StorageIcon;
else if (x.name === 'services.nodeports') {
icon = PortIcon;
title = 'Port';
} else return [];
@@ -161,7 +152,7 @@ function Valuation() {
{
title,
price: [1, 24, 168, 720, 8760].map(
(v) => Math.floor(v * x.price * (props.scale || 1)) / 1000000
(v) => Math.floor(v * (x.unit_price || 0) * (props.scale || 1)) / 1000000
),
unit: props.unit,
bg: props.bg,
@@ -173,11 +164,11 @@ function Valuation() {
?.sort((a, b) => a.idx - b.idx) || [];
const gpuProps = valuationMap.get('gpu')!;
const gpuData = gpuEnabled
? _data?.data?.billingRecords
?.filter((x) => x.resourceType.startsWith('gpu-'))
? _data?.data?.properties
?.filter((x) => x.name.startsWith('gpu-'))
?.map((x) => {
const name = x.resourceType.replace('gpu-', '').replace('_', ' ');
const price = (x.price * (gpuProps.scale || 1)) / 1000000;
const name = x.name.replace('gpu-', '').replace('_', ' ');
const price = ((x.unit_price || 0) * (gpuProps.scale || 1)) / 1000000;
return {
name,
price
@@ -334,16 +325,12 @@ function Valuation() {
export default Valuation;
export async function getServerSideProps(content: any) {
const locale = content?.req?.cookies?.NEXT_LOCALE || 'zh';
process.env.NODE_ENV === 'development' && i18n?.reloadResources(locale, undefined);
export async function getServerSideProps({ locale }: { locale: string }) {
const queryClient = new QueryClient();
await queryClient.prefetchQuery(['valuation'], getValuation);
return {
props: {
...(await serverSideTranslations(locale, undefined, null, content.locales)),
...(await serverSideTranslations(locale, ['common'], null, ['zh', 'en'])),
dehydratedState: dehydrate(queryClient)
}
};
@@ -57,3 +57,13 @@ export type BillingData<T = Costs> = {
rechargeAmount: number;
};
};
export type PropertiesCost = {
amount: {
cpu: number;
memory: number;
network: number;
gpu?: number;
'services.nodeports': number;
storage: number;
};
};
@@ -1,7 +1,7 @@
export type ValuationStandard = {
name: string;
unit: string;
price: string;
unit_price?: number;
};
export type ValuationBillingRecord = {
price: number;