mirror of
https://github.com/labring/sealos.git
synced 2026-09-24 15:46:19 +08:00
refactor(costcenter): service api (#4475)
This commit is contained in:
@@ -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'}>
|
||||
|
||||
+22
-38
@@ -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';
|
||||
|
||||
+13
-60
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user