From e6bf29b3c54b0bcf863532c57772ff7ce888eff9 Mon Sep 17 00:00:00 2001 From: xudaotutou <13435638964@163.com> Date: Tue, 9 Jan 2024 22:56:01 +0800 Subject: [PATCH] refactor(costcenter): service api (#4475) --- .../src/components/billing/NamespaceMenu.tsx | 24 +++- .../src/components/cost_overview/buget.tsx | 45 +++++--- .../cost_overview/components/lineChart.tsx | 60 ++++------ .../cost_overview/components/pieChart.tsx | 73 +++--------- .../src/components/cost_overview/cost.tsx | 46 +++++--- .../src/components/cost_overview/trend.tsx | 20 +++- .../providers/costcenter/src/pages/_app.tsx | 37 +++--- .../costcenter/src/pages/api/billing/buget.ts | 61 ++++++++++ .../costcenter/src/pages/api/billing/costs.ts | 46 ++++++++ .../pages/api/billing/getNamespaceList.tsx | 108 +++++------------- .../pages/api/billing/propertiesUsedAmount.ts | 46 ++++++++ .../costcenter/src/pages/api/properties.ts | 30 +++++ .../costcenter/src/pages/billing/index.tsx | 12 +- .../src/pages/cost_overview/index.tsx | 8 +- .../costcenter/src/pages/valuation/index.tsx | 57 ++++----- .../providers/costcenter/src/types/billing.ts | 10 ++ .../costcenter/src/types/valuation.ts | 2 +- 17 files changed, 397 insertions(+), 288 deletions(-) create mode 100644 frontend/providers/costcenter/src/pages/api/billing/buget.ts create mode 100644 frontend/providers/costcenter/src/pages/api/billing/costs.ts create mode 100644 frontend/providers/costcenter/src/pages/api/billing/propertiesUsedAmount.ts create mode 100644 frontend/providers/costcenter/src/pages/api/properties.ts diff --git a/frontend/providers/costcenter/src/components/billing/NamespaceMenu.tsx b/frontend/providers/costcenter/src/components/billing/NamespaceMenu.tsx index 2d6c7ae53..88c5284d5 100644 --- a/frontend/providers/costcenter/src/components/billing/NamespaceMenu.tsx +++ b/frontend/providers/costcenter/src/components/billing/NamespaceMenu.tsx @@ -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>('/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 ( @@ -76,7 +90,7 @@ export default function NamespaceMenu({ isDisabled={isDisabled} onClick={() => { setNamespaceIdx(idx); - setNamespace(idx === 0 ? '' : namespaceList[namespaceIdx + 1]); + setNamespace(idx === 0 ? '' : v); }} > {v} diff --git a/frontend/providers/costcenter/src/components/cost_overview/buget.tsx b/frontend/providers/costcenter/src/components/cost_overview/buget.tsx index adf30d72c..c53d08ad7 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/buget.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/buget.tsx @@ -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 ( diff --git a/frontend/providers/costcenter/src/components/cost_overview/components/lineChart.tsx b/frontend/providers/costcenter/src/components/cost_overview/components/lineChart.tsx index 3d1310dc8..b1cca176c 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/components/lineChart.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/components/lineChart.tsx @@ -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'; diff --git a/frontend/providers/costcenter/src/components/cost_overview/components/pieChart.tsx b/frontend/providers/costcenter/src/components/cost_overview/components/pieChart.tsx index dcf5fa1ae..1fc053382 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/components/pieChart.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/components/pieChart.tsx @@ -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 ( ); diff --git a/frontend/providers/costcenter/src/components/cost_overview/cost.tsx b/frontend/providers/costcenter/src/components/cost_overview/cost.tsx index fd2b4d1fd..e9d44da3b 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/cost.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/cost.tsx @@ -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('/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 ( @@ -26,13 +40,13 @@ export const Cost = memo(function Cost() { {t('Cost Distribution')} - {isInitialLoading || !_deduction ? ( + {isInitialLoading || !data ? ( ) : ( - + )} ); -}); +}; diff --git a/frontend/providers/costcenter/src/components/cost_overview/trend.tsx b/frontend/providers/costcenter/src/components/cost_overview/trend.tsx index 86cc5f543..82001500c 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/trend.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/trend.tsx @@ -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 ( {t('Cost Trend')} - {isInitialLoading || !data?.data?.status.item?.length ? ( + {isInitialLoading || !data ? ( <> ) : ( - + )} diff --git a/frontend/providers/costcenter/src/pages/_app.tsx b/frontend/providers/costcenter/src/pages/_app.tsx index de64f7bd9..ec08b1e38 100644 --- a/frontend/providers/costcenter/src/pages/_app.tsx +++ b/frontend/providers/costcenter/src/pages/_app.tsx @@ -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 () => { diff --git a/frontend/providers/costcenter/src/pages/api/billing/buget.ts b/frontend/providers/costcenter/src/pages/api/billing/buget.ts new file mode 100644 index 000000000..ba3239319 --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/billing/buget.ts @@ -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' }); + } +} diff --git a/frontend/providers/costcenter/src/pages/api/billing/costs.ts b/frontend/providers/costcenter/src/pages/api/billing/costs.ts new file mode 100644 index 000000000..3cf239242 --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/billing/costs.ts @@ -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' }); + } +} diff --git a/frontend/providers/costcenter/src/pages/api/billing/getNamespaceList.tsx b/frontend/providers/costcenter/src/pages/api/billing/getNamespaceList.tsx index ea6d5ae7c..bca183e0e 100644 --- a/frontend/providers/costcenter/src/pages/api/billing/getNamespaceList.tsx +++ b/frontend/providers/costcenter/src/pages/api/billing/getNamespaceList.tsx @@ -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((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((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; - 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); diff --git a/frontend/providers/costcenter/src/pages/api/billing/propertiesUsedAmount.ts b/frontend/providers/costcenter/src/pages/api/billing/propertiesUsedAmount.ts new file mode 100644 index 000000000..4b796232f --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/billing/propertiesUsedAmount.ts @@ -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' }); + } +} diff --git a/frontend/providers/costcenter/src/pages/api/properties.ts b/frontend/providers/costcenter/src/pages/api/properties.ts new file mode 100644 index 000000000..4f08ce6b7 --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/properties.ts @@ -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' }); + } +} diff --git a/frontend/providers/costcenter/src/pages/billing/index.tsx b/frontend/providers/costcenter/src/pages/billing/index.tsx index b0b476fd5..df65d388f 100644 --- a/frontend/providers/costcenter/src/pages/billing/index.tsx +++ b/frontend/providers/costcenter/src/pages/billing/index.tsx @@ -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 ( @@ -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'])) } }; } diff --git a/frontend/providers/costcenter/src/pages/cost_overview/index.tsx b/frontend/providers/costcenter/src/pages/cost_overview/index.tsx index 07baac7d7..40e6db1fc 100644 --- a/frontend/providers/costcenter/src/pages/cost_overview/index.tsx +++ b/frontend/providers/costcenter/src/pages/cost_overview/index.tsx @@ -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 | 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'])) } }; } diff --git a/frontend/providers/costcenter/src/pages/valuation/index.tsx b/frontend/providers/costcenter/src/pages/valuation/index.tsx index b50c76d8b..3969e970e 100644 --- a/frontend/providers/costcenter/src/pages/valuation/index.tsx +++ b/frontend/providers/costcenter/src/pages/valuation/index.tsx @@ -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>('/api/price'); - function CycleMenu({ cycleIdx, setCycleIdx, @@ -129,31 +124,27 @@ function CycleMenu({ ); } - +const getValuation = () => + request>('/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((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) } }; diff --git a/frontend/providers/costcenter/src/types/billing.ts b/frontend/providers/costcenter/src/types/billing.ts index d9ac94fb7..19e333f70 100644 --- a/frontend/providers/costcenter/src/types/billing.ts +++ b/frontend/providers/costcenter/src/types/billing.ts @@ -57,3 +57,13 @@ export type BillingData = { rechargeAmount: number; }; }; +export type PropertiesCost = { + amount: { + cpu: number; + memory: number; + network: number; + gpu?: number; + 'services.nodeports': number; + storage: number; + }; +}; diff --git a/frontend/providers/costcenter/src/types/valuation.ts b/frontend/providers/costcenter/src/types/valuation.ts index da4244ceb..4cf96d3d3 100644 --- a/frontend/providers/costcenter/src/types/valuation.ts +++ b/frontend/providers/costcenter/src/types/valuation.ts @@ -1,7 +1,7 @@ export type ValuationStandard = { name: string; unit: string; - price: string; + unit_price?: number; }; export type ValuationBillingRecord = { price: number;