From 955f25f05955b1d9d6c3362a45f4f237678253b7 Mon Sep 17 00:00:00 2001 From: xudaotutou <13435638964@163.com> Date: Fri, 2 Aug 2024 11:34:38 +0800 Subject: [PATCH] feat:update costcenter --- .../AccountCenter/SmsModify/SmsBind.tsx | 4 +- .../AccountCenter/SmsModify/SmsChange.tsx | 3 +- .../AccountCenter/SmsModify/SmsUnbind.tsx | 4 +- .../account/AccountCenter/index.tsx | 3 +- .../src/components/team/RemoveMember.tsx | 4 +- .../desktop/src/components/team/userTable.tsx | 8 +- .../ui/src/components/icons/PortIcon.tsx | 26 +- frontend/pnpm-lock.yaml | 3 + .../applaunchpad/next-i18next.config.js | 2 +- .../costcenter/.vscode/settings.json | 6 + .../costcenter/data/config.yaml.local | 26 - frontend/providers/costcenter/next.config.js | 1 - frontend/providers/costcenter/package.json | 1 + .../costcenter/public/locales/en/applist.json | 4 +- .../costcenter/public/locales/en/common.json | 43 +- .../costcenter/public/locales/zh/applist.json | 4 +- .../costcenter/public/locales/zh/common.json | 47 +- .../costcenter/src/assert/Vector.svg | 2 +- .../src/assert/Vector_disbabled.svg | 1 + .../providers/costcenter/src/assert/app.svg | 25 + .../costcenter/src/assert/clander.svg | 5 +- .../providers/costcenter/src/assert/cpu.svg | 5 + .../costcenter/src/assert/cronjob.svg | 15 + .../providers/costcenter/src/assert/cvm.svg | 25 + .../costcenter/src/assert/dashboard.svg | 3 + .../costcenter/src/assert/dashboard_black.svg | 3 + .../providers/costcenter/src/assert/db.svg | 20 + .../assert/format_letter_spacing_standard.svg | 10 +- .../format_letter_spacing_standard_black.svg | 10 +- .../costcenter/src/assert/invoice-active.svg | 7 +- .../costcenter/src/assert/invoice.svg | 15 +- .../providers/costcenter/src/assert/job.svg | 25 + .../costcenter/src/assert/layers.svg | 4 + .../costcenter/src/assert/layers_black.svg | 4 + .../costcenter/src/assert/lineChart.svg | 5 + .../costcenter/src/assert/lineChart_black.svg | 5 + .../costcenter/src/assert/memory.svg | 11 + .../costcenter/src/assert/network.svg | 5 + .../costcenter/src/assert/objectstorage.svg | 10 + .../providers/costcenter/src/assert/port.svg | 8 + .../costcenter/src/assert/receipt_long.svg | 10 +- .../src/assert/receipt_long_black.svg | 10 +- .../costcenter/src/assert/sealos.svg | 15 + .../costcenter/src/assert/sealos_coin.png | Bin 0 -> 5214 bytes .../costcenter/src/assert/storage.svg | 5 + .../costcenter/src/assert/terminal.svg | 10 + .../providers/costcenter/src/assert/to.svg | 5 + .../costcenter/src/assert/toleft.svg | 2 +- .../costcenter/src/assert/toleft_disabled.svg | 1 + .../src/components/CheckoutForm.tsx | 1 - .../src/components/CurrencySymbol.tsx | 55 +-- .../src/components/RechargeModal.tsx | 189 +++++--- .../costcenter/src/components/Refresh.tsx | 33 ++ .../src/components/TransferModal.tsx | 98 ++-- .../src/components/billing/AmountDisplay.tsx | 70 ++- .../src/components/billing/AppMenu.tsx | 103 ---- .../components/billing/BillingLineChart.tsx | 179 +++++++ .../src/components/billing/BillingTrend.tsx | 55 +++ .../src/components/billing/InOutTabPanel.tsx | 230 +++++---- .../src/components/billing/NamespaceMenu.tsx | 103 ---- .../components/billing/RechargeTabPanel.tsx | 11 +- .../src/components/billing/SwitchPage.tsx | 92 ++-- .../components/billing/TransferTabPnel.tsx | 42 +- .../src/components/billing/TypeMenu.tsx | 89 +--- .../src/components/billing/billingDetails.tsx | 3 - .../src/components/billing/billingTable.tsx | 117 +---- .../components/billing/selectDateRange.tsx | 76 ++- .../src/components/cost_overview/buget.tsx | 60 ++- .../cost_overview/components/barChart.tsx | 135 ++++++ .../cost_overview/components/lineChart.tsx | 213 ++++---- .../cost_overview/components/pieChart.tsx | 115 ++--- .../components/quotaPieChart.tsx | 74 +++ .../cost_overview/components/user.tsx | 36 +- .../src/components/cost_overview/cost.tsx | 80 ++- .../src/components/cost_overview/trend.tsx | 57 ++- .../src/components/cost_overview/trendBar.tsx | 99 ++++ .../src/components/icons/CpuIcon.tsx | 22 +- .../src/components/icons/DashboardIcon.tsx | 21 + .../src/components/icons/MemoryIcon.tsx | 26 +- .../src/components/icons/NetworkIcon.tsx | 22 +- .../src/components/icons/PortIcon.tsx | 5 + .../src/components/icons/StorageIcon.tsx | 34 +- .../src/components/invoice/invoiceTable.tsx | 1 - .../src/components/menu/AppNameMenu.tsx | 63 +++ .../src/components/menu/AppTypeMenu.tsx | 44 ++ .../src/components/menu/BaseMenu.tsx | 127 +++++ .../src/components/menu/CycleMenu.tsx | 38 ++ .../src/components/menu/NamespaceMenu.tsx | 52 ++ .../src/components/menu/RegionMenu.tsx | 46 ++ .../costcenter/src/components/outerLink.tsx | 2 +- .../components/table/AppBillingDetails.tsx | 78 +++ .../src/components/table/AppBillingTable.tsx | 159 ++++++ .../src/components/table/AppOverviewTable.tsx | 146 ++++++ .../src/components/table/BaseTable.tsx | 155 ++++++ .../src/components/table/billingDetails.tsx | 162 +++++++ .../src/components/table/billingTable.tsx | 182 +++++++ .../src/components/valuation/quota.tsx | 128 +++-- .../src/components/valuation/quotaPie.tsx | 0 .../costcenter/src/constants/billing.ts | 10 +- .../costcenter/src/constants/payment.ts | 9 +- .../providers/costcenter/src/layout/index.tsx | 2 +- .../costcenter/src/layout/sidebar.tsx | 93 ++-- .../providers/costcenter/src/pages/_app.tsx | 23 +- .../src/pages/api/account/getAmount.ts | 4 +- .../src/pages/api/account/payment/stripe.ts | 1 - .../src/pages/api/account/transfer.ts | 1 - .../src/pages/api/billing/appBilling.ts | 74 +++ .../src/pages/api/billing/appOverview.ts | 89 ++++ .../costcenter/src/pages/api/billing/buget.ts | 66 --- .../src/pages/api/billing/consumption.ts | 65 +++ .../src/pages/api/billing/costDistrube.ts | 68 +++ .../costcenter/src/pages/api/billing/costs.ts | 87 +++- .../src/pages/api/billing/getAppList.ts | 55 +-- .../src/pages/api/billing/getAppNameList.ts | 82 ++++ .../src/pages/api/billing/getNamespaceList.ts | 9 +- .../costcenter/src/pages/api/billing/index.ts | 14 + .../src/pages/api/billing/recharge.ts | 36 +- .../pages/api/billing/rechargeBillingList.ts | 57 +++ .../src/pages/api/billing/rechargeList.ts | 57 +++ .../src/pages/api/billing/regionCost.ts | 65 +++ .../src/pages/api/billing/transfer.ts | 1 + .../costcenter/src/pages/api/getRegions.ts | 35 ++ .../src/pages/api/platform/getAppConfig.ts | 6 +- .../costcenter/src/pages/api/properties.ts | 9 +- .../src/pages/app_overview/index.tsx | 214 ++++++++ .../costcenter/src/pages/billing/index.tsx | 105 ++-- .../src/pages/cost_overview/index.tsx | 85 ++-- .../src/pages/create_invoice/index.tsx | 12 +- .../src/pages/resource_analysis/index.tsx | 135 ++++++ .../costcenter/src/pages/valuation/index.tsx | 457 +++++++++--------- .../costcenter/src/service/backend/auth.ts | 19 +- .../src/service/backend/kubernetes.ts | 2 - .../costcenter/src/service/backend/region.ts | 38 ++ .../costcenter/src/service/sendToBot.ts | 2 - .../costcenter/src/stores/appType.ts | 49 ++ .../costcenter/src/stores/billing.ts | 150 +++++- .../costcenter/src/stores/overview.ts | 3 +- .../providers/costcenter/src/types/app.ts | 5 + .../providers/costcenter/src/types/billing.ts | 30 +- .../providers/costcenter/src/types/config.ts | 8 + .../providers/costcenter/src/types/cycle.ts | 3 + .../providers/costcenter/src/types/region.ts | 16 + frontend/providers/license/next.config.js | 14 +- .../objectstorage/.vscode/settings.json | 5 + 144 files changed, 5003 insertions(+), 1765 deletions(-) create mode 100644 frontend/providers/costcenter/.vscode/settings.json delete mode 100644 frontend/providers/costcenter/data/config.yaml.local create mode 100644 frontend/providers/costcenter/src/assert/Vector_disbabled.svg create mode 100644 frontend/providers/costcenter/src/assert/app.svg create mode 100644 frontend/providers/costcenter/src/assert/cpu.svg create mode 100644 frontend/providers/costcenter/src/assert/cronjob.svg create mode 100644 frontend/providers/costcenter/src/assert/cvm.svg create mode 100644 frontend/providers/costcenter/src/assert/dashboard.svg create mode 100644 frontend/providers/costcenter/src/assert/dashboard_black.svg create mode 100644 frontend/providers/costcenter/src/assert/db.svg create mode 100644 frontend/providers/costcenter/src/assert/job.svg create mode 100644 frontend/providers/costcenter/src/assert/layers.svg create mode 100644 frontend/providers/costcenter/src/assert/layers_black.svg create mode 100644 frontend/providers/costcenter/src/assert/lineChart.svg create mode 100644 frontend/providers/costcenter/src/assert/lineChart_black.svg create mode 100644 frontend/providers/costcenter/src/assert/memory.svg create mode 100644 frontend/providers/costcenter/src/assert/network.svg create mode 100644 frontend/providers/costcenter/src/assert/objectstorage.svg create mode 100644 frontend/providers/costcenter/src/assert/port.svg create mode 100644 frontend/providers/costcenter/src/assert/sealos.svg create mode 100644 frontend/providers/costcenter/src/assert/sealos_coin.png create mode 100644 frontend/providers/costcenter/src/assert/storage.svg create mode 100644 frontend/providers/costcenter/src/assert/terminal.svg create mode 100644 frontend/providers/costcenter/src/assert/to.svg create mode 100644 frontend/providers/costcenter/src/assert/toleft_disabled.svg create mode 100644 frontend/providers/costcenter/src/components/Refresh.tsx delete mode 100644 frontend/providers/costcenter/src/components/billing/AppMenu.tsx create mode 100644 frontend/providers/costcenter/src/components/billing/BillingLineChart.tsx create mode 100644 frontend/providers/costcenter/src/components/billing/BillingTrend.tsx delete mode 100644 frontend/providers/costcenter/src/components/billing/NamespaceMenu.tsx create mode 100644 frontend/providers/costcenter/src/components/cost_overview/components/barChart.tsx create mode 100644 frontend/providers/costcenter/src/components/cost_overview/components/quotaPieChart.tsx create mode 100644 frontend/providers/costcenter/src/components/cost_overview/trendBar.tsx create mode 100644 frontend/providers/costcenter/src/components/icons/DashboardIcon.tsx create mode 100644 frontend/providers/costcenter/src/components/icons/PortIcon.tsx create mode 100644 frontend/providers/costcenter/src/components/menu/AppNameMenu.tsx create mode 100644 frontend/providers/costcenter/src/components/menu/AppTypeMenu.tsx create mode 100644 frontend/providers/costcenter/src/components/menu/BaseMenu.tsx create mode 100644 frontend/providers/costcenter/src/components/menu/CycleMenu.tsx create mode 100644 frontend/providers/costcenter/src/components/menu/NamespaceMenu.tsx create mode 100644 frontend/providers/costcenter/src/components/menu/RegionMenu.tsx create mode 100644 frontend/providers/costcenter/src/components/table/AppBillingDetails.tsx create mode 100644 frontend/providers/costcenter/src/components/table/AppBillingTable.tsx create mode 100644 frontend/providers/costcenter/src/components/table/AppOverviewTable.tsx create mode 100644 frontend/providers/costcenter/src/components/table/BaseTable.tsx create mode 100644 frontend/providers/costcenter/src/components/table/billingDetails.tsx create mode 100644 frontend/providers/costcenter/src/components/table/billingTable.tsx create mode 100644 frontend/providers/costcenter/src/components/valuation/quotaPie.tsx create mode 100644 frontend/providers/costcenter/src/pages/api/billing/appBilling.ts create mode 100644 frontend/providers/costcenter/src/pages/api/billing/appOverview.ts delete mode 100644 frontend/providers/costcenter/src/pages/api/billing/buget.ts create mode 100644 frontend/providers/costcenter/src/pages/api/billing/consumption.ts create mode 100644 frontend/providers/costcenter/src/pages/api/billing/costDistrube.ts create mode 100644 frontend/providers/costcenter/src/pages/api/billing/getAppNameList.ts create mode 100644 frontend/providers/costcenter/src/pages/api/billing/rechargeBillingList.ts create mode 100644 frontend/providers/costcenter/src/pages/api/billing/rechargeList.ts create mode 100644 frontend/providers/costcenter/src/pages/api/billing/regionCost.ts create mode 100644 frontend/providers/costcenter/src/pages/api/getRegions.ts create mode 100644 frontend/providers/costcenter/src/pages/app_overview/index.tsx create mode 100644 frontend/providers/costcenter/src/pages/resource_analysis/index.tsx create mode 100644 frontend/providers/costcenter/src/service/backend/region.ts create mode 100644 frontend/providers/costcenter/src/stores/appType.ts create mode 100644 frontend/providers/costcenter/src/types/app.ts create mode 100644 frontend/providers/costcenter/src/types/cycle.ts create mode 100644 frontend/providers/costcenter/src/types/region.ts create mode 100644 frontend/providers/objectstorage/.vscode/settings.json diff --git a/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsBind.tsx b/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsBind.tsx index 7c0720114fd3..2da2083da0ca 100644 --- a/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsBind.tsx +++ b/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsBind.tsx @@ -149,7 +149,9 @@ const smsBindGen = (smsType: SmsType) => > - {smsType === 'phone' ? t('common:phone') : t('common:email')} + + {smsType === 'phone' ? t('common:phone') : t('common:email')} + > - {smsType === 'phone' ? t('common:phone') : t('common:email')} + + {smsType === 'phone' ? t('common:phone') : t('common:email')} + ) { else if (pageState === PageState.PASSWORD) return t('common:changepassword'); else if (Object.values(PhoneState).includes(pageState as PhoneState)) return t('common:changephone'); // bind or unbind - else if (Object.values(EmailState).includes(pageState as EmailState)) return t('common:changeemail'); + else if (Object.values(EmailState).includes(pageState as EmailState)) + return t('common:changeemail'); else return ''; }, [t, pageState]); const queryClient = useQueryClient(); diff --git a/frontend/desktop/src/components/team/RemoveMember.tsx b/frontend/desktop/src/components/team/RemoveMember.tsx index 535f9eeaebdb..58b1e8f1b077 100644 --- a/frontend/desktop/src/components/team/RemoveMember.tsx +++ b/frontend/desktop/src/components/team/RemoveMember.tsx @@ -60,7 +60,9 @@ export default function RemoveMember({ ? t('common:quit') : t('common:remove'); const removeTips = - selfUserCrUid === targetUserCrUid ? t('common:quit_workspace_tips') : t('common:remove_member_tips'); + selfUserCrUid === targetUserCrUid + ? t('common:quit_workspace_tips') + : t('common:remove_member_tips'); return ( <> )} {wechatEnabled && ( )} @@ -518,56 +542,63 @@ const RechargeModal = forwardRef( ) : ( <> - { - setDetail(false); - }} - > - {t('Recharge Amount')} + {t('preferential_rules')} - + - {steps && - ratios && - steps.length === ratios.length && - steps.map((step, idx) => ( - - {step + '<='} {t('Recharge Amount')}{' '} - {idx < steps.length - 1 ? `< ${steps[idx + 1]}` : ''} {t('Bonus')}{' '} - {ratios[idx]}%{' '} - - ))} - {specialBonus && - specialBonus.map(([k, v], i) => ( - - {k + '='} {t('Recharge Amount')} {t('Bonus')} {v} - - ))} + + + {t('Recharge Amount')} + + + {t('preferential_strength')} + + {steps && + ratios && + steps.length === ratios.length && + steps.map((step, idx) => ( + <> + + {step + '<='} + {t('Recharge Amount')} + {idx < steps.length - 1 ? `< ${steps[idx + 1]}` : ''} + + + {t('Bonus')} + {ratios[idx].toFixed(2)}% + + + ))} + {specialBonus && + specialBonus.map(([k, v], i) => ( + <> + + {k + '='} {t('Recharge Amount')}{' '} + + + {t('Bonus')} {v} + + + ))} + )} diff --git a/frontend/providers/costcenter/src/components/Refresh.tsx b/frontend/providers/costcenter/src/components/Refresh.tsx new file mode 100644 index 000000000000..d92c59b7f042 --- /dev/null +++ b/frontend/providers/costcenter/src/components/Refresh.tsx @@ -0,0 +1,33 @@ +import { Button, ButtonProps, useToast } from '@chakra-ui/react'; +import { RefreshIcon } from '@sealos/ui'; + +export function Refresh({ + onRefresh, + ...props +}: { + onRefresh(): void; +} & ButtonProps) { + const toast = useToast(); + return ( + + ); +} diff --git a/frontend/providers/costcenter/src/components/TransferModal.tsx b/frontend/providers/costcenter/src/components/TransferModal.tsx index bcb2d016aa78..d0c3548f1b19 100644 --- a/frontend/providers/costcenter/src/components/TransferModal.tsx +++ b/frontend/providers/costcenter/src/components/TransferModal.tsx @@ -157,39 +157,58 @@ const TransferModal = forwardRef( return ( - - - - {t('Transfer Amount')} + + + {t('Transfer')} - + - {t('Recipient ID')} + + {t('Recipient ID')} + { e.preventDefault(); @@ -197,43 +216,52 @@ const TransferModal = forwardRef( }} isDisabled={mutation.isLoading} /> - {t('Transfer Amount')} + + {t('Transfer Amount')} + (str.trim() ? setAmount(v) : setAmount(0))} - isDisabled={mutation.isLoading} > - - - - + + + + + - + - - + + {t('Balance')} - - + + {formatMoney(balance).toFixed(2)} diff --git a/frontend/providers/costcenter/src/components/billing/AmountDisplay.tsx b/frontend/providers/costcenter/src/components/billing/AmountDisplay.tsx index adaa9f428b0f..b33ff9479bd1 100644 --- a/frontend/providers/costcenter/src/components/billing/AmountDisplay.tsx +++ b/frontend/providers/costcenter/src/components/billing/AmountDisplay.tsx @@ -1,47 +1,65 @@ import { useQuery } from '@tanstack/react-query'; import request from '@/service/request'; -import { deFormatMoney, formatMoney } from '@/utils/format'; +import { formatMoney } from '@/utils/format'; import { useTranslation } from 'next-i18next'; import { Text, Box, Flex } from '@chakra-ui/react'; import CurrencySymbol from '@/components/CurrencySymbol'; import useOverviewStore from '@/stores/overview'; import useBillingStore from '@/stores/billing'; +import { useMemo } from 'react'; -export default function AmountDisplay() { - const startTime = useOverviewStore((s) => s.startTime); - const endTime = useOverviewStore((s) => s.endTime); - const { namespace, appType } = useBillingStore(); - const { data, isSuccess } = useQuery({ - queryKey: ['billing', 'buget', { startTime, endTime, appType, namespace }], +export default function AmountDisplay({ onlyOut = false }: { onlyOut?: boolean }) { + const { startTime, endTime } = useOverviewStore(); + const { getNamespace, getAppType, getRegion, getAppName } = useBillingStore(); + const rechargeQueryBody = { + startTime, + endTime + }; + const expenditureQueryBody = { + appType: getAppType(), + namespace: getNamespace()?.[0] || '', + startTime, + endTime, + regionUid: getRegion()?.uid || '', + appName: getAppName() + }; + const { data: expenditureData, isSuccess: expenditureIsSuccess } = useQuery({ + queryKey: ['consumption', expenditureQueryBody], queryFn: () => { - return request.post<{ amount: number }[]>('/api/billing/buget', { - startTime, - endTime, - appType, - namespace - }); + return request.post<{ amount: number }>('/api/billing/consumption', expenditureQueryBody); } }); - - const list = [ - { - bgColor: '#36ADEF', - title: 'Deduction', - value: data?.data[0].amount || 0 + const { data: rechargeData, isSuccess: rechargeIsSuccess } = useQuery({ + queryKey: ['recharge', rechargeQueryBody], + queryFn: () => { + return request.post<{ amount: number }>('/api/billing/recharge', rechargeQueryBody); }, - { - bgColor: '#47C8BF', - title: 'Charge', - value: data?.data[1].amount || 0 - } - ] as const; + enabled: !onlyOut + }); + // const { data } = useQuery({}) + const list = useMemo(() => { + const list = [ + { + bgColor: 'blue.600', + title: 'Total Expenditure', + value: expenditureData?.data.amount || 0 + } + ]; + if (!onlyOut) + list.push({ + bgColor: 'teal.500', + title: 'Total Recharge', + value: rechargeData?.data.amount || 0 + }); + return list; + }, [onlyOut, rechargeData, expenditureData]); const { t } = useTranslation(); return ( {list.map((item) => ( - {t(item.title)} + {t(item.title)}: {formatMoney(item.value).toFixed(2)} diff --git a/frontend/providers/costcenter/src/components/billing/AppMenu.tsx b/frontend/providers/costcenter/src/components/billing/AppMenu.tsx deleted file mode 100644 index fab26906ec22..000000000000 --- a/frontend/providers/costcenter/src/components/billing/AppMenu.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import request from '@/service/request'; -import useBillingStore from '@/stores/billing'; -import { ApiResp } from '@/types'; -import { - Button, - Flex, - FlexProps, - Popover, - PopoverContent, - PopoverTrigger, - useDisclosure -} from '@chakra-ui/react'; -import { useQuery } from '@tanstack/react-query'; -import { useTranslation } from 'next-i18next'; -import { useState } from 'react'; - -export default function AppMenu({ - isDisabled, - ...props -}: { - isDisabled: boolean; -} & FlexProps) { - const [appIdx, setAppIdx] = useState(0); - const { data } = useQuery({ - queryFn() { - return request>('/api/billing/getAppList'); - }, - queryKey: ['appList'] - }); - const { setAppType } = useBillingStore(); - const { isOpen, onClose, onOpen } = useDisclosure(); - const { t } = useTranslation(); - const { t: appT } = useTranslation('applist'); - const appList: string[] = ['All APP', ...(data?.data?.appList || [])].map((v) => appT(v)); - return ( - - - - - - - {appList.map((v, idx) => ( - - ))} - - - - ); -} diff --git a/frontend/providers/costcenter/src/components/billing/BillingLineChart.tsx b/frontend/providers/costcenter/src/components/billing/BillingLineChart.tsx new file mode 100644 index 000000000000..2443e056ad75 --- /dev/null +++ b/frontend/providers/costcenter/src/components/billing/BillingLineChart.tsx @@ -0,0 +1,179 @@ +import ReactEChartsCore from 'echarts-for-react/lib/core'; +// Import the echarts core module, which provides the necessary interfaces for using echarts. +import * as echarts from 'echarts/core'; +import { + GridComponent, + VisualMapComponent, + MarkLineComponent, + TooltipComponent +} from 'echarts/components'; +import { LineChart } from 'echarts/charts'; +import { UniversalTransition } from 'echarts/features'; +import { CanvasRenderer } from 'echarts/renderers'; +import { + addDays, + addHours, + addMonths, + addWeeks, + addYears, + differenceInDays, + differenceInHours, + differenceInMonths, + differenceInWeeks, + differenceInYears, + format, + isSameDay, + isSameHour, + isSameMonth, + isSameWeek, + isSameYear, + startOfDay, + startOfHour, + startOfMonth, + startOfWeek, + startOfYear, + subDays, + subHours, + subMonths, + subWeeks, + subYears +} from 'date-fns'; +import { useTranslation } from 'next-i18next'; +import { Cycle } from '@/types/cycle'; +import useOverviewStore from '@/stores/overview'; + +echarts.use([ + GridComponent, + VisualMapComponent, + MarkLineComponent, + LineChart, + CanvasRenderer, + TooltipComponent, + UniversalTransition +]); + +export default function Trend({ data, cycle }: { data: [number, string][]; cycle: Cycle }) { + const { t } = useTranslation(); + let methods = [ + isSameDay, + startOfDay, + differenceInDays, + subDays, + addDays, + 7 as number, + 'MM-dd' as string + ] as const; + if (cycle === 'Week') { + methods = [isSameWeek, startOfWeek, differenceInWeeks, subWeeks, addWeeks, 7, 'MM-dd']; + } else if (cycle === 'Month') { + methods = [isSameMonth, startOfMonth, differenceInMonths, subMonths, addMonths, 6, 'yyyy-MM']; + } else if (cycle === 'Hour') { + methods = [isSameHour, startOfHour, differenceInHours, subHours, addHours, 24, 'MM-dd HH:mm']; + } else if (cycle === 'Year') { + methods = [isSameYear, startOfYear, differenceInYears, subYears, addYears, 3, 'yyyy']; + } + const { startTime } = useOverviewStore(); + const startOfTime = methods[1](startTime); + const source = [ + // ['date', 'amount'], + ...data + .toSorted(([aD], [bD]) => aD - bD) + .reduce<[Date, number][]>( + (pre, [curDate, curVal]) => { + const len = pre.length; + const time = new Date(curDate * 1000); + let val = parseInt(curVal); + let preTime = pre[len - 1][0]; + + if (methods[0](preTime, time)) { + pre[len - 1][1] = pre[len - 1][1] + val; + } else { + while (methods[2](time, preTime) > 1) { + preTime = methods[4](preTime, 1); + pre.push([methods[1](preTime), 0]); + } + pre.push([methods[1](time), val]); + } + return pre; + }, + [[startOfTime, 0]] as [Date, number][] + ) + .map(([date, val]) => [format(date, methods[6]), val / 1_000_000]) + ]; + const series = { + type: 'line', + smooth: true, + datasetIndex: 0, + encode: { + x: 'date', + y: 'amount' + }, + connectNulls: true, + data: source + }; + const option = { + xAxis: { + type: 'category', + symbolOffset: [10, 10], + label: { + show: true + }, + axisLine: { + lineStyle: { + color: 'rgba(177, 200, 222, 0.6)' + } + }, + axisLabel: { + color: 'rgba(107, 112, 120, 1)' + } + }, + yAxis: { + name: '', + type: 'value', + // boundaryGap: false, + nameTextStyle: { + color: '#667085' + }, + splitLine: { + lineStyle: { + type: 'dashed' + } + }, + axisTick: { + show: false + }, + axisLabel: { + fontSize: '12px', + color: '#667085' + } + }, + dataset: { + // dimensions: ['date', 'amount', 'namespace'], + // source + }, + grid: { + left: '10%', + right: '10%', + bottom: '40px', + top: '40px' + }, + color: ['#11B6FC'], + tooltip: { + trigger: 'axis', + // borderWidth: 0, + axisPointer: { + type: 'line' + } + }, + series + }; + return ( + + ); +} diff --git a/frontend/providers/costcenter/src/components/billing/BillingTrend.tsx b/frontend/providers/costcenter/src/components/billing/BillingTrend.tsx new file mode 100644 index 000000000000..924cdf248d94 --- /dev/null +++ b/frontend/providers/costcenter/src/components/billing/BillingTrend.tsx @@ -0,0 +1,55 @@ +import { Heading, Box, Flex, Img, Divider } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import dynamic from 'next/dynamic'; +import chart7 from '@/assert/Chart7.svg'; +import { memo, useMemo, useState } from 'react'; +import Notfound from '@/components/notFound'; +import { useQuery } from '@tanstack/react-query'; +import request from '@/service/request'; +import { subDays } from 'date-fns'; +import useBillingStore from '@/stores/billing'; +import useOverviewStore from '@/stores/overview'; +const LineChart = dynamic(() => import('./BillingLineChart'), { ssr: false }); + +export const BillingTrend = memo(function Trend() { + const { t, i18n } = useTranslation(); + const { getCycle } = useBillingStore(); + const { startTime, endTime } = useOverviewStore(); + const { getAppName, getRegion, getAppType, getNamespace } = useBillingStore(); + const regionUid = getRegion()?.uid || ''; + const queryBody = { + endTime, + startTime, + regionUid, + appType: getAppType(), + appName: getAppName(), + namespace: getNamespace()?.[0] || '' + }; + const { data, isInitialLoading } = useQuery({ + queryKey: ['billing', 'trend', queryBody], + queryFn: () => { + return request.post<[number, string][]>('/api/billing/regionCost', queryBody); + } + }); + + const arr = useMemo(() => data?.data || [], [data?.data]); + return ( + + + + {t('Cost Trend')} + + + + {isInitialLoading || !data ? ( + <> + + + + ) : ( + + )} + + + ); +}); diff --git a/frontend/providers/costcenter/src/components/billing/InOutTabPanel.tsx b/frontend/providers/costcenter/src/components/billing/InOutTabPanel.tsx index 1ea49fad8e0b..55a5484db5af 100644 --- a/frontend/providers/costcenter/src/components/billing/InOutTabPanel.tsx +++ b/frontend/providers/costcenter/src/components/billing/InOutTabPanel.tsx @@ -1,106 +1,170 @@ import useOverviewStore from '@/stores/overview'; -import { memo, useContext, useEffect, useState } from 'react'; -import { BillingData, BillingSpec, BillingType } from '@/types'; -import { useQuery } from '@tanstack/react-query'; -import { formatISO } from 'date-fns'; +import { useEffect, useMemo, useState } from 'react'; +import { + ApiResp, + APPBillingItem, + AppOverviewBilling, + BillingData, + BillingSpec, + BillingType +} from '@/types'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import request from '@/service/request'; import { useTranslation } from 'next-i18next'; -import { Flex, TabPanel, Text } from '@chakra-ui/react'; -import AppMenu from '@/components/billing/AppMenu'; +import { Flex, Heading, HStack, Img, TabPanel, Text, useMediaQuery } from '@chakra-ui/react'; import SelectRange from '@/components/billing/selectDateRange'; -import SearchBox from '@/components/billing/SearchBox'; -import { CommonBillingTable } from '@/components/billing/billingTable'; import AmountDisplay from '@/components/billing/AmountDisplay'; import SwitchPage from '@/components/billing/SwitchPage'; import useBillingStore from '@/stores/billing'; +import AppNameMenu from '../menu/AppNameMenu'; +import AppTypeMenu from '../menu/AppTypeMenu'; +import CycleMenu from '../menu/CycleMenu'; +import NamespaceMenu from '../menu/NamespaceMenu'; +import RegionMenu from '../menu/RegionMenu'; +import { AppBillingTable } from '../table/AppBillingTable'; +import router from 'next/router'; +import { isNumber, isString } from 'lodash'; export default function InOutTabPanel() { - const startTime = useOverviewStore((state) => state.startTime); - const endTime = useOverviewStore((state) => state.endTime); - const [selectType, setType] = useState(BillingType.ALL); - const [orderID, setOrderID] = useState(''); + const { t } = useTranslation(); + const { getAppName, getRegion, getAppType, getNamespace } = useBillingStore(); + const { startTime, endTime } = useOverviewStore(); + const regionUid = getRegion()?.uid || ''; + const [page, setPage] = useState(1); const [totalPage, setTotalPage] = useState(1); - const [currentPage, setcurrentPage] = useState(1); + const [totalItem, setTotalItem] = useState(0); const [pageSize, setPageSize] = useState(10); - const [totalItem, setTotalItem] = useState(10); - const { namespace, appType } = useBillingStore(); - useEffect(() => { - setcurrentPage(1); - }, [startTime, endTime, selectType, namespace]); - const { data, isFetching, isSuccess } = useQuery( - ['billing', 'out', { currentPage, startTime, endTime, orderID, namespace, appType }], - () => { - const spec: BillingSpec = { - page: currentPage, - pageSize: pageSize, - type: BillingType.CONSUME, - startTime: formatISO(startTime, { representation: 'complete' }), - // startTime, - endTime: formatISO(endTime, { representation: 'complete' }), - // endTime, - appType, - namespace, - orderID - }; - return request('/api/billing', { - method: 'POST', - data: { - spec - } - }); + const [isBigScreen1, isBigScreen2] = useMediaQuery([ + '(min-width: 1200px)', + '(min-width: 1440px)' + ]); + const queryBody = { + endTime, + startTime, + regionUid, + appType: getAppType(), + appName: getAppName(), + namespace: getNamespace()?.[0] || '', + page, + pageSize + }; + // useEffect(() => { + // if (!router.query) return + // const { + // appNameIdx, + // appTypeIdx, + // namespaceIdx, + // regionIdx, + // } = router.query + // if (isNumber(appNameIdx) && isNumber(appTypeIdx) && isNumber(namespaceIdx) && isNumber(regionIdx)) { + // setRegion(regionIdx) + // setNamespace(namespaceIdx) + // setAppType(appTypeIdx) + // setAppName(appNameIdx) + // } + // }, [router]) + + const { data, isPreviousData, isFetching } = useQuery({ + queryFn() { + return request.post< + any, + ApiResp<{ + costs: APPBillingItem[]; + current_page: number; + total_pages: number; + total_records: number; + }> + >('/api/billing/appBilling', queryBody); }, - { - onSuccess(data) { - const totalPage = data.data.status.pageLength; - if (totalPage === 0) { - // search reset - setTotalPage(1); - setTotalItem(1); - } else { - setTotalItem(data.data.status.totalCount); - setTotalPage(totalPage); - } - if (totalPage < currentPage) setcurrentPage(1); - }, - staleTime: 1000 - } - ); - const { t } = useTranslation(); - const tableResult = data?.data?.status?.item || []; + onSuccess(data) { + if (!data.data) { + return; + } + const { total_records: total, total_pages: totalPage } = data.data; + if (totalPage === 0) { + // search reset + setTotalPage(1); + setTotalItem(1); + } else { + setTotalItem(total); + setTotalPage(totalPage); + } + if (totalPage < page) { + setPage(1); + } + }, + keepPreviousData: true, + queryKey: ['appOverviewBilling', queryBody, page, pageSize] + }); + + const appOverviews = useMemo(() => data?.data?.costs || [], [data?.data?.costs]); return ( - - - + + + + {t('Transaction Time')} + + + + + + {t('region')} + + + + + + {t('workspace')} + + + + + {t('APP Type')} - + - - - {t('Transaction Time')} + + + {t('app_name')} - {} + - - - [BillingType.CONSUME].includes(x.type))} - flex={'auto'} - overflow={'auto'} - overflowY={'auto'} - /> - - - */} + + + - + + + + + + {/* */} ); } diff --git a/frontend/providers/costcenter/src/components/billing/NamespaceMenu.tsx b/frontend/providers/costcenter/src/components/billing/NamespaceMenu.tsx deleted file mode 100644 index 753fea91db1f..000000000000 --- a/frontend/providers/costcenter/src/components/billing/NamespaceMenu.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import request from '@/service/request'; -import useBillingStore from '@/stores/billing'; -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'; -import { useState } from 'react'; - -export default function NamespaceMenu({ isDisabled }: { isDisabled: boolean }) { - const startTime = useOverviewStore((s) => s.startTime); - const endTime = useOverviewStore((s) => s.endTime); - const [namespaceIdx, setNamespaceIdx] = useState(0); - const { setNamespace } = useBillingStore(); - const { data: nsListData } = useQuery({ - queryFn() { - return request.post('/api/billing/getNamespaceList', { - startTime, - endTime - }); - }, - queryKey: [ - 'nsList', - { - startTime, - endTime - } - ] - }); - const { t } = useTranslation(); - const namespaceList: string[] = [ - t('All Namespace'), - ...((nsListData?.data?.list as string[]) || []) - ]; - return ( - - - - - - - {namespaceList.map((v, idx) => ( - - ))} - - - - ); -} diff --git a/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx b/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx index 1de43ba86066..51fcc0906878 100644 --- a/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx +++ b/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx @@ -10,7 +10,7 @@ import { PaginationState, useReactTable } from '@tanstack/react-table'; -import { BaseTable } from '@/components/billing/billingTable'; +import { BaseTable } from '@/components/table/BaseTable'; import { ApiResp, BillingType, RechargeBillingData, RechargeBillingItem } from '@/types'; import { useQuery } from '@tanstack/react-query'; import { format, formatISO, parseISO } from 'date-fns'; @@ -25,18 +25,21 @@ import CurrencySymbol from '@/components/CurrencySymbol'; import { TableHeaderID } from '@/constants/billing'; import Amount from '@/components/billing/AmountTableHeader'; import SearchBox from '@/components/billing/SearchBox'; +import useBillingStore from '@/stores/billing'; export default function RechargeTabPanel() { const { startTime, endTime } = useOverviewStore(); + const { getRegion } = useBillingStore(); const { data, isFetching, isSuccess } = useQuery( ['billing', 'in', { startTime, endTime }], () => { const body = { startTime: formatISO(startTime, { representation: 'complete' }), // startTime, - endTime: formatISO(endTime, { representation: 'complete' }) + endTime: formatISO(endTime, { representation: 'complete' }), + regionUid: getRegion()?.uid || '' }; - return request>('/api/billing/recharge', { + return request>('/api/billing/rechargeBillingList', { method: 'POST', data: body }); @@ -57,7 +60,7 @@ export default function RechargeTabPanel() { {t(header.id)} {!!needCurrency && ( - () + )} diff --git a/frontend/providers/costcenter/src/components/billing/SwitchPage.tsx b/frontend/providers/costcenter/src/components/billing/SwitchPage.tsx index 0c99d2dc3a92..4a316502c52e 100644 --- a/frontend/providers/costcenter/src/components/billing/SwitchPage.tsx +++ b/frontend/providers/costcenter/src/components/billing/SwitchPage.tsx @@ -1,17 +1,9 @@ -import { - Button, - ButtonProps, - Flex, - FlexProps, - Img, - SystemCSSProperties, - SystemStyleObject, - Text -} from '@chakra-ui/react'; -import { Dispatch, SetStateAction } from 'react'; -import { useTranslation } from 'react-i18next'; +import { Button, ButtonProps, Flex, FlexProps, Img, Text } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; import arrow_icon from '@/assert/Vector.svg'; import arrow_left_icon from '@/assert/toleft.svg'; +import arrorw_icon_disabled from '@/assert/Vector_disbabled.svg'; +import arrow_left_icon_disabled from '@/assert/toleft_disabled.svg'; export default function SwitchPage({ totalPage, @@ -19,74 +11,106 @@ export default function SwitchPage({ pageSize, currentPage, setCurrentPage, + isPreviousData, ...props }: { currentPage: number; totalPage: number; totalItem: number; pageSize: number; + isPreviousData?: boolean; setCurrentPage: (idx: number) => void; } & FlexProps) { const { t } = useTranslation(); const switchStyle: ButtonProps = { width: '24px', height: '24px', - background: '#EDEFF1', - // '#EDEFF1':'#F1F4F6' - borderRadius: '9999px', - color: '#262A32', + minW: '0', + background: 'grayModern.250', flexGrow: '0', + borderRadius: 'full', + // variant:'unstyled', _hover: { - opacity: '0.7' + background: 'grayModern.150', + minW: '0' }, _disabled: { - color: '828289', - background: '#F1F4F6' + borderRadius: 'full', + background: 'grayModern.150', + cursor: 'not-allowed', + minW: '0' } }; return ( - - {t('Total')}: - + + + {t('Total')}: + + {totalItem} - {currentPage}/{totalPage} + {currentPage} + / + {totalPage} - {pageSize} - /{t('Page')} + + {pageSize} + + + /{t('Page')} + ); } diff --git a/frontend/providers/costcenter/src/components/billing/TransferTabPnel.tsx b/frontend/providers/costcenter/src/components/billing/TransferTabPnel.tsx index f41a2e6a48a8..1c739232b99b 100644 --- a/frontend/providers/costcenter/src/components/billing/TransferTabPnel.tsx +++ b/frontend/providers/costcenter/src/components/billing/TransferTabPnel.tsx @@ -21,20 +21,23 @@ import { TransferBillingTable } from '@/components/billing/billingTable'; import SwitchPage from '@/components/billing/SwitchPage'; import NotFound from '../notFound'; import useBillingStore from '@/stores/billing'; +import { TRANSFER_LIST_TYPE } from '@/constants/billing'; export default function TransferTabPanel() { const startTime = useOverviewStore((state) => state.startTime); const endTime = useOverviewStore((state) => state.endTime); - const [selectType, setType] = useState(TransferType.ALL); + const [transferTypeIdx, settransferTypeIdx] = useState(0); const [orderID, setOrderID] = useState(''); const [totalPage, setTotalPage] = useState(1); const [currentPage, setcurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); const [totalItem, setTotalItem] = useState(10); - const { namespace } = useBillingStore(); + const { getNamespace } = useBillingStore(); + const namespace = getNamespace()?.[0] || ''; + const selectType = TRANSFER_LIST_TYPE[transferTypeIdx]; useEffect(() => { setcurrentPage(1); - }, [startTime, endTime, selectType, namespace]); + }, [startTime, endTime, transferTypeIdx, namespace]); const { data, isFetching, isSuccess } = useQuery( ['billing', { currentPage, startTime, endTime, orderID, selectType, namespace, pageSize }], () => { @@ -81,32 +84,25 @@ export default function TransferTabPanel() { - + {t('Type')} - + - {isSuccess && tableResult.length > 0 ? ( - <> - - - - - - ) : ( - - - - )} + + + + + ); } diff --git a/frontend/providers/costcenter/src/components/billing/TypeMenu.tsx b/frontend/providers/costcenter/src/components/billing/TypeMenu.tsx index 69a320cdd67e..674c7a95e986 100644 --- a/frontend/providers/costcenter/src/components/billing/TypeMenu.tsx +++ b/frontend/providers/costcenter/src/components/billing/TypeMenu.tsx @@ -1,76 +1,39 @@ import { TRANSFER_LIST_TYPE } from '@/constants/billing'; import { TransferType } from '@/types'; -import { Button, Popover, PopoverContent, PopoverTrigger, useDisclosure } from '@chakra-ui/react'; +import { + Button, + FlexProps, + Popover, + PopoverContent, + PopoverTrigger, + useDisclosure +} from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import { Dispatch, SetStateAction } from 'react'; +import BaseMenu from '../menu/BaseMenu'; export default function TypeMenu({ isDisabled, - selectType, - setType + idx, + setIdx, + ...props }: { isDisabled: boolean; - selectType: TransferType; - setType: Dispatch>; -}) { - const { isOpen, onClose, onOpen } = useDisclosure(); + idx: number; + setIdx: (idx: number) => void; +} & FlexProps) { const { t } = useTranslation(); + const itemList = TRANSFER_LIST_TYPE.map((v) => t(v.title)); return ( - - - - - - {TRANSFER_LIST_TYPE.map((v) => ( - - ))} - - + ); } diff --git a/frontend/providers/costcenter/src/components/billing/billingDetails.tsx b/frontend/providers/costcenter/src/components/billing/billingDetails.tsx index a8bd125cb3d3..23b1f564780f 100644 --- a/frontend/providers/costcenter/src/components/billing/billingDetails.tsx +++ b/frontend/providers/costcenter/src/components/billing/billingDetails.tsx @@ -50,9 +50,6 @@ export default function BillingDetails({ }); }, { - onSuccess(data) { - console.log(data); - }, enabled: isOpen } ); diff --git a/frontend/providers/costcenter/src/components/billing/billingTable.tsx b/frontend/providers/costcenter/src/components/billing/billingTable.tsx index d21cd72c3618..59b971343017 100644 --- a/frontend/providers/costcenter/src/components/billing/billingTable.tsx +++ b/frontend/providers/costcenter/src/components/billing/billingTable.tsx @@ -33,6 +33,7 @@ import Amount from '@/components/billing/AmountTableHeader'; import useSessionStore from '@/stores/session'; import lineUp from '@/assert/lineUp.svg'; import lineDown from '@/assert/lineDown.svg'; +import { BaseTable } from '../table/BaseTable'; export function CommonBillingTable({ data, isOverview = false, @@ -50,7 +51,7 @@ export function CommonBillingTable({ {t(header.id)} {!!needCurrency && ( - () + )} @@ -187,7 +188,7 @@ export function TransferBillingTable({ data }: { data: TransferBilling[] }) { {t(header.id)} {!!needCurrency && ( - () + )} @@ -299,7 +300,7 @@ export function BillingDetailsTable({ {t(header.id)} {!!needCurrency && ( - () + )} @@ -370,113 +371,3 @@ export function BillingDetailsTable({ }); return ; } - -export function BaseTable({ - table, - ...styles -}: { table: TTable } & TableContainerProps) { - return ( - - - - {table.getHeaderGroups().map((headers) => { - return ( - - {headers.headers.map((header) => { - const pinState = header.column.getIsPinned(); - return ( - - ); - })} - - ); - })} - - - {table.getRowModel().rows.map((item) => { - return ( - - {item.getAllCells().map((cell) => { - const pinState = cell.column.getIsPinned(); - return ( - - ); - })} - - ); - })} - {} - -
- {flexRender(header.column.columnDef.header, header.getContext())} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
- ); -} diff --git a/frontend/providers/costcenter/src/components/billing/selectDateRange.tsx b/frontend/providers/costcenter/src/components/billing/selectDateRange.tsx index cd3b7f4e1a13..37e5618ece2d 100644 --- a/frontend/providers/costcenter/src/components/billing/selectDateRange.tsx +++ b/frontend/providers/costcenter/src/components/billing/selectDateRange.tsx @@ -1,5 +1,6 @@ import useOverviewStore from '@/stores/overview'; import clander_icon from '@/assert/clander.svg'; +import to_icon from '@/assert/to.svg'; import { Flex, Input, @@ -8,20 +9,23 @@ import { Img, PopoverContent, Button, - Box + Box, + FlexProps } from '@chakra-ui/react'; import { format, parse, isValid, isAfter, isBefore, endOfDay, startOfDay, addDays } from 'date-fns'; import { useState, ChangeEventHandler, useCallback, useEffect } from 'react'; import { DateRange, SelectRangeEventHandler, DayPicker } from 'react-day-picker'; -import { useShallow } from 'zustand/react/shallow'; -import { subscribeWithSelector } from 'zustand/middleware'; +import { useTranslation } from 'next-i18next'; -export default function SelectRange({ isDisabled }: { isDisabled: boolean | undefined }) { +export default function SelectRange({ + isDisabled, + ...props +}: { isDisabled: boolean | undefined } & FlexProps) { const { setStartTime, setEndTime, startTime, endTime } = useOverviewStore(); const initState = { from: startTime, to: endTime }; const [selectedRange, setSelectedRange] = useState(initState); - const [fromValue, setFromValue] = useState(format(initState.from, 'y-MM-dd')); - const [toValue, setToValue] = useState(format(initState.to, 'y-MM-dd')); + const [fromValue, setFromValue] = useState(format(initState.from, 'y/MM/dd')); + const [toValue, setToValue] = useState(format(initState.to, 'y/MM/dd')); const [inputState, setInputState] = useState<0 | 1>(0); const onClose = () => { selectedRange?.from && setStartTime(startOfDay(selectedRange.from)); @@ -29,7 +33,7 @@ export default function SelectRange({ isDisabled }: { isDisabled: boolean | unde }; const handleFromChange: ChangeEventHandler = (e) => { setFromValue(e.target.value); - const date = parse(e.target.value, 'y-MM-dd', new Date()); + const date = parse(e.target.value, 'y/MM/dd', new Date()); if (!isValid(date)) { return setSelectedRange({ from: undefined, to: selectedRange?.to }); } @@ -47,7 +51,7 @@ export default function SelectRange({ isDisabled }: { isDisabled: boolean | unde const handleToChange: ChangeEventHandler = (e) => { setToValue(e.target.value); - const date = parse(e.target.value, 'y-MM-dd', new Date()); + const date = parse(e.target.value, 'y/MM/dd', new Date()); if (!isValid(date)) { return setSelectedRange({ from: selectedRange?.from, to: undefined }); @@ -83,14 +87,14 @@ export default function SelectRange({ isDisabled }: { isDisabled: boolean | unde to }); if (from) { - setFromValue(format(from, 'y-MM-dd')); + setFromValue(format(from, 'y/MM/dd')); } else { setFromValue(''); } if (to) { - setToValue(format(to, 'y-MM-dd')); + setToValue(format(to, 'y/MM/dd')); } else { - setToValue(from ? format(from, 'y-MM-dd') : ''); + setToValue(from ? format(from, 'y/MM/dd') : ''); } } else { // default is cancel @@ -122,7 +126,7 @@ export default function SelectRange({ isDisabled }: { isDisabled: boolean | unde ...selectedRange, from }); - setFromValue(format(from, 'y-MM-dd')); + setFromValue(format(from, 'y/MM/dd')); } } } @@ -146,12 +150,12 @@ export default function SelectRange({ isDisabled }: { isDisabled: boolean | unde ...selectedRange, to }); - setToValue(format(to, 'y-MM-dd')); + setToValue(format(to, 'y/MM/dd')); } } } } else { - // default is cancelgit + // default is cancel if (fromValue && selectedRange?.from) { setToValue(fromValue); setSelectedRange({ @@ -162,22 +166,30 @@ export default function SelectRange({ isDisabled }: { isDisabled: boolean | unde } } }; + const { t } = useTranslation(); return ( - + + */} diff --git a/frontend/providers/costcenter/src/components/cost_overview/buget.tsx b/frontend/providers/costcenter/src/components/cost_overview/buget.tsx index b18d75d7c46c..959592b5abd5 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/buget.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/buget.tsx @@ -12,59 +12,71 @@ 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() { +export function Buget({ expenditureAmount }: { expenditureAmount: number }) { const { t } = useTranslation(); - const startTime = useOverviewStore((state) => state.startTime); - const endTime = useOverviewStore((state) => state.endTime); - const { data, isSuccess } = useQuery({ - queryKey: ['billing', 'buget', { startTime, endTime }], + const { endTime } = useOverviewStore(); + const rechargeQueryBody = { + endTime + }; + const { data: rechargeData, isSuccess: rechargeIsSuccess } = useQuery({ + queryKey: ['recharge', rechargeQueryBody], queryFn: () => { - return request.post<{ amount: number }[]>('/api/billing/buget', { - startTime, - endTime - }); + return request.post<{ amount: number }>('/api/billing/recharge', rechargeQueryBody); } }); const currency = useEnvStore((s) => s.currency); const list = [ { - title: 'Deduction', - src: down_icon.src, - value: formatMoney(isSuccess ? data?.data[0].amount || 0 : 0) + title: 'Expenditure', + src: up_icon.src, + value: formatMoney(expenditureAmount || 0) }, { title: 'Charge', - src: up_icon.src, - value: formatMoney(isSuccess ? data?.data[1].amount || 0 : 0) + src: down_icon.src, + value: formatMoney(rechargeData?.data.amount || 0) } ]; return ( - + {t('Income And Expense')} - + {list.map((v) => ( - - + + - + {t(v.title)} - + - + {displayMoney(v.value)} diff --git a/frontend/providers/costcenter/src/components/cost_overview/components/barChart.tsx b/frontend/providers/costcenter/src/components/cost_overview/components/barChart.tsx new file mode 100644 index 000000000000..2227d22d9f26 --- /dev/null +++ b/frontend/providers/costcenter/src/components/cost_overview/components/barChart.tsx @@ -0,0 +1,135 @@ +import ReactEChartsCore from 'echarts-for-react/lib/core'; +// Import the echarts core module, which provides the necessary interfaces for using echarts. +import * as echarts from 'echarts/core'; +import { + GridComponent, + VisualMapComponent, + MarkLineComponent, + DatasetComponent, + TooltipComponent +} from 'echarts/components'; +import { BarChart, LineChart } from 'echarts/charts'; +import { UniversalTransition } from 'echarts/features'; +import { CanvasRenderer } from 'echarts/renderers'; +import { + addMonths, + differenceInMonths, + format, + isSameMonth, + startOfMonth, + subMonths +} from 'date-fns'; +import { useTranslation } from 'next-i18next'; + +echarts.use([GridComponent, BarChart, CanvasRenderer, TooltipComponent]); + +export default function Chart({ + data, + startTime, + endTime +}: { + data: [[number, number | string][], string][]; + startTime: Date; + endTime: Date; +}) { + const { t } = useTranslation(); + const series = data.map(([sourceRaw, seriesName]) => { + const source = [ + ...sourceRaw + .toSorted(([aDate], [bDate]) => aDate - bDate) + .reduce<[Date, number][]>( + (pre, [curDate, curVal]) => { + const len = pre.length; + const time = new Date(curDate); + let val = parseInt(curVal + ''); + let preTime = pre[len - 1][0]; + if (isSameMonth(preTime, time)) { + pre[len - 1][1] = pre[len - 1][1] + val; + } else { + while (differenceInMonths(time, preTime) > 1) { + preTime = addMonths(preTime, 1); + pre.push([startOfMonth(preTime), 0]); + } + pre.push([startOfMonth(time), val]); + } + return pre; + }, + [[startOfMonth(startTime), 0]] as [Date, number][] + ) + .map(([date, val]) => [format(date, 'yyyy-MM'), val / 1_000_000]) + ]; + return { + type: 'bar', + smooth: true, + showSymbol: false, + datasetIndex: 0, + encode: { + // 将 "amount" 列映射到 y 轴。 + x: 'date', + y: 'amount' + }, + name: seriesName, + data: source + }; + }); + + const option = { + xAxis: { + type: 'category', + symbolOffset: [10, 10], + label: { + show: true + }, + axisLine: { + lineStyle: { + color: '#8A95A7' + } + }, + axisLabel: { + color: '#667085' + } + }, + yAxis: { + name: '', + type: 'value', + boundaryGap: false, + nameTextStyle: { + color: '#667085' + }, + splitLine: { + lineStyle: { + type: 'dashed' + } + }, + axisTick: { + show: false + }, + axisLabel: { + color: '#667085' + } + }, + grid: { + left: '40px', + right: '5px' + }, + color: ['#24282C', '#11B6FC'], + tooltip: { + trigger: 'axis', + borderWidth: 0 + }, + legend: { + top: 'bottom' + }, + bottom: '10%', + series + }; + 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 6e5a42333739..848d87b8bd48 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/components/lineChart.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/components/lineChart.tsx @@ -6,14 +6,40 @@ import { VisualMapComponent, MarkLineComponent, DatasetComponent, - TooltipComponent + TooltipComponent, + LegendComponent } from 'echarts/components'; import { LineChart } from 'echarts/charts'; import { UniversalTransition } from 'echarts/features'; import { CanvasRenderer } from 'echarts/renderers'; -import { format } from 'date-fns'; +import { + addDays, + differenceInBusinessDays, + differenceInDays, + differenceInHours, + differenceInMonths, + differenceInWeeks, + differenceInYears, + format, + isSameDay, + isSameHour, + isSameMonth, + isSameWeek, + isSameYear, + startOfDay, + startOfHour, + startOfMonth, + startOfWeek, + startOfYear, + subDays, + subHours, + subMonths, + subWeeks, + subYears +} from 'date-fns'; import { useTranslation } from 'next-i18next'; import useOverviewStore from '@/stores/overview'; +import { Cycle } from '@/types/cycle'; echarts.use([ GridComponent, @@ -21,65 +47,91 @@ echarts.use([ DatasetComponent, MarkLineComponent, LineChart, + LegendComponent, CanvasRenderer, TooltipComponent, UniversalTransition ]); -export default function Trend({ data }: { data: [number, string][] }) { +export default function Trend({ + data, + cycle, + startTime, + endTime +}: { + data: [[number, string][], string][]; + cycle: Cycle; + startTime: Date; + endTime: Date; +}) { const { t } = useTranslation(); - const startTime = useOverviewStore((s) => s.startTime); - const endTime = useOverviewStore((s) => s.endTime); - 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]); - } - return pre; - }, [] as [number, number][]) - .map(([date, val]) => [date * 1000, val / 1000000]) - ]; + let methods = [isSameDay, startOfDay, differenceInDays, subDays, 7 as number] as const; + const series = data.map(([sourceRaw, seriesName]) => { + const source = [ + // ['date', 'amount'], + ...sourceRaw + .toSorted((a, b) => a[0] - b[0]) + .reduce<[Date, number][]>( + (pre, [curDate, curVal]) => { + const len = pre.length; + const time = new Date(curDate * 1000); + let val = parseInt(curVal); + let preTime = pre[len - 1][0]; + if (methods[0](preTime, time)) { + pre[len - 1][1] = pre[len - 1][1] + val; + // pre[len - 1][0] = time + } else { + while (methods[2](time, preTime) > 1) { + preTime = addDays(preTime, 1); + pre.push([preTime, 0]); + } + pre.push([methods[1](time), val]); + } + return pre; + }, + [[methods[1](startTime), 0]] as [Date, number][] + ) + .map(([date, val]) => [format(date, 'MM-dd'), val / 1_000_000]) + ]; + return { + type: 'line', + smooth: true, + datasetIndex: 0, + encode: { + // 将 "amount" 列映射到 y 轴。 + x: 'date', + y: 'amount' + }, + connectNulls: true, + name: seriesName, + data: source + }; + }); const option = { + legend: { + bottom: 0 + }, xAxis: { - type: 'time', - // minInterval: 24 * 60 * 60 * 1000, // 最小刻度为一天 - // maxInterval: 30 * 24 * 60 * 60 * 1000, // 最大刻度为一周 - min: startTime, - max: endTime, + type: 'category', symbolOffset: [10, 10], label: { show: true }, axisLine: { lineStyle: { - color: 'rgba(177, 200, 222, 0.6)' + color: '#8A95A7' } }, axisLabel: { - color: 'rgba(107, 112, 120, 1)' + color: '#667085' } }, yAxis: { - name: '元', + name: '', type: 'value', - boundaryGap: false, + // boundaryGap: false, nameTextStyle: { - color: 'rgba(107, 112, 120, 1)' - }, - axisLine: { - show: true, - lineStyle: { - color: 'rgba(177, 200, 222, 0.6)' - } + color: '#667085' }, splitLine: { lineStyle: { @@ -90,94 +142,27 @@ export default function Trend({ data }: { data: [number, string][] }) { show: false }, axisLabel: { - color: 'rgba(107, 112, 120, 1)' + fontSize: '12px', + color: '#667085' } }, dataset: { - dimensions: ['date', 'amount'], - source + // dimensions: ['date', 'amount', 'namespace'], + // source }, grid: { left: '40px', right: '5px' }, - color: ['#24282C'], + color: ['#485264', '#13C4B9', '#11B6FC', '#8774EE', '#C172E7'], tooltip: { trigger: 'axis', - borderWidth: 0, + // borderWidth: 0, axisPointer: { type: 'line' - }, - extraCssText: - 'box-shadow: 0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);', - backgroundColor: 'transparent', - padding: '0px', - - formatter: function (params: any) { - const { data, value } = params[0]; - const date = format(data[0], 'yyyy-MM-dd HH:mm:ss'); - const totalCost = value[1]; - // 创建外层 div 元素 - const resDom = document.createElement('div'); - resDom.style.background = '#FFFFFF'; - // resDom.style.width = ''; - resDom.style.padding = '16px'; - // resDom.style.height = '79px'; - resDom.style.padding = '16px'; - resDom.style.border = '1px solid rgba(205, 213, 218, 1)'; - resDom.style.borderRadius = '4px'; - // 创建日期 p 元素 - const dateP = document.createElement('p'); - dateP.style.color = '#5A646E'; - dateP.style.marginBottom = '8px'; - dateP.style.fontFamily = "'PingFang SC'"; - dateP.style.fontStyle = 'normal'; - dateP.style.fontWeight = '500'; - dateP.style.fontSize = '12px'; - dateP.style.lineHeight = '150%'; - dateP.textContent = date; - // 创建总消费 p 元素 - const totalCostP = document.createElement('p'); - totalCostP.style.fontFamily = "'PingFang SC'"; - totalCostP.style.fontStyle = 'normal'; - totalCostP.style.fontWeight = '500'; - totalCostP.style.fontSize = '14px'; - totalCostP.style.lineHeight = '150%'; - totalCostP.textContent = `${t('Total Cost')} : ${totalCost}`; - // 创建空 div 元素 - const emptyDiv = document.createElement('div'); - // 添加子元素到外层 div 元素中 - resDom.appendChild(dateP); - resDom.appendChild(totalCostP); - resDom.appendChild(emptyDiv); - return resDom; } }, - series: [ - { - type: 'line', - smooth: true, - showSymbol: false, - datasetIndex: 0, - encode: { - // 将 "amount" 列映射到 y 轴。 - x: 'date', - y: 'amount' - }, - areaStyle: { - color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ - { - offset: 0, - color: 'rgba(220, 227, 231, 0.8)' - }, - { - offset: 1, - color: 'rgba(233, 237, 239, 0)' - } - ]) - } - } - ] + series }; return ( ); } 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 001f0aa56297..3772048d03f8 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/components/pieChart.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/components/pieChart.tsx @@ -6,7 +6,7 @@ import { LabelLayout } from 'echarts/features'; import { CanvasRenderer } from 'echarts/renderers'; import { formatMoney } from '@/utils/format'; import { useMemo } from 'react'; -import { useBreakpointValue } from '@chakra-ui/react'; +import { ring, useBreakpointValue } from '@chakra-ui/react'; import { BillingData, Costs } from '@/types/billing'; import { useTranslation } from 'next-i18next'; import useEnvStore from '@/stores/env'; @@ -18,6 +18,7 @@ import { PieSeriesOption, TooltipComponentOption } from 'echarts'; +import { resourceType } from '@/constants/billing'; echarts.use([ TooltipComponent, @@ -28,68 +29,67 @@ echarts.use([ DatasetComponent ]); -export default function CostChart({ data }: { data: Costs }) { +export default function CostChart({ data }: { data: number[]; appName: string }) { const { t } = useTranslation(); - 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%'], - lg: ['45%', '70%'], - md: ['30%', '50%'], - sm: ['45%', '70%'] - }); - const aspectRatio = useBreakpointValue({ - xl: '5/4', - lg: '5/3', - md: '6/2', - sm: '5/4' + const radius = ['50%', '90%']; + // const aspectRatio = ['5/6'] + // useBreakpointValue({ + // xl: '5/4', + // lg: '5/3', + // md: '6/2', + // sm: '5/4' + // }); + const result = [0, 1, 2, 3, 4].map((_, i) => { + return [t(resourceType[i]), formatMoney(data[i]).toFixed(2)]; }); - const source = useMemo( - () => [ - ['name', 'cost'], - ['cpu', formatMoney(cpu).toFixed(2)], - ['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 + port + network); + const title = t('All APP', { ns: 'applist' }) + '\n' + t('Cost Form'); + const source = useMemo(() => [['name', 'cost'], ...result], [result]); const publicOption = { - name: 'Cost Form', + name: t('Cost Form'), radius: radius || ['45%', '70%'], avoidLabelOverlap: false, center: ['50%', '60%'], - left: 'left', + right: '20%', + top: '20px', + bottom: '20px', emptyCircleStyle: { borderCap: 'ronud' } }; const option = { dataset: { - dimensions: source[0], + // dimensions: source[0], source }, tooltip: { trigger: 'item' }, legend: { - top: '10%' + orient: 'vertical', + // : '100px', + top: 'middle', + align: 'left', + right: '10%', + textStyle: { + padding: [6, 6, 6, 6] + } }, - color: ['#24282C', '#485058', '#7B838B', '#BDC1C5', '#9CA2A8', '#DEE0E2'], + color: ['#1D2532', '#009BDE', '#40C6FF', '#6F5DD7', '#8774EE'], series: [ { type: 'pie', - emphasis: { - label: { - show: false - } - }, label: { - show: false, - fontSize: 14 + show: true, + fontSize: 12, + position: 'center', + // top:'middle', + color: '#485264', + fontWidth: '500', + formatter: function (params: any) { + return title; + } }, labelLine: { show: false @@ -104,38 +104,6 @@ export default function CostChart({ data }: { data: Costs }) { left: 0 }, ...publicOption - }, - { - type: 'pie', - radius: [publicOption.radius[0], publicOption.radius[0]], - center: publicOption.center, - selected: true, - label: { - position: 'center', - show: true, - formatter: function (params: any) { - let result = amount.toFixed(2) + `\n${t('Expenditure')}`; - if (result) return result; - else return ' '; - }, - emphasis: { - label: true - }, - fontSize: 16, - textStyle: { - textBorderColor: 'rgba(0,0,0,0)' - } - }, - emphasis: { - label: { - show: false - }, - scale: false - }, - encode: { - itemName: 'name', - value: 'cost' - } } ] }; @@ -146,9 +114,10 @@ export default function CostChart({ data }: { data: Costs }) { notMerge={true} lazyUpdate={true} style={{ - aspectRatio, + // aspectRatio, width: '100%', - flex: 1 + minWidth: '500px', + height: '260px' }} /> ); diff --git a/frontend/providers/costcenter/src/components/cost_overview/components/quotaPieChart.tsx b/frontend/providers/costcenter/src/components/cost_overview/components/quotaPieChart.tsx new file mode 100644 index 000000000000..2eaa2dac10d2 --- /dev/null +++ b/frontend/providers/costcenter/src/components/cost_overview/components/quotaPieChart.tsx @@ -0,0 +1,74 @@ +import * as echarts from 'echarts/core'; +import ReactEChartsCore from 'echarts-for-react/lib/core'; +import { PieChart } from 'echarts/charts'; +import { LabelLayout } from 'echarts/features'; +import { CanvasRenderer } from 'echarts/renderers'; +import { useMemo } from 'react'; +import { useBreakpointValue } from '@chakra-ui/react'; +import { UserQuotaItemType } from '@/pages/api/getQuota'; +import { useTranslation } from 'next-i18next'; + +echarts.use([PieChart, CanvasRenderer, LabelLayout]); + +export default function CostChart({ + data, + color = '#13C4B9' +}: { + data: UserQuotaItemType & { title: string }; + color?: string; +}) { + const radius = ['45%', '70%']; + const aspectRatio = useBreakpointValue({ + xl: '5/4', + lg: '5/3', + md: '6/2', + sm: '5/4' + }); + const publicOption = { + name: data.title, + radius: radius || ['45%', '70%'], + avoidLabelOverlap: false, + center: ['50%', '60%'], + left: 'left' + }; + const option = { + color: [color, '#F4F4F7'], + series: [ + { + type: 'pie', + clockwise: false, + label: { + show: true, + position: 'center', + formatter() { + return data.title; + } + }, + startAngle: 45, + data: [data.used, data.limit - data.used], + labelLine: { + show: false + }, + itemStyle: { + borderWidth: 1, + borderColor: 'rgba(0,0,0,0)', + left: 0 + }, + ...publicOption + } + ] + }; + return ( + + ); +} diff --git a/frontend/providers/costcenter/src/components/cost_overview/components/user.tsx b/frontend/providers/costcenter/src/components/cost_overview/components/user.tsx index 4b18d90c3d94..00af120b986c 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/components/user.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/components/user.tsx @@ -1,18 +1,7 @@ import request from '@/service/request'; import useSessionStore from '@/stores/session'; import { displayMoney, formatMoney } from '@/utils/format'; -import { - Box, - Button, - Center, - Flex, - Image, - Img, - LayoutProps, - Stack, - SystemStyleObject, - Text -} from '@chakra-ui/react'; +import { Box, Button, Center, Flex, Image, Stack, SystemStyleObject, Text } from '@chakra-ui/react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; @@ -26,7 +15,7 @@ import CurrencySymbol from '@/components/CurrencySymbol'; import useOverviewStore from '@/stores/overview'; import { RechargeContext } from '@/pages/cost_overview'; -export default memo(function UserCard() { +export default memo(function UserCard({ balance }: { balance: number }) { const getSession = useSessionStore((state) => state.getSession); const transferEnabled = useEnvStore((state) => state.transferEnabled); const rechargeEnabled = useEnvStore((state) => state.rechargeEnabled); @@ -44,11 +33,7 @@ export default memo(function UserCard() { }, [kubeconfig]); const { t } = useTranslation(); const session = useSessionStore().getSession(); - const { data: balance_raw } = useQuery({ - queryKey: ['getAccount'], - queryFn: () => - request>('/api/account/getAmount') - }); + const rechargeRef = useContext(RechargeContext).rechargeRef; const transferRef = useRef(); const queryClient = useQueryClient(); @@ -60,21 +45,17 @@ export default memo(function UserCard() { rechargeRef?.current?.onOpen(); } }, [rechargeRef?.current, rechargeSource]); - let real_balance = balance_raw?.data?.balance || 0; - if (balance_raw?.data?.deductionBalance) { - real_balance -= balance_raw?.data.deductionBalance; - } const currency = useEnvStore((s) => s.currency); - const balance = real_balance; const stripePromise = useEnvStore((s) => s.stripePromise); const persudoPublic: SystemStyleObject = { content: '""', position: 'absolute', - width: '313px', - height: '313px', + width: '400px', + height: '400px', backgroundColor: 'white', borderRadius: '50%', - right: '-100px', + left: '-37px', + // right: '50%', opacity: 0.1 }; return ( @@ -91,8 +72,7 @@ export default memo(function UserCard() { borderRadius="8px" color="white" overflow="hidden" - _before={{ ...persudoPublic, bottom: '-180px' }} - _after={{ ...persudoPublic, bottom: '-130px' }} + _before={{ ...persudoPublic, top: '40px' }} shrink={[1, 1, 1, 0]} > diff --git a/frontend/providers/costcenter/src/components/cost_overview/cost.tsx b/frontend/providers/costcenter/src/components/cost_overview/cost.tsx index e9d44da3b5bd..b115de9f1fba 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/cost.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/cost.tsx @@ -1,11 +1,15 @@ -import { Flex, Text } from '@chakra-ui/react'; +import { Box, Flex, HStack, Text } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import dynamic from 'next/dynamic'; import Notfound from '@/components/notFound'; import { useQuery } from '@tanstack/react-query'; import useOverviewStore from '@/stores/overview'; import request from '@/service/request'; -import { PropertiesCost } from '@/types'; +import { ApiResp, PropertiesCost } from '@/types'; +import useBillingStore from '@/stores/billing'; +import AppNameMenu from '../menu/AppNameMenu'; +import AppTypeMenu from '../menu/AppTypeMenu'; +import SelectRange from '../billing/selectDateRange'; const Chart = dynamic(() => import('./components/pieChart'), { ssr: false }); @@ -13,39 +17,73 @@ export const Cost = function Cost() { const { t } = useTranslation(); const startTime = useOverviewStore((state) => state.startTime); const endTime = useOverviewStore((state) => state.endTime); - const { data, isInitialLoading } = useQuery({ - queryKey: ['billing', 'properties', 'costs', { startTime, endTime }], + const { getNamespace, getAppName, getAppType, getRegion } = useBillingStore(); + + const query = { + namespace: getNamespace()?.[0] || '', + appType: getAppType(), + appName: getAppName(), + regionUid: getRegion()?.uid || '', + startTime, + endTime + }; + const { data, isInitialLoading, isFetching } = useQuery({ + queryKey: ['billing', 'properties', 'costs', query], queryFn: () => { - return request.post('/api/billing/propertiesUsedAmount', { - startTime, - endTime - }); + return request.post>('/api/billing/costDistrube', query); }, 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 - }; + const _data = data.data; + return [ + // @ts-ignore + _data['0'], + // @ts-ignore + _data['1'], + // @ts-ignore + _data['2'], + // @ts-ignore + _data['3'], + // @ts-ignore + _data['4'] + ]; } }); return ( - - - + + + {t('Cost Distribution')} + + + + {t('APP Type')} + + + + + + {t('app_name')} + + + + + + {t('Transaction Time')} + + + + + + + {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 82001500c873..90c723d4d03b 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/trend.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/trend.tsx @@ -1,40 +1,73 @@ -import { Heading, Box, Flex, Img } from '@chakra-ui/react'; +import { Heading, Box, Flex, Img, Divider } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import dynamic from 'next/dynamic'; import chart7 from '@/assert/Chart7.svg'; -import { memo } from 'react'; +import { memo, useMemo, useState } from 'react'; import Notfound from '@/components/notFound'; -import useOverviewStore from '@/stores/overview'; import { useQuery } from '@tanstack/react-query'; import request from '@/service/request'; +import { subDays } from 'date-fns'; +import useBillingStore from '@/stores/billing'; const LineChart = dynamic(() => import('./components/lineChart'), { ssr: false }); export const Trend = memo(function Trend() { - const { t } = useTranslation(); - const startTime = useOverviewStore((state) => state.startTime); - const endTime = useOverviewStore((state) => state.endTime); + const { t, i18n } = useTranslation(); + // const startTime = useOverviewStore((state) => state.startTime); + // const endTime = useOverviewStore((state) => state.endTime); + const [endTime] = useState(() => new Date()); + const startTime = subDays(endTime, 7); const { data, isInitialLoading } = useQuery({ queryKey: ['billing', 'trend', { startTime, endTime }], queryFn: () => { - return request.post<{ costs: [number, string][] }>('/api/billing/costs', { + return request.post< + [ + [number, string][], + { + en: string; + zh: string; + } + ][] + >('/api/billing/costs', { startTime, endTime }); } }); + const { getCycle } = useBillingStore(); + const arr = useMemo( + () => + (data?.data || []).map<[[number, string][], string]>((v) => [ + v[0], + i18n.language === 'zh' ? v[1].zh : v[1].en + ]), + [data?.data, i18n.language] + ); return ( - - - {t('Cost Trend')} + + + + {' '} + {t('Cost Trend')} + + + + {t('Last 7 days')} + - + {isInitialLoading || !data ? ( <> ) : ( - + )} diff --git a/frontend/providers/costcenter/src/components/cost_overview/trendBar.tsx b/frontend/providers/costcenter/src/components/cost_overview/trendBar.tsx new file mode 100644 index 000000000000..8c916dc54de3 --- /dev/null +++ b/frontend/providers/costcenter/src/components/cost_overview/trendBar.tsx @@ -0,0 +1,99 @@ +import { Heading, Box, Flex, Img, Divider } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import dynamic from 'next/dynamic'; +import chart7 from '@/assert/Chart7.svg'; +import { memo, useMemo, useState } from 'react'; +import Notfound from '@/components/notFound'; +import useOverviewStore from '@/stores/overview'; +import { useQuery } from '@tanstack/react-query'; +import request from '@/service/request'; +import { subDays, subMonths } from 'date-fns'; +const BarChart = dynamic(() => import('./components/barChart'), { ssr: false }); + +export const TrendBar = memo(function Trend() { + const { t, i18n } = useTranslation(); + // const startTime = useOverviewStore((state) => state.startTime); + // const endTime = useOverviewStore((state) => state.endTime); + const [endTime] = useState(() => new Date()); + const startTime = subMonths(endTime, 6); + const { data, isInitialLoading } = useQuery({ + queryKey: ['billing', 'trend', { startTime, endTime }], + queryFn: () => { + return request.post< + [ + [number, string][], + { + en: string; + zh: string; + } + ][] + >('/api/billing/costs', { + startTime, + endTime + }); + } + }); + const { data: rechareData } = useQuery({ + queryKey: ['billing', 'trend', 'recharge', { startTime, endTime }], + queryFn: () => { + return request.post<[number, number][]>('/api/billing/rechargeList', { + startTime, + endTime + }); + } + }); + const totalArr = useMemo( + () => + (data?.data ? data.data[0][0] : []).map<[number, string]>(([date, val]) => [ + date * 1000, + val + ]), + [data?.data] + ); + const rechargeArr = rechareData?.data || []; + const inOutData: [[number, string | number][], string][] = [ + [totalArr, t('Total Expenditure')], + [rechargeArr, t('Total Recharge')] + ]; + return ( + + + + {t('Annual Income and Expenditure')} + + + + {t('Last 6 Months')} + + + + {' '} + {t('All Regions')} + + + + + {isInitialLoading || !data ? ( + <> + + + + ) : ( + + )} + + + ); +}); diff --git a/frontend/providers/costcenter/src/components/icons/CpuIcon.tsx b/frontend/providers/costcenter/src/components/icons/CpuIcon.tsx index 4a273e59f90e..3f2ab2dd7c7b 100644 --- a/frontend/providers/costcenter/src/components/icons/CpuIcon.tsx +++ b/frontend/providers/costcenter/src/components/icons/CpuIcon.tsx @@ -1,19 +1,5 @@ -import { Icon, IconProps } from '@chakra-ui/react'; - -export default function CpuIcon(props: IconProps) { - return ( - - - - ); +import { Icon, IconProps, Img, ImgProps } from '@chakra-ui/react'; +import cpuIcon from '@/assert/cpu.svg'; +export default function CpuIcon(props: ImgProps) { + return ; } diff --git a/frontend/providers/costcenter/src/components/icons/DashboardIcon.tsx b/frontend/providers/costcenter/src/components/icons/DashboardIcon.tsx new file mode 100644 index 000000000000..3194d2a8ed1b --- /dev/null +++ b/frontend/providers/costcenter/src/components/icons/DashboardIcon.tsx @@ -0,0 +1,21 @@ +import { Icon, IconProps } from '@chakra-ui/react'; + +export default function DashboardIcon(props: IconProps) { + return ( + + + + ); +} diff --git a/frontend/providers/costcenter/src/components/icons/MemoryIcon.tsx b/frontend/providers/costcenter/src/components/icons/MemoryIcon.tsx index c5aa7bc5c7dd..baadd3ffe880 100644 --- a/frontend/providers/costcenter/src/components/icons/MemoryIcon.tsx +++ b/frontend/providers/costcenter/src/components/icons/MemoryIcon.tsx @@ -1,23 +1,5 @@ -import { Icon, IconProps } from '@chakra-ui/react'; - -export function MemoryIcon(props: IconProps) { - return ( - - - - - ); +import { Icon, IconProps, Img, ImgProps } from '@chakra-ui/react'; +import memoryIcon from '@/assert/memory.svg'; +export function MemoryIcon(props: ImgProps) { + return ; } diff --git a/frontend/providers/costcenter/src/components/icons/NetworkIcon.tsx b/frontend/providers/costcenter/src/components/icons/NetworkIcon.tsx index e03089f13e08..3879a79ce887 100644 --- a/frontend/providers/costcenter/src/components/icons/NetworkIcon.tsx +++ b/frontend/providers/costcenter/src/components/icons/NetworkIcon.tsx @@ -1,19 +1,5 @@ -import { Icon, IconProps } from '@chakra-ui/react'; - -export function NetworkIcon(props: IconProps) { - return ( - - - - ); +import { Img, ImgProps } from '@chakra-ui/react'; +import network from '@/assert/network.svg'; +export function NetworkIcon(props: ImgProps) { + return ; } diff --git a/frontend/providers/costcenter/src/components/icons/PortIcon.tsx b/frontend/providers/costcenter/src/components/icons/PortIcon.tsx new file mode 100644 index 000000000000..1ea58a0a3f3f --- /dev/null +++ b/frontend/providers/costcenter/src/components/icons/PortIcon.tsx @@ -0,0 +1,5 @@ +import { Img, ImgProps } from '@chakra-ui/react'; +import icon from '@/assert/port.svg'; +export function PortIcon(props: ImgProps) { + return ; +} diff --git a/frontend/providers/costcenter/src/components/icons/StorageIcon.tsx b/frontend/providers/costcenter/src/components/icons/StorageIcon.tsx index 039438594358..36649402970f 100644 --- a/frontend/providers/costcenter/src/components/icons/StorageIcon.tsx +++ b/frontend/providers/costcenter/src/components/icons/StorageIcon.tsx @@ -1,31 +1,5 @@ -import { Icon, IconProps } from '@chakra-ui/react'; - -export function StorageIcon(props: IconProps) { - return ( - - - - - - ); +import { Img, ImgProps } from '@chakra-ui/react'; +import storageIcon from '@/assert/storage.svg'; +export function StorageIcon(props: ImgProps) { + return ; } diff --git a/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx b/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx index 6440e08ad528..f8f98f5272d0 100644 --- a/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx +++ b/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx @@ -5,7 +5,6 @@ import { useTranslation } from 'next-i18next'; import { RechargeBillingItem, ReqGenInvoice } from '@/types'; import CurrencySymbol from '../CurrencySymbol'; import useEnvStore from '@/stores/env'; -import { formatMoney } from '@/utils/format'; export function InvoiceTable({ data, diff --git a/frontend/providers/costcenter/src/components/menu/AppNameMenu.tsx b/frontend/providers/costcenter/src/components/menu/AppNameMenu.tsx new file mode 100644 index 000000000000..d55b4fbbe2c4 --- /dev/null +++ b/frontend/providers/costcenter/src/components/menu/AppNameMenu.tsx @@ -0,0 +1,63 @@ +import request from '@/service/request'; +import useBillingStore from '@/stores/billing'; +import { ApiResp } from '@/types'; +import { FlexProps } from '@chakra-ui/react'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'next-i18next'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import BaseMenu from './BaseMenu'; +import { AppListItem } from '@/types/app'; +import useOverviewStore from '@/stores/overview'; + +export default function AppNameMenu({ + isDisabled, + innerWidth = '360px', + ...props +}: { + innerWidth?: string; + isDisabled: boolean; +} & FlexProps) { + const { setAppName, getNamespace, getRegion, getAppType, appNameIdx } = useBillingStore(); + const { startTime, endTime } = useOverviewStore(); + const regionUid = getRegion()?.uid || ''; + const queryBody = { + endTime, + startTime, + regionUid, + appType: getAppType(), + namespace: getNamespace()?.[0] || '' + }; + const { t } = useTranslation('applist'); + const { data, isFetching, isStale } = useQuery({ + queryFn() { + return request.post>( + '/api/billing/getAppNameList', + queryBody + ); + }, + queryKey: ['appNameList', 'menu', queryBody] + }); + const { t: commonT } = useTranslation(); + const { appNameList, setAppNameList } = useBillingStore(); + useEffect(() => { + const apps = (data?.data?.apps || []).filter((app) => !!app.appName).map((app) => app.appName); + setAppNameList([t('All APP'), ...apps]); + // setAppName(0); + }, [data?.data?.apps]); + const tappNameList: string[] = useMemo(() => { + return appNameList.map((app) => app || commonT('Other')); + }, [appNameList]); + return ( + + ); +} diff --git a/frontend/providers/costcenter/src/components/menu/AppTypeMenu.tsx b/frontend/providers/costcenter/src/components/menu/AppTypeMenu.tsx new file mode 100644 index 000000000000..69a4ab0d30d1 --- /dev/null +++ b/frontend/providers/costcenter/src/components/menu/AppTypeMenu.tsx @@ -0,0 +1,44 @@ +import request from '@/service/request'; +import useBillingStore from '@/stores/billing'; +import { ApiResp } from '@/types'; +import { + Button, + Flex, + FlexProps, + Popover, + PopoverContent, + PopoverTrigger, + useDisclosure +} from '@chakra-ui/react'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'next-i18next'; +import { useMemo, useState } from 'react'; +import BaseMenu from './BaseMenu'; +import useAppTypeStore from '@/stores/appType'; + +export default function AppTypeMenu({ + isDisabled, + innerWidth = '360px', + ...props +}: { + innerWidth?: string; + isDisabled: boolean; +} & FlexProps) { + const { setAppType } = useBillingStore(); + // const { isOpen, onClose, onOpen } = useDisclosure(); + const { t: appT } = useTranslation('applist'); + const { appTypeIdx, appTypeList } = useBillingStore(); + const appNameList = useMemo(() => appTypeList.map((v) => appT(v)), [appTypeList, appT]); + return ( + + ); +} diff --git a/frontend/providers/costcenter/src/components/menu/BaseMenu.tsx b/frontend/providers/costcenter/src/components/menu/BaseMenu.tsx new file mode 100644 index 000000000000..c5685e0cc85e --- /dev/null +++ b/frontend/providers/costcenter/src/components/menu/BaseMenu.tsx @@ -0,0 +1,127 @@ +import request from '@/service/request'; +import useBillingStore from '@/stores/billing'; +import { ChevronDownIcon } from '@chakra-ui/icons'; +import { + Button, + ButtonProps, + Flex, + FlexProps, + Popover, + PopoverContent, + PopoverTrigger, + useDisclosure +} from '@chakra-ui/react'; +import { useCallback, useEffect, useState } from 'react'; +// 多个下拉选项异步获取,如何处理菜单 + +export default function BaseMenu({ + isDisabled, + itemlist, + setItem, + itemIdx, + innerWidth = 'auto', + neeReset = false, + ...props +}: { + isDisabled: boolean; + setItem: (idx: number) => void; + itemIdx: number; + itemlist: string[]; + neeReset?: boolean; + innerWidth?: ButtonProps['width']; +} & FlexProps) { + const { isOpen, onClose, onOpen } = useDisclosure(); + + const onClick = useCallback((idx: number) => { + setItem(idx); + onClose(); + }, []); + // useEffect(() => { + // if (neeReset) { + // onClick(0); + // } + // }, [itemlist, neeReset]); + + return ( + + + + + + + {itemlist.map((v, idx) => ( + + ))} + + + + ); +} diff --git a/frontend/providers/costcenter/src/components/menu/CycleMenu.tsx b/frontend/providers/costcenter/src/components/menu/CycleMenu.tsx new file mode 100644 index 000000000000..06b90215e264 --- /dev/null +++ b/frontend/providers/costcenter/src/components/menu/CycleMenu.tsx @@ -0,0 +1,38 @@ +import request from '@/service/request'; +import useBillingStore from '@/stores/billing'; +import { + Button, + Flex, + FlexProps, + Popover, + PopoverContent, + PopoverTrigger, + useDisclosure +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { useState } from 'react'; +import BaseMenu from './BaseMenu'; +import { CYCLE } from '@/constants/valuation'; +import { Cycle } from '@/types/cycle'; + +export default function CycleMenu({ + isDisabled, + ...props +}: { + isDisabled: boolean; +} & FlexProps) { + const { setCycle, cycleIdx } = useBillingStore(); + const { t } = useTranslation(); + const cycleList: string[] = CYCLE.map((v) => t(v)); + return ( + + ); +} diff --git a/frontend/providers/costcenter/src/components/menu/NamespaceMenu.tsx b/frontend/providers/costcenter/src/components/menu/NamespaceMenu.tsx new file mode 100644 index 000000000000..8d3cbb59641c --- /dev/null +++ b/frontend/providers/costcenter/src/components/menu/NamespaceMenu.tsx @@ -0,0 +1,52 @@ +import request from '@/service/request'; +import useBillingStore from '@/stores/billing'; +import useOverviewStore from '@/stores/overview'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'next-i18next'; +import BaseMenu from './BaseMenu'; +import { FlexProps, useMediaQuery } from '@chakra-ui/react'; +import { useEffect } from 'react'; + +export default function NamespaceMenu({ + isDisabled, + innerWidth = '360px', + ...props +}: { innerWidth?: string; isDisabled: boolean } & FlexProps) { + const startTime = useOverviewStore((s) => s.startTime); + const endTime = useOverviewStore((s) => s.endTime); + const { setNamespace, setNamespaceList, namespaceList, namespaceIdx } = useBillingStore(); + const { getRegion } = useBillingStore(); + const queryBody = { + startTime, + endTime, + regionUid: getRegion()?.uid || '' + }; + const { data: nsListData, isFetching } = useQuery({ + queryFn() { + return request.post('/api/billing/getNamespaceList', queryBody); + }, + queryKey: ['nsList', 'menu', queryBody] + }); + const { t } = useTranslation(); + useEffect(() => { + const namespaceList: [string, string][] = [ + ['', t('all_workspace')], + ...((nsListData?.data as [string, string][]) || []) + ]; + setNamespaceList(namespaceList); + // setNamespace(0); + }, [nsListData, t]); + + return ( + v[1])} + {...props} + innerWidth={innerWidth} + /> + ); +} diff --git a/frontend/providers/costcenter/src/components/menu/RegionMenu.tsx b/frontend/providers/costcenter/src/components/menu/RegionMenu.tsx new file mode 100644 index 000000000000..07728d9e02da --- /dev/null +++ b/frontend/providers/costcenter/src/components/menu/RegionMenu.tsx @@ -0,0 +1,46 @@ +import request from '@/service/request'; +import useBillingStore from '@/stores/billing'; +import { ApiResp } from '@/types'; +import { RegionClient } from '@/types/region'; +import { FlexProps, useMediaQuery } from '@chakra-ui/react'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'next-i18next'; +import { useEffect, useMemo, useState } from 'react'; +import BaseMenu from './BaseMenu'; + +export default function RegionMenu({ + isDisabled, + innerWidth = '360px', + ...props +}: { + innerWidth?: string; + isDisabled: boolean; +} & FlexProps) { + const { setRegion, setRegionList, regionList, regionIdx } = useBillingStore(); + const { i18n } = useTranslation(); + const { data, isFetching } = useQuery({ + queryFn() { + return request>('/api/getRegions'); + }, + queryKey: ['regionList', 'menu'] + }); + useEffect(() => { + setRegionList(data?.data || []); + // setRegion(0); + }, [data?.data]); + const itemList = useMemo( + () => regionList.map((v) => (i18n?.language === 'zh' ? v.name.zh : v.name.en)), + [regionList, i18n?.language] + ); + return ( + + ); +} diff --git a/frontend/providers/costcenter/src/components/outerLink.tsx b/frontend/providers/costcenter/src/components/outerLink.tsx index 60f35d107ed5..fbd7d41fdec2 100644 --- a/frontend/providers/costcenter/src/components/outerLink.tsx +++ b/frontend/providers/costcenter/src/components/outerLink.tsx @@ -11,7 +11,7 @@ export default function Index({ text, href }: { text: string; href?: string }) { fontStyle="normal" fontWeight="400" fontSize="12px" - color="#1D8CDC" + color="brightBlue.600" {...(href ? { href: href diff --git a/frontend/providers/costcenter/src/components/table/AppBillingDetails.tsx b/frontend/providers/costcenter/src/components/table/AppBillingDetails.tsx new file mode 100644 index 000000000000..d00cadc82a2f --- /dev/null +++ b/frontend/providers/costcenter/src/components/table/AppBillingDetails.tsx @@ -0,0 +1,78 @@ +import { Text, Button, ButtonProps } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import DetailsIcon from '../icons/DetailsIcon'; +import { useRouter } from 'next/router'; +import useBillingStore from '@/stores/billing'; + +export default function AppOverviewDetails({ + appName, + appType, + namespace, + regionUid, + ...props +}: { + appName: string; + appType: string; + namespace: string; + regionUid: string; +} & ButtonProps) { + const router = useRouter(); + const { + appNameList, + namespaceList, + regionList, + appTypeList, + setAppName, + setAppType, + setNamespace + } = useBillingStore(); + const appNameIdx = appNameList.findIndex((v) => v === appName); + const appTypeIdx = appTypeList.findIndex((v) => v === appType); + const namespaceIdx = namespaceList.findIndex((v) => v[0] === namespace); + const toAppDetailPage = () => { + namespaceIdx >= 0 && setNamespace(namespaceIdx); + appTypeIdx >= 0 && setAppType(appTypeIdx); + appNameIdx >= 0 && setAppName(appNameIdx); + const query = encodeURIComponent( + `appNameIdx=${appNameIdx}&appTypeIdx=${appTypeIdx}&namespaceIdx=${namespaceIdx}®ionIdx={regionIdx}` + ); + router.push(`/billing`, { + query + }); + }; + const { t } = useTranslation(); + return ( + + ); +} diff --git a/frontend/providers/costcenter/src/components/table/AppBillingTable.tsx b/frontend/providers/costcenter/src/components/table/AppBillingTable.tsx new file mode 100644 index 000000000000..c1d84f811a26 --- /dev/null +++ b/frontend/providers/costcenter/src/components/table/AppBillingTable.tsx @@ -0,0 +1,159 @@ +import { TableHeaderID } from '@/constants/billing'; +import useEnvStore from '@/stores/env'; +import { APPBillingItem, AppOverviewBilling, BillingType } from '@/types'; +import { TableContainerProps, Flex, Text, Box, Img } from '@chakra-ui/react'; +import { + createColumnHelper, + HeaderContext, + CellContext, + useReactTable, + getCoreRowModel +} from '@tanstack/react-table'; +import { useMemo } from 'react'; +import { useTranslation } from 'next-i18next'; +import Amount from '../billing/AmountTableHeader'; +import CurrencySymbol from '../CurrencySymbol'; +import { AppImg, BaseTable } from './BaseTable'; +import useAppTypeStore from '@/stores/appType'; +import BillingDetails from './billingDetails'; +import appIcon from '@/assert/app.svg'; +import jobIcon from '@/assert/job.svg'; +import osIcon from '@/assert/objectstorage.svg'; +import cvmIcon from '@/assert/cvm.svg'; +import terminalIcon from '@/assert/terminal.svg'; +import dbIcon from '@/assert/db.svg'; +import sealosIcon from '@/assert/sealos.svg'; +import { subHours, parseISO, format } from 'date-fns'; +import useBillingStore from '@/stores/billing'; +const getCustomTh = (data?: { tNs?: string; needCurrency?: boolean }) => + function CustomTh({ header }: HeaderContext) { + const tNs = data?.tNs || 'common'; + const needCurrency = data?.needCurrency || false; + const { t } = useTranslation(tNs); + const currency = useEnvStore((s) => s.currency); + return ( + + + {t(header.id)} + + {!!needCurrency && ( + + + + )} + + ); + }; +const getAmountCell = (data?: { isTotal?: boolean }) => + function AmountCell(props: CellContext) { + const isTotal = data?.isTotal || false; + return ; + }; +const getAppTypeCell = () => + function TranslationCell(props: CellContext) { + const { t } = useTranslation('applist'); + const { getAppType } = useAppTypeStore(); + const appType = getAppType(props.cell.getValue() + ''); + const text = t(appType); + return {t(text)}; + }; +const getAppBillingDetailCell = () => + function TranslationCell(props: CellContext) { + const item = props.row.original; + return ( + + ); + }; + +export function AppBillingTable({ + data, + ...styles +}: { data: APPBillingItem[] } & TableContainerProps) { + const { t, i18n } = useTranslation(); + const { getRegion, namespaceList } = useBillingStore(); + const region = getRegion(); + const namespaceMap = useMemo(() => new Map(namespaceList), [namespaceList]); + const regionName = i18n.language === 'zh' ? region?.name.zh || '' : region?.name.en || ''; + const columns = useMemo(() => { + const columnHelper = createColumnHelper(); + return [ + columnHelper.accessor((row) => row.app_name, { + header: getCustomTh(), + id: TableHeaderID.APPName, + cell(props) { + const original = props.row.original; + const app_type = original.app_type; + const appName = + props.getValue() || (app_type === 3 ? t('TERMINAL', { ns: 'applist' }) : t('Other')); + return ( + + + {appName} + + ); + }, + enablePinning: true + }), + columnHelper.accessor((row) => row.app_type, { + id: TableHeaderID.APPType, + header: getCustomTh(), + cell: getAppTypeCell() + }), + columnHelper.display({ + id: TableHeaderID.Region, + header: getCustomTh(), + cell: regionName + }), + columnHelper.accessor((row) => row.namespace, { + id: TableHeaderID.Namespace, + header: getCustomTh(), + cell(props) { + return namespaceMap.get(props.getValue()) || props.getValue(); + } + }), + columnHelper.accessor((row) => row.time, { + id: TableHeaderID.TransactionTime, + header: getCustomTh(), + cell(props) { + const time = parseISO(props.cell.getValue()); + return ( + + {format(subHours(time, 1), 'yyyy-MM-dd')} + {format(subHours(time, 1), 'HH:MM') + ' ~ ' + format(time, 'HH:MM')} + + ); + } + }), + columnHelper.accessor((row) => row.amount, { + id: TableHeaderID.TotalAmount, + header: getCustomTh({ + needCurrency: true + }), + cell: getAmountCell({ isTotal: true }) + }), + columnHelper.display({ + header: getCustomTh(), + id: TableHeaderID.Handle, + enablePinning: true, + cell: getAppBillingDetailCell() + }) + ]; + }, [regionName, t]); + const table = useReactTable({ + data, + state: { + columnPinning: { + left: [TableHeaderID.APPName], + right: [TableHeaderID.Handle] + } + }, + columns, + getCoreRowModel: getCoreRowModel() + }); + return ; +} diff --git a/frontend/providers/costcenter/src/components/table/AppOverviewTable.tsx b/frontend/providers/costcenter/src/components/table/AppOverviewTable.tsx new file mode 100644 index 000000000000..a05fdc3beda1 --- /dev/null +++ b/frontend/providers/costcenter/src/components/table/AppOverviewTable.tsx @@ -0,0 +1,146 @@ +import { TableHeaderID } from '@/constants/billing'; +import useEnvStore from '@/stores/env'; +import { AppOverviewBilling, BillingType } from '@/types'; +import { TableContainerProps, Flex, Text } from '@chakra-ui/react'; +import { + createColumnHelper, + HeaderContext, + CellContext, + useReactTable, + getCoreRowModel +} from '@tanstack/react-table'; +import { useMemo } from 'react'; +import { useTranslation } from 'next-i18next'; +import Amount from '../billing/AmountTableHeader'; +import CurrencySymbol from '../CurrencySymbol'; +import { AppImg, BaseTable } from './BaseTable'; +import useAppTypeStore from '@/stores/appType'; +import AppOverviewDetails from './AppBillingDetails'; +import useBillingStore from '@/stores/billing'; + +const getCustomTh = (data?: { tNs?: string; needCurrency?: boolean }) => + function CustomTh({ header }: HeaderContext) { + const tNs = data?.tNs || 'common'; + const needCurrency = data?.needCurrency || false; + const { t } = useTranslation(tNs); + const currency = useEnvStore((s) => s.currency); + return ( + + {t(header.id)} + {!!needCurrency && ( + + + + )} + + ); + }; +const getAmountCell = (data?: { isTotal?: boolean }) => + function AmountCell(props: CellContext) { + const isTotal = data?.isTotal || false; + return ; + }; +const getAppTypeCell = () => + function TranslationCell(props: CellContext) { + const { t } = useTranslation('applist'); + const { getAppType } = useAppTypeStore(); + const appType = getAppType(props.cell.getValue() + ''); + const text = t(appType); + return t(text); + }; +const getAppBillingDetailCell = (data?: { tNs?: string }) => + function TranslationCell(props: CellContext) { + const { t } = useTranslation('applist'); + const { getAppType } = useAppTypeStore(); + const item = props.row.original; + const appType = getAppType(item.appType + ''); + return ( + + ); + }; +export function AppOverviewTable({ + data, + ...styles +}: { data: AppOverviewBilling[] } & TableContainerProps) { + const { getAppType } = useAppTypeStore(); + const { t, i18n } = useTranslation(); + const { getRegion, namespaceList } = useBillingStore(); + const region = getRegion(); + const namespaceMap = useMemo(() => new Map(namespaceList), [namespaceList]); + const regionName = i18n.language === 'zh' ? region?.name.zh || '' : region?.name.en || ''; + const columns = useMemo(() => { + const columnHelper = createColumnHelper(); + return [ + columnHelper.accessor((row) => row.appName || t('Other'), { + header: getCustomTh(), + id: TableHeaderID.APPName, + cell(props) { + const original = props.row.original; + const app_type = original.appType; + const appName = + props.getValue() || (app_type === 3 ? t('TERMINAL', { ns: 'applist' }) : t('Other')); + return ( + + + {appName} + + ); + }, + enablePinning: true + }), + columnHelper.accessor((row) => row.appType, { + id: TableHeaderID.APPType, + header: getCustomTh(), + cell: getAppTypeCell() + }), + columnHelper.display({ + id: TableHeaderID.Region, + header: getCustomTh(), + cell: regionName + }), + columnHelper.accessor((row) => row.namespace, { + id: TableHeaderID.Namespace, + header: getCustomTh(), + cell(props) { + return namespaceMap.get(props.getValue()) || props.getValue(); + } + }), + columnHelper.accessor((row) => row.amount, { + id: TableHeaderID.TotalAmount, + header: getCustomTh({ + needCurrency: true + }), + cell: getAmountCell({ isTotal: true }) + }), + columnHelper.display({ + header: getCustomTh(), + id: TableHeaderID.Handle, + enablePinning: true, + cell(props) { + const { appType, namespace } = props.row.original; + + return ( + + ); + } + }) + ]; + }, [t, regionName]); + const table = useReactTable({ + data, + state: { + columnPinning: { + left: [TableHeaderID.APPName], + right: [TableHeaderID.Handle] + } + }, + columns, + getCoreRowModel: getCoreRowModel() + }); + return ; +} diff --git a/frontend/providers/costcenter/src/components/table/BaseTable.tsx b/frontend/providers/costcenter/src/components/table/BaseTable.tsx new file mode 100644 index 000000000000..d53cfa5fc502 --- /dev/null +++ b/frontend/providers/costcenter/src/components/table/BaseTable.tsx @@ -0,0 +1,155 @@ +import { + TableContainerProps, + TableContainer, + Table, + Thead, + Tr, + Th, + Tbody, + Td, + Img, + ImgProps +} from '@chakra-ui/react'; +import { flexRender, Table as ReactTable } from '@tanstack/react-table'; +import appIcon from '@/assert/app.svg'; +import jobIcon from '@/assert/job.svg'; +import osIcon from '@/assert/objectstorage.svg'; +import cvmIcon from '@/assert/cvm.svg'; +import terminalIcon from '@/assert/terminal.svg'; +import dbIcon from '@/assert/db.svg'; +import sealosIcon from '@/assert/sealos.svg'; +import { useTranslation } from 'next-i18next'; +import useAppTypeStore from '@/stores/appType'; + +export function BaseTable({ + table, + ...styles +}: { table: ReactTable } & TableContainerProps) { + return ( + + + + {table.getHeaderGroups().map((headers) => { + return ( + + {headers.headers.map((header, i) => { + const pinState = header.column.getIsPinned(); + return ( + + ); + })} + + ); + })} + + + {table.getRowModel().rows.map((item) => { + return ( + + {item.getAllCells().map((cell, i) => { + const pinState = cell.column.getIsPinned(); + return ( + + ); + })} + + ); + })} + {} + +
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ ); +} +export function AppImg({ app_type, ...props }: { app_type: string } & ImgProps) { + const { getAppType } = useAppTypeStore(); + let uri = ''; + if (getAppType(app_type) === 'DB' || app_type === 'DB') { + uri = dbIcon.src; + } else if (getAppType(app_type) === 'APP' || app_type === 'APP') { + uri = appIcon.src; + } else if (getAppType(app_type) === 'TERMINAL' || app_type === 'TERMINAL') { + uri = terminalIcon.src; + } else if (getAppType(app_type) === 'JOB' || app_type === 'JOB') { + uri = jobIcon.src; + } else if (getAppType(app_type) === 'OBJECT-STORAGE' || app_type === 'OBJECT-STORAGE') { + uri = osIcon.src; + } else if (getAppType(app_type) === 'CLOUD-VM' || app_type === 'CLOUD-VM') { + uri = cvmIcon.src; + } else { + uri = sealosIcon.src; + } + return ; +} diff --git a/frontend/providers/costcenter/src/components/table/billingDetails.tsx b/frontend/providers/costcenter/src/components/table/billingDetails.tsx new file mode 100644 index 000000000000..7ba5a80e6c83 --- /dev/null +++ b/frontend/providers/costcenter/src/components/table/billingDetails.tsx @@ -0,0 +1,162 @@ +import { + useDisclosure, + Text, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + Button, + Flex, + ButtonProps +} from '@chakra-ui/react'; +// import { BillingDetailsTable } from './billingTable'; +import { ApiResp, APPBillingItem } from '@/types'; +import { useQuery } from '@tanstack/react-query'; +import request from '@/service/request'; +import { format, parseISO } from 'date-fns'; +import { useTranslation } from 'react-i18next'; +import DetailsIcon from '../icons/DetailsIcon'; +import { BillingDetailsTable } from './billingTable'; +import useBillingStore from '@/stores/billing'; +import useOverviewStore from '@/stores/overview'; +import useAppTypeStore from '@/stores/appType'; +import { AppImg } from './BaseTable'; + +export default function BillingDetails({ + namespace, + app_type, + appName, + orderID, + ...props +}: { + appName: string; + namespace: string; + orderID: string; + app_type: number; +} & ButtonProps) { + const { isOpen, onOpen, onClose } = useDisclosure(); + const { getRegion } = useBillingStore(); + const { endTime, startTime } = useOverviewStore(); + const { getAppType } = useAppTypeStore(); + const appType = getAppType(app_type + ''); + const query = { + endTime, + startTime, + regionUid: getRegion()?.uid || '', + appType, + appName, + orderID, + namespace + }; + const { t } = useTranslation(); + return ( + <> + + + + ); +} +export function BillingDetailsModal({ + query, + onClose, + isOpen +}: { + query: { + endTime: Date; + startTime: Date; + regionUid: string; + appType: string; + appName: string; + orderID: string; + namespace: string; + }; + isOpen: boolean; + onClose: () => void; +}) { + const { data } = useQuery( + ['billingDetails', query], + () => { + return request< + any, + ApiResp<{ + costs: APPBillingItem[]; + current_page: number; + total_pages: number; + total_records: number; + }> + >('/api/billing/appBilling', { + method: 'POST', + data: query + }); + }, + { + enabled: isOpen + } + ); + const { t } = useTranslation(); + return ( + + + + + + {query.appName} + + + + + + {t('Order Number')}: {query.orderID} + + + {t('Transaction Time')}: {format(query.startTime, 'yyyy-MM-dd HH:MM')} ~ + {format(query.endTime, ' HH:MM')} + + + + + + + ); +} diff --git a/frontend/providers/costcenter/src/components/table/billingTable.tsx b/frontend/providers/costcenter/src/components/table/billingTable.tsx new file mode 100644 index 000000000000..b7aca70e1955 --- /dev/null +++ b/frontend/providers/costcenter/src/components/table/billingTable.tsx @@ -0,0 +1,182 @@ +import { resourceType, TableHeaderID } from '@/constants/billing'; +import { APPBillingItem, BillingType } from '@/types/billing'; +import { Box, Flex, Img, TableContainerProps, Text } from '@chakra-ui/react'; +import { format, parseISO, subHours } from 'date-fns'; +import { useTranslation } from 'next-i18next'; +import useEnvStore from '@/stores/env'; +import CurrencySymbol from '../CurrencySymbol'; +import { + CellContext, + createColumnHelper, + flexRender, + getCoreRowModel, + HeaderContext, + Table as TTable, + useReactTable +} from '@tanstack/react-table'; +import { useMemo } from 'react'; +import Amount from '@/components/billing/AmountTableHeader'; +import { AppImg, BaseTable } from '../table/BaseTable'; +import { valuationMap } from '@/constants/payment'; +import useAppTypeStore from '@/stores/appType'; +import appIcon from '@/assert/app.svg'; +import jobIcon from '@/assert/job.svg'; +import osIcon from '@/assert/objectstorage.svg'; +import cvmIcon from '@/assert/cvm.svg'; +import terminalIcon from '@/assert/terminal.svg'; +import dbIcon from '@/assert/db.svg'; +import sealosIcon from '@/assert/sealos.svg'; +const getAmountCell = (data?: { isTotal?: boolean }) => + function AmountCell(props: CellContext) { + const isTotal = data?.isTotal || false; + return ; + }; +const getAppTypeCell = () => + function TranslationCell(props: CellContext) { + const { t } = useTranslation('applist'); + const { getAppType } = useAppTypeStore(); + const appType = getAppType(props.cell.getValue() + ''); + const text = t(appType); + return t(text); + }; +export function BillingDetailsTable({ + data, + ...styles +}: { data: APPBillingItem[] } & TableContainerProps) { + const { t } = useTranslation(); + const currency = useEnvStore((s) => s.currency); + + const columns = useMemo(() => { + const columnHelper = createColumnHelper(); + const customTh = (needCurrency?: boolean) => + function CustomTh({ header }: HeaderContext) { + return ( + + {t(header.id)} + {!!needCurrency && ( + + + + )} + + ); + }; + const customCell = (isTotal?: boolean) => + function CustomCell(props: CellContext) { + return ; + }; + const getUnit = (x: string) => { + return function CustomCell(props: CellContext) { + const resourceEntity = valuationMap.get(x); + if (!resourceEntity) return '0'; + const unit = resourceEntity.unit; + return props.cell.getValue() / resourceEntity.scale + ' ' + unit; + }; + }; + return [ + columnHelper.accessor((row) => row.app_name, { + header: customTh(), + id: TableHeaderID.APPName, + cell(props) { + const original = props.row.original; + const app_type = original.app_type; + const appName = + props.getValue() || (app_type === 3 ? t('TERMINAL', { ns: 'applist' }) : t('Other')); + return ( + + + {appName} + + ); + }, + enablePinning: true + }), + columnHelper.accessor((row) => row.app_type, { + header: customTh(), + id: TableHeaderID.APPType, + cell: getAppTypeCell(), + enablePinning: true + }), + columnHelper.accessor((row) => row.used[0], { + id: TableHeaderID.CPU, + header: customTh(), + cell: getUnit('cpu') + }), + columnHelper.accessor((row) => row.used_amount[0], { + id: TableHeaderID.CPUAmount, + header: customTh(), + cell: customCell() + }), + columnHelper.accessor((row) => row.used[1], { + id: TableHeaderID.Memory, + header: customTh(), + cell: getUnit('memory') + }), + columnHelper.accessor((row) => row.used_amount[1], { + id: TableHeaderID.MemoryAmount, + header: customTh(), + cell: customCell() + }), + columnHelper.accessor((row) => row.used[2], { + id: TableHeaderID.Storage, + header: customTh(), + cell: getUnit('storage') + }), + columnHelper.accessor((row) => row.used_amount[2], { + id: TableHeaderID.StorageAmount, + header: customTh(), + cell: customCell() + }), + columnHelper.accessor((row) => row.used[3], { + id: TableHeaderID.Network, + header: customTh(), + cell: customCell() + }), + columnHelper.accessor((row) => row.used_amount[3], { + id: TableHeaderID.NetworkAmount, + header: customTh(), + cell: getUnit('network') + }), + columnHelper.accessor((row) => row.used[4], { + id: TableHeaderID.Port, + header: customTh(), + cell: getUnit('services.nodeports') + }), + columnHelper.accessor((row) => row.used_amount[4], { + id: TableHeaderID.PortAmount, + header: customTh(), + cell: customCell() + }), + columnHelper.accessor((row) => row.time, { + id: TableHeaderID.TransactionTime, + header: customTh(), + cell(props) { + const time = props.cell.getValue(); + return ( + format(subHours(parseISO(time), 1), 'yyyy-MM-dd HH:MM') + + ' ~ ' + + format(parseISO(time), 'HH:MM') + ); + } + }), + columnHelper.accessor((row) => row.amount, { + id: TableHeaderID.TotalAmount, + header: customTh(true), + cell: getAmountCell({ isTotal: true }), + enablePinning: true + }) + ]; + }, [t, currency]); + const table = useReactTable({ + data, + state: { + columnPinning: { + left: [TableHeaderID.APPName], + right: [TableHeaderID.TotalAmount] + } + }, + columns, + getCoreRowModel: getCoreRowModel() + }); + return ; +} diff --git a/frontend/providers/costcenter/src/components/valuation/quota.tsx b/frontend/providers/costcenter/src/components/valuation/quota.tsx index 6693f1571ed1..a8a94af2276c 100644 --- a/frontend/providers/costcenter/src/components/valuation/quota.tsx +++ b/frontend/providers/costcenter/src/components/valuation/quota.tsx @@ -2,62 +2,94 @@ import { valuationMap } from '@/constants/payment'; import { UserQuotaItemType } from '@/pages/api/getQuota'; import request from '@/service/request'; import useEnvStore from '@/stores/env'; +import CpuIcon from '../icons/CpuIcon'; +import { MemoryIcon } from '../icons/MemoryIcon'; +import { StorageIcon } from '../icons/StorageIcon'; import { ApiResp } from '@/types'; -import { Flex, Stack } from '@chakra-ui/react'; +import { + Box, + Divider, + Flex, + Heading, + HStack, + Img, + Stack, + StackProps, + Text +} from '@chakra-ui/react'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; -import MyTooltip from '../MyTooltip'; - -export default function Quota() { +const QuotaPie = dynamic(() => import('../cost_overview/components/quotaPieChart'), { ssr: false }); +import dynamic from 'next/dynamic'; +export default function Quota(props: StackProps) { const { t } = useTranslation(); const { data } = useQuery(['quota'], () => request>('/api/getQuota') ); - const quota = (data?.data?.quota || []).flatMap((_quota) => { - const x = valuationMap.get(_quota.type); - if (!x) return []; - return [ - { - ..._quota, - unit: x.unit, - bg: x.bg - } - ]; - }); - const gpuEnabled = useEnvStore((s) => s.gpuEnabled); + const quota = (data?.data?.quota || []) + .filter((d) => d.type !== 'gpu') + .map((d) => { + return { + ...d, + title: t(d.type), + unit: valuationMap.get(d.type)?.unit, + bg: valuationMap.get(d.type)?.bg + }; + }); return ( - - {quota - .filter((x) => gpuEnabled || x.type !== 'gpu') - .map((item) => ( - - - {t(item.type)} - - - - - - ))} + + {quota.map((item) => ( + + + + + {item.type === 'cpu' ? ( + + ) : item.type === 'memory' ? ( + + ) : item.type === 'storage' ? ( + + ) : ( + <> + )} + + {t(item.type)} + + + + + {' '} + {t('Used')}: {item.used} + {item.unit} + + + + {' '} + {t('Remain')}: {item.limit - item.used} + {item.unit} + + + + {' '} + {t('Total')}: {item.limit} + {item.unit} + + + + + ))} ); } diff --git a/frontend/providers/costcenter/src/components/valuation/quotaPie.tsx b/frontend/providers/costcenter/src/components/valuation/quotaPie.tsx new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/frontend/providers/costcenter/src/constants/billing.ts b/frontend/providers/costcenter/src/constants/billing.ts index b0508bc7a060..b75b9151d568 100644 --- a/frontend/providers/costcenter/src/constants/billing.ts +++ b/frontend/providers/costcenter/src/constants/billing.ts @@ -30,15 +30,23 @@ export enum TableHeaderID { 'TransactionTime' = 'Transaction Time', 'APPType' = 'APP Type', 'CPU' = 'CPU', + 'CPUAmount' = 'CPU Amount', 'GPU' = 'GPU', 'Port' = 'Port', + 'PortAmount' = 'Port Amount', 'TrueAmount' = 'True Amount', 'Memory' = 'Memory', + 'MemoryAmount' = 'Memory Amount', 'Storage' = 'Storage', + 'StorageAmount' = 'Storage Amount', 'Network' = 'Network', + 'NetworkAmount' = 'Network Amount', 'TotalAmount' = 'Total Amount', 'Handle' = 'Handle', - 'Namespace' = 'Namespace', + 'Region' = 'Region', + 'Namespace' = 'workspace', 'TransferType' = 'Transfer Type', 'TraderID' = 'Trader ID' } + +export const resourceType = ['cpu', 'memory', 'storage', 'network', 'nodeports'] as const; diff --git a/frontend/providers/costcenter/src/constants/payment.ts b/frontend/providers/costcenter/src/constants/payment.ts index c2f95b661419..a2568acc78cd 100644 --- a/frontend/providers/costcenter/src/constants/payment.ts +++ b/frontend/providers/costcenter/src/constants/payment.ts @@ -93,5 +93,12 @@ export const valuationMap = new Map([ ['storage', { unit: 'GB', scale: 1024, bg: '#9A8EE0', idx: 2 }], ['gpu', { unit: 'GPU', scale: 1000, bg: '#6FCA88', idx: 3 }], ['network', { unit: 'M', scale: 1, bg: '#F182AA', idx: 4 }], - ['services.nodeports', { unit: '', scale: 1, bg: '#F182AA', idx: 4 }] + ['services.nodeports', { unit: '', scale: 1, bg: '#F182AA', idx: 5 }] ]); +// export const BillingUnitMap = new Map([ +// ['cpu', { unit: ''}] +// ['port', { unit: ''}] +// ['port', { unit: ''}] +// ['port', { unit: ''}] +// ['port', { unit: ''}] +// ]) diff --git a/frontend/providers/costcenter/src/layout/index.tsx b/frontend/providers/costcenter/src/layout/index.tsx index cd3a6a5bca63..f08b791245bc 100644 --- a/frontend/providers/costcenter/src/layout/index.tsx +++ b/frontend/providers/costcenter/src/layout/index.tsx @@ -35,7 +35,7 @@ export default function Layout({ children }: any) { w="100vw" h="100vh" position="relative" - background="#EAEBF0" + background={'grayModern.100'} pt={'4px'} pb="10px" alignItems={'center'} diff --git a/frontend/providers/costcenter/src/layout/sidebar.tsx b/frontend/providers/costcenter/src/layout/sidebar.tsx index 1fd4dea6925c..7ca81b42aeb7 100644 --- a/frontend/providers/costcenter/src/layout/sidebar.tsx +++ b/frontend/providers/costcenter/src/layout/sidebar.tsx @@ -1,13 +1,17 @@ -import { Flex, Text, Img } from '@chakra-ui/react'; +import { Flex, Text, Img, Divider, Box } from '@chakra-ui/react'; import { useRouter } from 'next/router'; -import bar_a_icon from '@/assert/bar_chart_4_bars_black.svg'; -import bar_icon from '@/assert/bar_chart_4_bars.svg'; import letter_icon from '@/assert/format_letter_spacing_standard.svg'; import letter_a_icon from '@/assert/format_letter_spacing_standard_black.svg'; import receipt_icon from '@/assert/receipt_long.svg'; import receipt_a_icon from '@/assert/receipt_long_black.svg'; +import layers_icon from '@/assert/layers.svg'; +import layers_a_icon from '@/assert/layers_black.svg'; +import linechart_icon from '@/assert/lineChart.svg'; +import linechart_a_icon from '@/assert/lineChart_black.svg'; import invoice_icon from '@/assert/invoice.svg'; import invoice_a_icon from '@/assert/invoice-active.svg'; +import dashbordIcon from '@/assert/dashboard.svg'; +import dashboard_a_icon from '@/assert/dashboard_black.svg'; import type { StaticImageData } from 'next/image'; import { useTranslation } from 'next-i18next'; import useEnvStore from '@/stores/env'; @@ -29,9 +33,17 @@ export default function SideBar() { { id: 'CostOverview', url: '/cost_overview', + value: 'SideBar.Index', + icon: dashbordIcon, + aicon: dashboard_a_icon, + display: true + }, + { + id: 'BillingOverview', + url: '/app_overview', value: 'SideBar.CostOverview', - icon: bar_icon, - aicon: bar_a_icon, + icon: linechart_icon, + aicon: linechart_a_icon, display: true }, { @@ -42,6 +54,14 @@ export default function SideBar() { aicon: receipt_a_icon, display: true }, + { + id: 'ResourceAnalysis', + url: '/resource_analysis', + value: 'SideBar.resource_analysis', + icon: layers_icon, + aicon: layers_a_icon, + display: true + }, { id: 'ValuationStandard', url: '/valuation', @@ -60,38 +80,47 @@ export default function SideBar() { } ]; return ( - + {ready && menus .filter((item) => item.display) - .map((item) => { + .map((item, idx) => { return ( - { - router.push(item.url); - }} - as="button" - > - - icon of module - - + { + router.push(item.url); + }} + as="button" + fontWeight={500} + fontSize={'14px'} > - {t(item.value)} - - + + icon of module + + + {t(item.value)} + + + {[0, 2, 4].includes(idx) && } +
); })}
diff --git a/frontend/providers/costcenter/src/pages/_app.tsx b/frontend/providers/costcenter/src/pages/_app.tsx index a9c64079469f..a8ea00fb23cd 100644 --- a/frontend/providers/costcenter/src/pages/_app.tsx +++ b/frontend/providers/costcenter/src/pages/_app.tsx @@ -4,7 +4,7 @@ import { EVENT_NAME } from 'sealos-desktop-sdk'; import '@/styles/globals.scss'; import { theme } from '@/styles/chakraTheme'; import { ChakraProvider } from '@chakra-ui/react'; -import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { Hydrate, QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'; import type { AppProps } from 'next/app'; import Router, { useRouter } from 'next/router'; import NProgress from 'nprogress'; @@ -16,6 +16,8 @@ import request from '@/service/request'; import { ApiResp } from '@/types/api'; import { Response as initDataRes } from '@/pages/api/platform/getAppConfig'; import useEnvStore from '@/stores/env'; +import useAppTypeStore from '@/stores/appType'; +import useBillingStore from '@/stores/billing'; // Make sure to call `loadStripe` outside a component’s render to avoid // recreating the `Stripe` object on every render. @@ -36,6 +38,8 @@ Router.events.on('routeChangeError', () => NProgress.done()); const App = ({ Component, pageProps }: AppProps) => { const state = useEnvStore(); const router = useRouter(); + const { setAppTypeMap, appTypeMap } = useAppTypeStore(); + const { setAppTypeList } = useBillingStore(); useEffect(() => { const changeI18n = (data: { currentLanguage: string }) => { router.replace(router.basePath, router.asPath, { locale: data.currentLanguage }); @@ -76,6 +80,23 @@ const App = ({ Component, pageProps }: AppProps) => { }; }, []); + useEffect(() => { + (async () => { + const { data } = await queryClient.fetchQuery({ + queryFn() { + return request }>>( + '/api/billing/getAppList' + ); + }, + queryKey: ['appList'] + }); + const record = data?.appMap; + if (record) { + setAppTypeMap(new Map(Object.entries(record))); + setAppTypeList(['all_app_type', ...(Object.values(record) || [])]); + } + })(); + }, []); return ( diff --git a/frontend/providers/costcenter/src/pages/api/account/getAmount.ts b/frontend/providers/costcenter/src/pages/api/account/getAmount.ts index 2ea5d37ef76f..bffac1f20d31 100644 --- a/frontend/providers/costcenter/src/pages/api/account/getAmount.ts +++ b/frontend/providers/costcenter/src/pages/api/account/getAmount.ts @@ -14,8 +14,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return jsonRes(res, { code: 401, message: 'user null' }); } const body = JSON.stringify({ - kubeConfig: kc.exportConfig(), - owner: user.name + kubeConfig: kc.exportConfig() + // owner: user.name }); const response = await fetch(base + '/account/v1alpha1/account', { method: 'POST', diff --git a/frontend/providers/costcenter/src/pages/api/account/payment/stripe.ts b/frontend/providers/costcenter/src/pages/api/account/payment/stripe.ts index 7a9ecad8c646..87a4a5ea7742 100644 --- a/frontend/providers/costcenter/src/pages/api/account/payment/stripe.ts +++ b/frontend/providers/costcenter/src/pages/api/account/payment/stripe.ts @@ -37,7 +37,6 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse const paymentCrd = generatePaymentCrd(form); const res = await ApplyYaml(kc, paymentCrd); - console.log(res); return jsonRes(resp, { data: { paymentName: paymentName, diff --git a/frontend/providers/costcenter/src/pages/api/account/transfer.ts b/frontend/providers/costcenter/src/pages/api/account/transfer.ts index 1e035d34ca96..442fa781f30f 100644 --- a/frontend/providers/costcenter/src/pages/api/account/transfer.ts +++ b/frontend/providers/costcenter/src/pages/api/account/transfer.ts @@ -7,7 +7,6 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse if (!global.AppConfig.costCenter.transferEnabled) { throw new Error('transfer is not enabled'); } - // console.log(global) const { amount, to: toUser } = req.body; const kc = await authSession(req.headers); diff --git a/frontend/providers/costcenter/src/pages/api/billing/appBilling.ts b/frontend/providers/costcenter/src/pages/api/billing/appBilling.ts new file mode 100644 index 000000000000..f23e7f1797fe --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/billing/appBilling.ts @@ -0,0 +1,74 @@ +import { authSession } from '@/service/backend/auth'; +import { jsonRes } from '@/service/backend/response'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { formatISO } from 'date-fns'; +import { getRegionByUid, makeAPIURL } from '@/service/backend/region'; +import { AppListItem } from '@/types/app'; +import { APPBillingItem } from '@/types'; +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 = formatISO(new Date(), { + representation: 'complete' + }), + startTime = formatISO(new Date(), { + representation: 'complete' + }), + regionUid, + appType, + orderID, + appName, + namespace, + page = 1, + pageSize = 100 + } = req.body as { + endTime?: Date; + startTime?: Date; + regionUid: string; + appType: string; + appName: string; + namespace: string; + orderID?: string; + pageSize: number; + page: number; + }; + + const region = await getRegionByUid(regionUid); + const url = makeAPIURL(region, '/account/v1alpha1/costs/app'); + const bodyRaw = { + endTime, + kubeConfig: kc.exportConfig(), + startTime, + appType, + appName, + orderID, + namespace, + page, + pageSize + }; + const body = JSON.stringify(bodyRaw); + const response = await fetch(url, { + method: 'POST', + body + }); + const res = await response.json(); + if (!response.ok) { + console.log(res); + throw Error('get appbilling error'); + } + const data = res.app_costs; + return jsonRes(resp, { + code: 200, + data + }); + } catch (error) { + console.log(error); + jsonRes(resp, { code: 500, message: 'get appbilling error' }); + } +} diff --git a/frontend/providers/costcenter/src/pages/api/billing/appOverview.ts b/frontend/providers/costcenter/src/pages/api/billing/appOverview.ts new file mode 100644 index 000000000000..c0db20b8e480 --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/billing/appOverview.ts @@ -0,0 +1,89 @@ +import { authSession } from '@/service/backend/auth'; +import { jsonRes } from '@/service/backend/response'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { formatISO, subMonths } from 'date-fns'; +import { getRegionByUid, getRegionList, makeAPIURL } from '@/service/backend/region'; +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' }); + } + // return jsonRes(resp, { + // code: 200, + // data: {"overviews":[{"amount":60492,"namespace":"ns-5uxfy8jl","regionDomain":"","appType":2,"appName":"hello-world"}],"total":1,"totalPage":1} + // }); + const { + endTime = formatISO(new Date(), { + representation: 'complete' + }), + startTime = formatISO(new Date(), { + representation: 'complete' + }), + regionUid, + appType, + appName, + namespace, + pageSize, + page + } = req.body as { + endTime?: Date; + startTime?: Date; + regionUid: string; + appType: string; + appName: string; + namespace: string; + pageSize: number; + page: number; + }; + if (!endTime) + return jsonRes(resp, { + code: 400, + message: 'endTime is invalid' + }); + if (!startTime) + return jsonRes(resp, { + code: 400, + message: 'endTime is invalid' + }); + if (!page) + return jsonRes(resp, { + code: 400, + message: 'page is invalid' + }); + if (!pageSize) + return jsonRes(resp, { + code: 400, + message: 'pageSize is invalid' + }); + const region = await getRegionByUid(regionUid); + const url = makeAPIURL(region, '/account/v1alpha1/cost-overview'); + const bodyRaw = { + endTime, + kubeConfig: kc.exportConfig(), + startTime, + appType, + appName, + namespace, + page, + pageSize + }; + const body = JSON.stringify(bodyRaw); + const response = await fetch(url, { + method: 'POST', + body + }); + if (!response.ok) { + throw Error('get cost overview error'); + } + const result = await response.json(); + return jsonRes(resp, { + code: 200, + data: result.data + }); + } catch (error) { + console.log(error); + jsonRes(resp, { code: 500, message: 'interval server error' }); + } +} diff --git a/frontend/providers/costcenter/src/pages/api/billing/buget.ts b/frontend/providers/costcenter/src/pages/api/billing/buget.ts deleted file mode 100644 index 484470bf179f..000000000000 --- a/frontend/providers/costcenter/src/pages/api/billing/buget.ts +++ /dev/null @@ -1,66 +0,0 @@ -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, appType, namespace } = req.body as { - endTime?: Date; - startTime?: Date; - appType?: string; - namespace?: string; - }; - if (!endTime) - return jsonRes(resp, { - code: 400, - message: 'endTime is invalid' - }); - if (!startTime) - return jsonRes(resp, { - code: 400, - message: 'startTime is invalid' - }); - const base = global.AppConfig.costCenter.components.accountService.url as string; - const consumptionUrl = base + '/account/v1alpha1/costs/consumption'; - const rechagreUrl = base + '/account/v1alpha1/costs/recharge'; - - const results = await Promise.all([ - ( - await fetch(consumptionUrl, { - method: 'POST', - body: JSON.stringify({ - endTime, - kubeConfig: kc.exportConfig(), - owner: user.name, - appType, - namespace, - startTime - }) - }) - ).json(), - ( - await fetch(rechagreUrl, { - method: 'POST', - body: JSON.stringify({ - endTime, - kubeConfig: kc.exportConfig(), - owner: user.name, - appType - }) - }) - ).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/consumption.ts b/frontend/providers/costcenter/src/pages/api/billing/consumption.ts new file mode 100644 index 000000000000..9b4f1adeabcc --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/billing/consumption.ts @@ -0,0 +1,65 @@ +import { authSession } from '@/service/backend/auth'; +import { getRegionByUid, makeAPIURL } from '@/service/backend/region'; +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, + appType = '', + namespace = '', + appName = '', + regionUid + } = req.body as { + endTime?: Date; + startTime?: Date; + appType?: string; + namespace?: string; + appName?: string; + regionUid?: string; + }; + if (!endTime) + return jsonRes(resp, { + code: 400, + message: 'endTime is invalid' + }); + if (!startTime) + return jsonRes(resp, { + code: 400, + message: 'startTime is invalid' + }); + const bodyRaw = { + endTime, + kubeConfig: kc.exportConfig(), + startTime, + appType, + appName, + namespace + }; + const region = await getRegionByUid(regionUid); + const consumptionUrl = makeAPIURL(region, '/account/v1alpha1/costs/consumption'); + + const results = await fetch(consumptionUrl, { + method: 'POST', + body: JSON.stringify(bodyRaw) + }); + const data = await results.json(); + if (!results.ok) { + console.log(data); + throw Error(); + } + return jsonRes(resp, { + code: 200, + data + }); + } catch (error) { + console.log(error); + jsonRes(resp, { code: 500, message: 'get consumption error' }); + } +} diff --git a/frontend/providers/costcenter/src/pages/api/billing/costDistrube.ts b/frontend/providers/costcenter/src/pages/api/billing/costDistrube.ts new file mode 100644 index 000000000000..f74e5299248f --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/billing/costDistrube.ts @@ -0,0 +1,68 @@ +import { authSession } from '@/service/backend/auth'; +import { jsonRes } from '@/service/backend/response'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { formatISO, subMonths } from 'date-fns'; +import { getRegionByUid, getRegionList, makeAPIURL } from '@/service/backend/region'; +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 = formatISO(new Date(), { + representation: 'complete' + }), + startTime = formatISO(new Date(), { + representation: 'complete' + }), + regionUid, + appType, + appName, + namespace + } = req.body as { + endTime?: Date; + startTime?: Date; + regionUid: string; + appType: string; + appName: string; + namespace: string; + }; + if (!endTime) + return jsonRes(resp, { + code: 400, + message: 'endTime is invalid' + }); + if (!startTime) + return jsonRes(resp, { + code: 400, + message: 'endTime is invalid' + }); + const region = await getRegionByUid(regionUid); + const url = makeAPIURL(region, '/account/v1alpha1/cost-basic-distribution'); + const body = JSON.stringify({ + endTime, + kubeConfig: kc.exportConfig(), + startTime, + appType, + appName, + namespace + }); + const response = await fetch(url, { + method: 'POST', + body + }); + if (!response.ok) { + throw Error('get cost overview error'); + } + const result = await response.json(); + return jsonRes(resp, { + code: 200, + data: result.data + }); + } catch (error) { + console.log(error); + jsonRes(resp, { code: 500, message: 'interval server error' }); + } +} diff --git a/frontend/providers/costcenter/src/pages/api/billing/costs.ts b/frontend/providers/costcenter/src/pages/api/billing/costs.ts index 8029f151ba36..8563caaf5db2 100644 --- a/frontend/providers/costcenter/src/pages/api/billing/costs.ts +++ b/frontend/providers/costcenter/src/pages/api/billing/costs.ts @@ -1,4 +1,5 @@ import { authSession } from '@/service/backend/auth'; +import { getRegionList } from '@/service/backend/region'; import { jsonRes } from '@/service/backend/response'; import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, resp: NextApiResponse) { @@ -22,23 +23,81 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse code: 400, message: 'endTime is invalid' }); - const url = - global.AppConfig.costCenter.components.accountService.url + '/account/v1alpha1/costs'; - const res = await ( - await fetch(url, { - method: 'POST', - body: JSON.stringify({ - endTime, - kubeConfig: kc.exportConfig(), - owner: user.name, - startTime + const regions = await getRegionList(); + if (!regions) throw Error('get all regions error'); + const urls = regions.map( + (region: { accountSvc: string }) => 'http://' + region.accountSvc + '/account/v1alpha1/costs' + ) as string[]; + const body = JSON.stringify({ + endTime, + kubeConfig: kc.exportConfig(), + startTime + }); + const reslistRaw = await Promise.all( + urls.map((url) => + fetch(url, { + method: 'POST', + body + }) + ) + ); + // if (reslistRaw.some((res) => !res.ok)) throw Error('get costs list error'); + const resultList = await Promise.all(reslistRaw.map((res) => res.json())); + + const someArr = resultList + .map<{ data: [number, string][]; region: { name: { zh: string; en: string } } }>( + (result, i) => ({ + data: result?.data?.costs || [], + region: regions[i] }) - }) - ).json(); + ) + .map< + [ + [number, string][], + { + zh: string; + en: string; + } + ] + >((d) => [d.data.toSorted(([aDate], [bDate]) => aDate - bDate), d.region.name]); + + let pointers: number[] = new Array(someArr.length).fill(0); + const maxPointers = someArr.map((d) => d[0].length); + let mergedData: [number, string][] = []; + // merge multi-region datas + while (pointers.some((v, i) => maxPointers[i] > v)) { + let minPair: (typeof mergedData)[number] = [new Date().getTime() / 1000, '0']; + let minRegionIndex = -1; + // find out minRegionIndex & minPair by older date + for (let i = 0; i < pointers.length; i++) { + if (pointers[i] >= maxPointers[i]) { + continue; + } + const currentRegionData = someArr[i][0]; + const currentRegionPointer = pointers[i]; + const currentRegionPair = currentRegionData[currentRegionPointer]; + if (currentRegionPair[0] > minPair[0]) continue; + minPair = currentRegionPair; + minRegionIndex = i; + } + if (minRegionIndex === -1) break; + if (mergedData.length === 0) mergedData.push([...minPair]); + else { + const latestMegedPair = mergedData[mergedData.length - 1]; + if (latestMegedPair[0] === minPair[0]) { + const mergedAmount = BigInt(latestMegedPair[1]); + const minPairAmount = BigInt(minPair[1]); + latestMegedPair[1] = (mergedAmount + minPairAmount).toString(); + } else { + mergedData.push([...minPair]); + } + } + pointers[minRegionIndex]++; + } + someArr.unshift([mergedData, { zh: '汇总', en: 'Total' }]); return jsonRes(resp, { code: 200, - data: res.data, - message: res.message + data: someArr }); } catch (error) { console.log(error); diff --git a/frontend/providers/costcenter/src/pages/api/billing/getAppList.ts b/frontend/providers/costcenter/src/pages/api/billing/getAppList.ts index eea1f6e20028..d579b4d15576 100644 --- a/frontend/providers/costcenter/src/pages/api/billing/getAppList.ts +++ b/frontend/providers/costcenter/src/pages/api/billing/getAppList.ts @@ -6,54 +6,19 @@ import { ApplyYaml } from '@/service/backend/kubernetes'; import * as yaml from 'js-yaml'; 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 namespace = GetUserDefaultNameSpace(user.name); - const name = new Date().getTime() + 'appquery'; - const crdSchema = { - apiVersion: `account.sealos.io/v1`, - kind: 'BillingInfoQuery', - metadata: { - name - }, - spec: { - queryType: 'AppType' - } - }; - 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(); - }); + const url = + global.AppConfig.costCenter.components.accountService.url + + '/account/v1alpha1/cost-app-type-list'; + const res = await ( + await fetch(url, { + method: 'POST' + }) + ).json(); + const appMap = res.data; return jsonRes(resp, { code: 200, data: { - appList: JSON.parse(result) + appMap } }); } catch (error) { diff --git a/frontend/providers/costcenter/src/pages/api/billing/getAppNameList.ts b/frontend/providers/costcenter/src/pages/api/billing/getAppNameList.ts new file mode 100644 index 000000000000..5f2bb1006dad --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/billing/getAppNameList.ts @@ -0,0 +1,82 @@ +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 { formatISO } from 'date-fns'; +import * as yaml from 'js-yaml'; +import { getRegionByUid, makeAPIURL } from '@/service/backend/region'; +import { AppListItem } from '@/types/app'; +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 = formatISO(new Date(), { + representation: 'complete' + }), + startTime = formatISO(new Date(), { + representation: 'complete' + }), + regionUid, + appType, + appName, + namespace + } = req.body as { + endTime?: Date; + startTime?: Date; + regionUid: string; + appType: string; + appName: string; + namespace: string; + }; + if (!endTime) + return jsonRes(resp, { + code: 400, + message: 'endTime is invalid' + }); + if (!startTime) + return jsonRes(resp, { + code: 400, + message: 'endTime is invalid' + }); + const region = await getRegionByUid(regionUid); + const url = makeAPIURL(region, '/account/v1alpha1/cost-app-list'); + const bodyRaw = { + endTime, + kubeConfig: kc.exportConfig(), + startTime, + appType, + appName, + namespace + }; + const body = JSON.stringify(bodyRaw); + + const response = await fetch(url, { + method: 'POST', + body + }); + const res = await response.json(); + if (!response.ok) { + console.log(res); + throw Error('get applist error'); + } + + const data = res.data as { + apps: AppListItem[]; + total: number; + totalPage: number; + }; + return jsonRes(resp, { + code: 200, + data, + message: res + }); + } catch (error) { + console.log(error); + jsonRes(resp, { code: 500, message: 'get applist error' }); + } +} diff --git a/frontend/providers/costcenter/src/pages/api/billing/getNamespaceList.ts b/frontend/providers/costcenter/src/pages/api/billing/getNamespaceList.ts index 6862781c5d40..7611ef4c8969 100644 --- a/frontend/providers/costcenter/src/pages/api/billing/getNamespaceList.ts +++ b/frontend/providers/costcenter/src/pages/api/billing/getNamespaceList.ts @@ -1,4 +1,5 @@ import { authSession } from '@/service/backend/auth'; +import { getRegionByUid, makeAPIURL } from '@/service/backend/region'; import { jsonRes } from '@/service/backend/response'; import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, resp: NextApiResponse) { @@ -8,9 +9,10 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse if (user === null) { return jsonRes(resp, { code: 403, message: 'user null' }); } - const { endTime, startTime } = req.body as { + const { endTime, startTime, regionUid } = req.body as { endTime?: Date; startTime?: Date; + regionUid: string; }; if (!endTime) return jsonRes(resp, { @@ -22,15 +24,14 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse code: 400, message: 'endTime is invalid' }); - const url = - global.AppConfig.costCenter.components.accountService.url + '/account/v1alpha1/namespaces'; + const region = await getRegionByUid(regionUid); + const url = makeAPIURL(region, '/account/v1alpha1/namespaces'); const res = await ( await fetch(url, { method: 'POST', body: JSON.stringify({ endTime, kubeConfig: kc.exportConfig(), - owner: user.name, startTime, type: 0 }) diff --git a/frontend/providers/costcenter/src/pages/api/billing/index.ts b/frontend/providers/costcenter/src/pages/api/billing/index.ts index 286cce25ad96..aaaaabc9de9a 100644 --- a/frontend/providers/costcenter/src/pages/api/billing/index.ts +++ b/frontend/providers/costcenter/src/pages/api/billing/index.ts @@ -6,6 +6,7 @@ import { ApplyYaml } from '@/service/backend/kubernetes'; import * as yaml from 'js-yaml'; import crypto from 'crypto'; import type { BillingData, BillingItem, BillingSpec, Costs, RawCosts } from '@/types/billing'; +import { formatISO, subMonths } from 'date-fns'; const convertGpu = (_deduction?: RawCosts) => _deduction ? (Object.entries(_deduction) as [keyof RawCosts, number][]).reduce( @@ -70,6 +71,19 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse namespace, plural: 'billingrecordqueries' }; + const url = + global.AppConfig.costCenter.components.accountService.url + '/account/v1alpha1/cost-app-list'; + const res = await ( + await fetch(url, { + method: 'POST', + body: JSON.stringify({ + endTime: formatISO(subMonths(new Date(), 2), { representation: 'complete' }), + kubeConfig: kc.exportConfig(), + owner: user.name, + startTime: formatISO(new Date(), { representation: 'complete' }) + }) + }) + ).json(); try { await ApplyYaml(kc, yaml.dump(crdSchema)); await new Promise((resolve) => setTimeout(() => resolve(), 1000)); diff --git a/frontend/providers/costcenter/src/pages/api/billing/recharge.ts b/frontend/providers/costcenter/src/pages/api/billing/recharge.ts index 89adb3e4995c..e41dbcab259d 100644 --- a/frontend/providers/costcenter/src/pages/api/billing/recharge.ts +++ b/frontend/providers/costcenter/src/pages/api/billing/recharge.ts @@ -4,6 +4,7 @@ import { jsonRes } from '@/service/backend/response'; import { GetUserDefaultNameSpace } from '@/service/backend/kubernetes'; import { BillingData, BillingSpec, RechargeBillingData } from '@/types'; import crypto from 'crypto'; +import { getRegionByUid, getRegionList, makeAPIURL } from '@/service/backend/region'; export default async function handler(req: NextApiRequest, resp: NextApiResponse) { try { @@ -13,9 +14,6 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse if (user === null) { return jsonRes(resp, { code: 403, message: 'user null' }); } - const namespace = GetUserDefaultNameSpace(user.name); - const body = req.body; - let spec: BillingSpec = body.spec; const { endTime, startTime } = req.body as { endTime?: Date; startTime?: Date; @@ -25,37 +23,27 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse code: 400, message: 'endTime is invalid' }); - if (!startTime) - return jsonRes(resp, { - code: 400, - message: 'endTime is invalid' - }); - const data = { + const queryRaw = { endTime, kubeConfig: kc.exportConfig(), - owner: user.name, startTime }; - const url = - global.AppConfig.costCenter.components.accountService.url + '/account/v1alpha1/payment'; - const response = await fetch(url, { + const rechagreUrl = makeAPIURL(null, '/account/v1alpha1/costs/recharge'); + const response = await fetch(rechagreUrl, { method: 'POST', - body: JSON.stringify(data) + body: JSON.stringify(queryRaw) }); - if (!response.clone().ok) - return jsonRes(resp, { - code: 404, - data: { - payment: [] - } - }); - const res = (await response.clone().json()) as RechargeBillingData; + const result = await response.json(); + if (!response.ok) { + console.log(result); + throw Error(); + } return jsonRes(resp, { - data: res + data: result }); } catch (error) { console.log(error); - jsonRes(resp, { code: 500, message: 'get billing error' }); + jsonRes(resp, { code: 500, message: 'get recharge error' }); } } diff --git a/frontend/providers/costcenter/src/pages/api/billing/rechargeBillingList.ts b/frontend/providers/costcenter/src/pages/api/billing/rechargeBillingList.ts new file mode 100644 index 000000000000..9225553f0b9c --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/billing/rechargeBillingList.ts @@ -0,0 +1,57 @@ +import { authSession } from '@/service/backend/auth'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/backend/response'; +import { GetUserDefaultNameSpace } from '@/service/backend/kubernetes'; +import { BillingSpec, RechargeBillingData } from '@/types'; +import { parseISO } from 'date-fns'; +import { makeAPIURL } from '@/service/backend/region'; + +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + const kc = await authSession(req.headers); + // get user account payment amount + const user = kc.getCurrentUser(); + if (user === null) { + return jsonRes(resp, { code: 403, message: 'user null' }); + } + const { 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 data = { + endTime, + kubeConfig: kc.exportConfig(), + owner: user.name, + startTime + }; + const url = makeAPIURL(null, '/account/v1alpha1/payment'); + const response = await fetch(url, { + method: 'POST', + body: JSON.stringify(data) + }); + if (!response.clone().ok) + return jsonRes(resp, { + code: 404, + data: { + payment: [] + } + }); + const res = (await response.clone().json()) as RechargeBillingData; + return jsonRes(resp, { + data: res + }); + } catch (error) { + console.log(error); + jsonRes(resp, { code: 500, message: 'get billing error' }); + } +} diff --git a/frontend/providers/costcenter/src/pages/api/billing/rechargeList.ts b/frontend/providers/costcenter/src/pages/api/billing/rechargeList.ts new file mode 100644 index 000000000000..f4d0ded8bdd7 --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/billing/rechargeList.ts @@ -0,0 +1,57 @@ +import { authSession } from '@/service/backend/auth'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/backend/response'; +import { GetUserDefaultNameSpace } from '@/service/backend/kubernetes'; +import { BillingSpec, RechargeBillingData } from '@/types'; +import { parseISO } from 'date-fns'; +import { makeAPIURL } from '@/service/backend/region'; + +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + const kc = await authSession(req.headers); + // get user account payment amount + const user = kc.getCurrentUser(); + if (user === null) { + return jsonRes(resp, { code: 403, message: 'user null' }); + } + const { 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 data = { + endTime, + kubeConfig: kc.exportConfig(), + owner: user.name, + startTime + }; + const url = makeAPIURL(null, '/account/v1alpha1/payment'); + const response = await fetch(url, { + method: 'POST', + body: JSON.stringify(data) + }); + if (!response.clone().ok) + return jsonRes(resp, { + code: 404, + data: { + payment: [] + } + }); + const res = (await response.clone().json()) as RechargeBillingData; + return jsonRes(resp, { + data: res.payment.map((payment) => [parseISO(payment.CreatedAt).getTime(), payment.Amount]) + }); + } catch (error) { + console.log(error); + jsonRes(resp, { code: 500, message: 'get billing error' }); + } +} diff --git a/frontend/providers/costcenter/src/pages/api/billing/regionCost.ts b/frontend/providers/costcenter/src/pages/api/billing/regionCost.ts new file mode 100644 index 000000000000..1d5678b13e60 --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/billing/regionCost.ts @@ -0,0 +1,65 @@ +import { authSession } from '@/service/backend/auth'; +import { getRegionByUid, getRegionList, makeAPIURL } from '@/service/backend/region'; +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, + regionUid, + appType = '', + appName = '', + namespace = '' + } = req.body as { + endTime?: Date; + startTime?: Date; + regionUid: string; + appType?: string; + appName?: string; + namespace?: string; + }; + if (!endTime) + return jsonRes(resp, { + code: 400, + message: 'endTime is invalid' + }); + if (!startTime) + return jsonRes(resp, { + code: 400, + message: 'endTime is invalid' + }); + const regions = await getRegionList(); + if (!regions) throw Error('get all regions error'); + const region = await getRegionByUid(regionUid); + const url = makeAPIURL(region, '/account/v1alpha1/costs'); + const bodyRaw = { + endTime, + kubeConfig: kc.exportConfig(), + startTime, + appType, + appName, + namespace + }; + const body = JSON.stringify(bodyRaw); + const resRaw = await fetch(url, { + method: 'POST', + body + }); + const result = (await resRaw.json()) as { data: { costs: [number, string][] } }; + if (!resRaw.ok) throw Error('get costs list error'); + const data = result.data.costs; + return jsonRes(resp, { + code: 200, + data + }); + } catch (error) { + console.log(error); + jsonRes(resp, { code: 500, message: 'get billing cost error' }); + } +} diff --git a/frontend/providers/costcenter/src/pages/api/billing/transfer.ts b/frontend/providers/costcenter/src/pages/api/billing/transfer.ts index 2b5db90e26a7..4dde3811a81e 100644 --- a/frontend/providers/costcenter/src/pages/api/billing/transfer.ts +++ b/frontend/providers/costcenter/src/pages/api/billing/transfer.ts @@ -39,6 +39,7 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse startTime, transferID: orderID, endTime, + namesapce: GetUserDefaultNameSpace(user.name), type }; diff --git a/frontend/providers/costcenter/src/pages/api/getRegions.ts b/frontend/providers/costcenter/src/pages/api/getRegions.ts new file mode 100644 index 000000000000..49237f3c3480 --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/getRegions.ts @@ -0,0 +1,35 @@ +import { authSession } from '@/service/backend/auth'; +import { getRegionByUid, getRegionList } from '@/service/backend/region'; +import { jsonRes } from '@/service/backend/response'; +import { Region } from '@/types/region'; +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 regions = (await getRegionList()) || []; + const currentRegionUid = global.AppConfig.cloud.regionUID; + const currentRegionIdx = regions.findIndex((region: Region) => region.uid === currentRegionUid); + if (currentRegionIdx === -1) { + throw Error('current region not found'); + } + if (regions.length > 1 && currentRegionIdx !== 0) { + // switch region-0 and region-[currentRegionIdx] + const temp = regions[currentRegionIdx]; + regions[currentRegionIdx] = regions[0]; + regions[0] = temp; + } + if (!regions) throw Error('get all regions error'); + return jsonRes(resp, { + code: 200, + data: regions + }); + } catch (error) { + console.log(error); + jsonRes(resp, { code: 500, message: 'get billing cost error' }); + } +} diff --git a/frontend/providers/costcenter/src/pages/api/platform/getAppConfig.ts b/frontend/providers/costcenter/src/pages/api/platform/getAppConfig.ts index 31603cc01843..a301321b32c4 100644 --- a/frontend/providers/costcenter/src/pages/api/platform/getAppConfig.ts +++ b/frontend/providers/costcenter/src/pages/api/platform/getAppConfig.ts @@ -15,7 +15,7 @@ export type Response = { GPU_ENABLED: boolean; }; -function geAppConfig(defaultConfig: AppConfigType, initConfig: AppConfigType): AppConfigType { +function getAppConfig(defaultConfig: AppConfigType, initConfig: AppConfigType): AppConfigType { function mergeConfig(defaultConfig: any, newConfig: any): any { if (typeof defaultConfig !== 'object' || defaultConfig === null) { return newConfig !== undefined ? newConfig : defaultConfig; @@ -38,8 +38,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!global.AppConfig || process.env.NODE_ENV !== 'production') { const filename = process.env.NODE_ENV === 'development' ? 'data/config.yaml.local' : '/app/data/config.yaml'; - const res: any = yaml.load(readFileSync(filename, 'utf-8')); - global.AppConfig = geAppConfig(DefaultAppConfig, res); + const yamlResult: any = yaml.load(readFileSync(filename, 'utf-8')); + global.AppConfig = getAppConfig(DefaultAppConfig, yamlResult); console.log(global.AppConfig); } jsonRes(res, { diff --git a/frontend/providers/costcenter/src/pages/api/properties.ts b/frontend/providers/costcenter/src/pages/api/properties.ts index a4670ca63825..c515ccd65904 100644 --- a/frontend/providers/costcenter/src/pages/api/properties.ts +++ b/frontend/providers/costcenter/src/pages/api/properties.ts @@ -1,4 +1,5 @@ import { authSession } from '@/service/backend/auth'; +import { getRegionByUid, makeAPIURL } from '@/service/backend/region'; import { jsonRes } from '@/service/backend/response'; import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, resp: NextApiResponse) { @@ -8,14 +9,14 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse if (user === null) { return jsonRes(resp, { code: 403, message: 'user null' }); } - const url = - global.AppConfig.costCenter.components.accountService.url + '/account/v1alpha1/properties'; + const { regionUid } = req.body as { regionUid: string }; + const region = await getRegionByUid(regionUid); + const url = makeAPIURL(region, '/account/v1alpha1/properties'); const res = await ( await fetch(url, { method: 'POST', body: JSON.stringify({ - kubeConfig: kc.exportConfig(), - owner: user.name + kubeConfig: kc.exportConfig() }) }) ).json(); diff --git a/frontend/providers/costcenter/src/pages/app_overview/index.tsx b/frontend/providers/costcenter/src/pages/app_overview/index.tsx new file mode 100644 index 000000000000..a6ce8d07c676 --- /dev/null +++ b/frontend/providers/costcenter/src/pages/app_overview/index.tsx @@ -0,0 +1,214 @@ +import { + Box, + Button, + Flex, + Grid, + GridItem, + Heading, + HStack, + Img, + Tab, + TabList, + TabPanels, + Tabs, + Text, + useMediaQuery +} from '@chakra-ui/react'; +import { createContext, useEffect, useMemo, useState } from 'react'; +import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import { useTranslation } from 'next-i18next'; +import linechart_icon from '@/assert/lineChart.svg'; +import NamespaceMenu from '@/components/menu/NamespaceMenu'; +import useBillingStore from '@/stores/billing'; +import SelectRange from '@/components/billing/selectDateRange'; +import RegionMenu from '@/components/menu/RegionMenu'; +import CycleMenu from '@/components/menu/CycleMenu'; +import AppNameMenu from '@/components/menu/AppNameMenu'; +import AppTypeMenu from '@/components/menu/AppTypeMenu'; +import request from '@/service/request'; +import { ApiResp, AppOverviewBilling } from '@/types'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import useOverviewStore from '@/stores/overview'; +import { AppListItem } from '@/types/app'; +import { AppOverviewTable } from '@/components/table/AppOverviewTable'; +import AmountDisplay from '@/components/billing/AmountDisplay'; +import SwitchPage from '@/components/billing/SwitchPage'; +import { BillingTrend } from '@/components/billing/BillingTrend'; +import { Refresh } from '@/components/Refresh'; +import useAppTypeStore from '@/stores/appType'; + +function Billing() { + const { t } = useTranslation(); + const { setNamespace, getAppName, getAppType, getNamespace, getRegion, getCycle } = + useBillingStore(); + const { startTime, endTime } = useOverviewStore(); + const regionUid = getRegion()?.uid || ''; + const [orderID, setOrderID] = useState(''); + const [page, setPage] = useState(1); + const [totalPage, setTotalPage] = useState(1); + const [totalItem, setTotalItem] = useState(0); + const [pageSize, setPageSize] = useState(3); + const queryBody = { + endTime, + startTime, + regionUid, + appType: getAppType(), + appName: getAppName(), + namespace: getNamespace()?.[0] || '', + page, + pageSize + }; + const { data, isPreviousData, isFetching } = useQuery({ + queryFn() { + return request.post< + any, + ApiResp<{ + overviews: AppOverviewBilling[]; + total: number; + totalPage: number; + }> + >('/api/billing/appOverview', queryBody); + }, + onSuccess(data) { + if (!data.data) { + return; + } + const { total, totalPage } = data.data; + if (totalPage === 0) { + // search reset + setTotalPage(1); + setTotalItem(1); + } else { + setTotalItem(total); + setTotalPage(totalPage); + } + if (totalPage < page) { + setPage(1); + } + }, + keepPreviousData: true, + queryKey: ['appOverviewBilling', queryBody, page, pageSize] + }); + const [isBigScreen1, isBigScreen2] = useMediaQuery([ + '(min-width: 1280px)', + '(min-width: 1500px)' + ]); + const selectMr = '40px'; + const queryClient = useQueryClient(); + const appOverviews = useMemo(() => data?.data?.overviews || [], [data?.data?.overviews]); + return ( + + + + + {t('SideBar.CostOverview')} + { + queryClient.invalidateQueries({}); + }} + /> + + + + + {t('Transaction Time')} + + + + + + + + + {t('region')} + + + + + + {t('workspace')} + + + + + + {t('APP Type')} + + + + + + {t('app_name')} + + + + + + + + + + {' '} + {t('Billing List')} + + + + + + + + + + + + ); +} + +export default Billing; + +export async function getServerSideProps({ locale }: { locale: string }) { + return { + props: { + ...(await serverSideTranslations(locale, ['common', 'applist'], undefined, ['zh', 'en'])) + } + }; +} diff --git a/frontend/providers/costcenter/src/pages/billing/index.tsx b/frontend/providers/costcenter/src/pages/billing/index.tsx index adb38dfce977..e12863d2050a 100644 --- a/frontend/providers/costcenter/src/pages/billing/index.tsx +++ b/frontend/providers/costcenter/src/pages/billing/index.tsx @@ -1,57 +1,74 @@ -import { Flex, Heading, Img, Tab, TabList, TabPanels, Tabs } from '@chakra-ui/react'; -import { createContext, useMemo, useState } from 'react'; +import { Box, Flex, Heading, Img, Tab, TabList, TabPanels, Tabs } from '@chakra-ui/react'; +import { createContext, useEffect, useMemo, useState } from 'react'; import receipt_icon from '@/assert/receipt_long_black.svg'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { useTranslation } from 'next-i18next'; -import NamespaceMenu from '@/components/billing/NamespaceMenu'; +import NamespaceMenu from '@/components/menu/NamespaceMenu'; import RechargeTabPanel from '@/components/billing/RechargeTabPanel'; import InOutTabPanel from '@/components/billing/InOutTabPanel'; import TransferTabPanel from '@/components/billing/TransferTabPnel'; -import useBillingStore from '@/stores/billing'; +import { Refresh } from '@/components/Refresh'; +import { useQueryClient } from '@tanstack/react-query'; function Billing() { const { t } = useTranslation(); - const { setNamespace } = useBillingStore(); + const queryClient = useQueryClient(); return ( - - - - {t('SideBar.BillingDetails')} - - - - - - {t('Expenditure')} - - + + + - {t('Charge')} - - - {t('Transfer')} - - - - - - - - - + + {t('Expenditure')} + + + {t('Charge')} + + + {t('Transfer')} + + { + return queryClient.invalidateQueries(); + }} + ml="auto" + /> + + + + + + + +
+ ); } diff --git a/frontend/providers/costcenter/src/pages/cost_overview/index.tsx b/frontend/providers/costcenter/src/pages/cost_overview/index.tsx index b9a9889ed31a..3934a8e0e6a7 100644 --- a/frontend/providers/costcenter/src/pages/cost_overview/index.tsx +++ b/frontend/providers/costcenter/src/pages/cost_overview/index.tsx @@ -9,17 +9,14 @@ import { Buget } from '@/components/cost_overview/buget'; import UserCard from '@/components/cost_overview/components/user'; import { Cost } from '@/components/cost_overview/cost'; import { Trend } from '@/components/cost_overview/trend'; +import { TrendBar } from '@/components/cost_overview/trendBar'; import useBillingData from '@/hooks/useBillingData'; -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'; -import CurrencySymbol from '@/components/CurrencySymbol'; import useEnvStore from '@/stores/env'; - -const getProperties = () => request.post('/api/billing/propertiesUsedAmount'); +import { ApiResp } from '@/types'; +import request from '@/service/request'; +import { useQuery } from '@tanstack/react-query'; export const RechargeContext = createContext<{ rechargeRef: MutableRefObject | null }>({ rechargeRef: null @@ -58,60 +55,41 @@ function CostOverview() { } }, []); const { NotEnoughModal } = useNotEnough(); - const { data, isInitialLoading } = useBillingData({ pageSize: 3 }); - const billingItems = data?.data?.status.item.filter((v) => v.type === 0) || []; const totast = useToast(); const rechargeRef = useRef(); - const currency = useEnvStore((s) => s.currency); + const { data: balance_raw } = useQuery({ + queryKey: ['getAccount'], + queryFn: () => + request>('/api/account/getAmount') + }); + + let rechargAmount = balance_raw?.data?.balance || 0; + let expenditureAmount = balance_raw?.data?.deductionBalance || 0; + let balance = rechargAmount - expenditureAmount; return ( - - - - - - {t('SideBar.CostOverview')} - - - - - - - + + + - + - + - - - - - - {t('Recent Transactions')} - - - ({t('currencyUnit')}:{' '} - ) - - - - {(isInitialLoading || billingItems.length === 0) && ( - - - - )} + + + + + @@ -127,9 +105,8 @@ function CostOverview() { direction={'column'} justify={'flex-start'} > - - - + + diff --git a/frontend/providers/costcenter/src/pages/create_invoice/index.tsx b/frontend/providers/costcenter/src/pages/create_invoice/index.tsx index a1cccf4bb62e..fc90df75b3d8 100644 --- a/frontend/providers/costcenter/src/pages/create_invoice/index.tsx +++ b/frontend/providers/costcenter/src/pages/create_invoice/index.tsx @@ -42,7 +42,7 @@ function Invoice() { } ], () => { - return request>('/api/billing/recharge', { + return request>('/api/billing/rechargeBillingList', { data: { startTime, endTime @@ -63,7 +63,15 @@ function Invoice() { ); return ( - + {processState === 0 ? ( <> diff --git a/frontend/providers/costcenter/src/pages/resource_analysis/index.tsx b/frontend/providers/costcenter/src/pages/resource_analysis/index.tsx new file mode 100644 index 000000000000..de2b64c21a54 --- /dev/null +++ b/frontend/providers/costcenter/src/pages/resource_analysis/index.tsx @@ -0,0 +1,135 @@ +import { + Box, + Flex, + Heading, + HStack, + Img, + Stack, + Tab, + TabList, + TabPanels, + Tabs, + Text, + VStack +} from '@chakra-ui/react'; +import { createContext, useMemo, useState } from 'react'; +import receipt_icon from '@/assert/receipt_long_black.svg'; +import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import { useTranslation } from 'next-i18next'; +import NamespaceMenu from '@/components/menu/NamespaceMenu'; +import useBillingStore from '@/stores/billing'; +import RegionMenu from '@/components/menu/RegionMenu'; +import request from '@/service/request'; +import { ApiResp, AppOverviewBilling } from '@/types'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import useOverviewStore from '@/stores/overview'; +import Quota from '@/components/valuation/quota'; +import { Cost } from '@/components/cost_overview/cost'; +import { Refresh } from '@/components/Refresh'; + +export default function Resource() { + const { t } = useTranslation(); + const { setNamespace, getRegion, getAppName, getAppType, getNamespace, getCycle } = + useBillingStore(); + const { startTime, endTime } = useOverviewStore(); + const regionUid = getRegion()?.uid || ''; + const [orderID, setOrderID] = useState(''); + const [page, setPage] = useState(1); + const [totalPage, setTotalPage] = useState(1); + const [totalItem, setTotalItem] = useState(0); + const [pageSize, setPageSize] = useState(4); + const queryBody = { + endTime, + startTime, + regionUid, + appType: getAppType(), + appName: getAppName(), + namespace: getNamespace(), + page, + pageSize + }; + const queryClient = useQueryClient(); + + const { data, isPreviousData, isFetching } = useQuery({ + queryFn() { + return request.post< + any, + ApiResp<{ + overviews: AppOverviewBilling[]; + total: number; + totalPage: number; + }> + >('/api/billing/appOverview', queryBody); + }, + onSuccess(data) { + if (!data.data) { + return; + } + const { total, totalPage } = data.data; + if (totalPage === 0) { + // search reset + setTotalPage(1); + setTotalItem(1); + } else { + setTotalItem(total); + setTotalPage(totalPage); + } + if (totalPage < page) { + setPage(1); + } + }, + keepPreviousData: true, + queryKey: ['appOverviewBilling', queryBody, page, pageSize] + }); + const appOverviews = useMemo(() => data?.data?.overviews || [], [data?.data?.overviews]); + return ( + + + + + + {t('SideBar.resource_analysis')} + + + + + + {t('region')} + + + + + + {t('workspace')} + + + + { + queryClient.invalidateQueries(['costs'], { exact: false }); + }} + ml={'auto'} + mb="16px" + /> + + + + + {t('Source Quota')} + + + + + + + + ); +} + +export async function getServerSideProps({ locale }: { locale: string }) { + return { + props: { + ...(await serverSideTranslations(locale, ['common', 'applist'], undefined, ['zh', 'en'])) + } + }; +} diff --git a/frontend/providers/costcenter/src/pages/valuation/index.tsx b/frontend/providers/costcenter/src/pages/valuation/index.tsx index 5f44021d8222..c855998c5f2e 100644 --- a/frontend/providers/costcenter/src/pages/valuation/index.tsx +++ b/frontend/providers/costcenter/src/pages/valuation/index.tsx @@ -12,15 +12,15 @@ import { Th, Tbody, Td, - Popover, - PopoverTrigger, - Button, - PopoverContent, - ButtonProps, Icon, - Link + Tab, + TabList, + Tabs, + TabPanel, + TabPanels, + FlexProps, + ImgProps } from '@chakra-ui/react'; -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 { valuationMap } from '@/constants/payment'; @@ -30,7 +30,6 @@ import { useTranslation } from 'next-i18next'; import { ApiResp } from '@/types/api'; import nvidaIcon from '@/assert/bi_nvidia.svg'; import { CYCLE } from '@/constants/valuation'; -import PredictCard from '@/components/valuation/predictCard'; import useEnvStore from '@/stores/env'; import CurrencySymbol from '@/components/CurrencySymbol'; import Quota from '@/components/valuation/quota'; @@ -39,8 +38,11 @@ import CpuIcon from '@/components/icons/CpuIcon'; import { MemoryIcon } from '@/components/icons/MemoryIcon'; import { NetworkIcon } from '@/components/icons/NetworkIcon'; import { StorageIcon } from '@/components/icons/StorageIcon'; -import { PortIcon } from '@sealos/ui'; import { ValuationStandard } from '@/types/valuation'; +import BaseMenu from '@/components/menu/BaseMenu'; +import RegionMenu from '@/components/menu/RegionMenu'; +import useBillingStore from '@/stores/billing'; +import { PortIcon } from '@/components/icons/PortIcon'; type CardItem = { title: string; @@ -48,7 +50,7 @@ type CardItem = { unit: string; bg: string; idx: number; - icon: typeof Icon; + icon: typeof Img; }; function CycleMenu({ @@ -58,90 +60,40 @@ function CycleMenu({ }: { cycleIdx: number; setCycleIdx: (x: number) => void; -} & ButtonProps) { +} & FlexProps) { const { t } = useTranslation(); return ( - - - - - - - {CYCLE.map((v, idx) => ( - - ))} - - - + t(v)) as unknown as string[]} + {...props} + /> ); } -const getValuation = () => - request>('/api/properties'); +const getValuation = (regionUid: string) => + request.post>('/api/properties', { + regionUid + }); function Valuation() { const { t } = useTranslation(); const gpuEnabled = useEnvStore((state) => state.gpuEnabled); const [cycleIdx, setCycleIdx] = useState(0); - const { data: _data } = useQuery(['valuation'], getValuation); + const { getRegion } = useBillingStore(); + const regionUid = getRegion()?.uid || ''; + const { data: _data } = useQuery(['valuation', regionUid], () => getValuation(regionUid)); const data = _data?.data?.properties ?.filter((x) => !x.name.startsWith('gpu-')) ?.flatMap((x) => { const props = valuationMap.get(x.name); if (!props) return []; - let icon; + let icon: typeof Img; let title = x.name; if (x.name === 'cpu') icon = CpuIcon; else if (x.name === 'memory') icon = MemoryIcon; @@ -165,6 +117,7 @@ function Valuation() { ]; }) ?.sort((a, b) => a.idx - b.idx) || []; + const networkData = data.filter((x) => x.title === 'network'); const gpuProps = valuationMap.get('gpu')!; const gpuData = gpuEnabled ? _data?.data?.properties @@ -179,6 +132,7 @@ function Valuation() { }) ?.sort((a, b) => (a.name > b.name ? 1 : -1)) || [] : []; + const headers = ['Valuation.Name', 'Valuation.Unit', 'Valuation.Price']; const currency = useEnvStore((s) => s.currency); return ( @@ -186,154 +140,215 @@ function Valuation() { flex={1} bg={'#FBFBFC'} alignItems="center" - p={'24px'} + px={'24px'} + py={'20px'} borderRadius={'4px'} overflowY={'auto'} > - - - {t('Valuation.Standard')} - - - - - - {t('common valuation')} - {t('Unit')} - + 、 + + + + {t('Price Table')} + + + + - - - - - {['Name', 'Unit', 'Price'].map((item) => ( - - ))} - - - - {data.map((x) => ( - - + + + {data + .filter((x) => x.title !== 'network') + .map((x, i, arr) => ( + + + + + + ))} + +
- - {t(item)} - {['Price'].includes(item) && } - -
- - - - {t(x.title)} - - {x.title === 'Port' && ( - + + + + + + + {t('common valuation')} + + + + + + {headers.map((item, idx) => ( + - - - ))} - -
- {t('onlyChargedWhenOpenDBPublicAccess')} - - )} - - - - {[x.unit, x.title !== 'network' ? `${t(CYCLE[cycleIdx])}` : ''] - .filter((v) => v.trim() !== '') - .join('/')} - {x.title === 'network' ? x.price[0] : x.price[cycleIdx]}
-
-
- {gpuEnabled && gpuData.length > 0 && ( - - - - {t('Gpu valuation')} + + + {t(item)} + + {idx === 2 && } + + + ))} +
+ + + + {t(x.title)} + + + + {[x.unit, `${t(CYCLE[cycleIdx])}`] + // .filter((v) => v.trim() !== '') + .join('/')} + + {x.price[cycleIdx]} +
+
+
+ + + + {t('Network valuation')} + + + + + + {headers.map((item, idx) => ( + + ))} + + + + {networkData.map((x, i, arr) => ( + + + + + + ))} + +
+ + + {t(item)} + + {idx === 2 && } + +
+ + {t(x.title)} + + /{t(x.unit)} + + {x.price[0]} +
+
+
+ {gpuEnabled && gpuData.length > 0 && ( + + + + {t('Gpu valuation')} + + + + + + {headers.map((item, idx) => ( + + ))} + + + + {gpuData.map((x) => ( + + + + + + ))} + +
+ + {t(item)} + {idx === 2 && } + +
+ + {t(x.name)} + + {t('GPU Unit')}/{t('Hour')} + {x.price}
+
+
+ )}
- - - - - {['Name', 'Unit', 'Price'].map((item) => ( - - ))} - - - - {gpuData.map((x) => ( - - - - - - ))} - -
- - {t(item)} - {['Price'].includes(item) && } - -
- - {t(x.name)} - - {t('GPU Unit')}/{t('Hour')} - {x.price}
-
- - )} -
-
- - - - {t('Source Quota')} - - - - - - - - - {t('Next month cost estimation')} - ({t('Predict Detail')}) - - - ( {t('Unit')} ) - - - - - - + + +
); @@ -343,7 +358,7 @@ export default Valuation; export async function getServerSideProps({ locale }: { locale: string }) { const queryClient = new QueryClient(); - await queryClient.prefetchQuery(['valuation'], getValuation); + await queryClient.prefetchQuery(['valuation'], () => getValuation('')); return { props: { ...(await serverSideTranslations(locale, ['common'], null, ['zh', 'en'])), diff --git a/frontend/providers/costcenter/src/service/backend/auth.ts b/frontend/providers/costcenter/src/service/backend/auth.ts index 651a5b15c937..f8753d8241d3 100644 --- a/frontend/providers/costcenter/src/service/backend/auth.ts +++ b/frontend/providers/costcenter/src/service/backend/auth.ts @@ -1,6 +1,12 @@ import { IncomingHttpHeaders } from 'http'; import { K8sApi } from '@/service/backend/kubernetes'; - +import * as yaml from 'js-yaml'; +// export function switchKubeconfigNamespace(kc: string, namespace: string) { +// const oldKc = yaml.load(kc); +// // @ts-ignore +// oldKc.contexts[0].context.namespace = namespace; +// return yaml.dump(oldKc); +// } export const authSession = async (header: IncomingHttpHeaders) => { try { if (!header?.authorization) { @@ -10,6 +16,17 @@ export const authSession = async (header: IncomingHttpHeaders) => { const kubeconfig = decodeURIComponent(header.authorization); const kc = K8sApi(kubeconfig); + // rewrite exportConfig to stop transform domain to ip + kc.exportConfig = () => { + const domain = global.AppConfig.cloud.domain; + if (!domain) return kubeconfig; + const oldKc = yaml.load(kubeconfig); + const newServer = `https://${domain}:6443`; + //@ts-ignore + oldKc.clusters[0].cluster.server = newServer; + const newkubeconfig = yaml.dump(oldKc); + return newkubeconfig; + }; return Promise.resolve(kc); } catch (err) { return Promise.reject('凭证错误'); diff --git a/frontend/providers/costcenter/src/service/backend/kubernetes.ts b/frontend/providers/costcenter/src/service/backend/kubernetes.ts index a0633dd5b6ec..7125a942c995 100644 --- a/frontend/providers/costcenter/src/service/backend/kubernetes.ts +++ b/frontend/providers/costcenter/src/service/backend/kubernetes.ts @@ -36,8 +36,6 @@ export function K8sApi(config: string): k8s.KubeConfig { }); } - // console.log(kc); - return kc; } diff --git a/frontend/providers/costcenter/src/service/backend/region.ts b/frontend/providers/costcenter/src/service/backend/region.ts new file mode 100644 index 000000000000..159e1bbddf6b --- /dev/null +++ b/frontend/providers/costcenter/src/service/backend/region.ts @@ -0,0 +1,38 @@ +import { Region } from '@/types/region'; + +export async function getRegionList() { + const regionUrl = + global.AppConfig.costCenter.components.accountService.url + '/account/v1alpha1/regions'; + const fetchResponse = await fetch(regionUrl, { + method: 'POST' + }); + const regionRes = await fetchResponse.json(); + if (!fetchResponse.ok) { + console.log('fetch region list error:', regionRes); + return null; + } + const regions: Region[] = regionRes?.regions || []; + if (!regions) { + console.log('region list is null'); + return null; + } + return regions; +} +export async function getRegionByUid(regionUid?: string) { + const regions = await getRegionList(); + if (!regions) { + return null; + } + const currentRegionIdx = regions.findIndex((region: Region) => region.uid === regionUid); + if (currentRegionIdx === -1) { + return null; + } + return regions[currentRegionIdx]; +} +export function makeAPIURL(region: Region | undefined | null, api: string) { + const baseUrl = region?.accountSvc + ? `http://${region?.accountSvc}` + : global.AppConfig.costCenter.components.accountService.url; + const url = baseUrl + api; + return url; +} diff --git a/frontend/providers/costcenter/src/service/sendToBot.ts b/frontend/providers/costcenter/src/service/sendToBot.ts index 2dda2ea608d8..9918718865b5 100644 --- a/frontend/providers/costcenter/src/service/sendToBot.ts +++ b/frontend/providers/costcenter/src/service/sendToBot.ts @@ -92,9 +92,7 @@ export const sendToBot = async ({ } } }); - console.log(body); const url = global.AppConfig.costCenter.invoice.feiShuBotURL; - console.log(url); const result = await axios.post(url, body, { timeout: 15000, headers: { diff --git a/frontend/providers/costcenter/src/stores/appType.ts b/frontend/providers/costcenter/src/stores/appType.ts new file mode 100644 index 000000000000..2f26ad49fa1b --- /dev/null +++ b/frontend/providers/costcenter/src/stores/appType.ts @@ -0,0 +1,49 @@ +import { create } from 'zustand'; +import { persist, StorageValue } from 'zustand/middleware'; +type AppTypeState = { + appTypeMap: Map; + setAppTypeMap(appTypeMap: Map): void; + getAppType(appType: string): string; +}; +const useAppTypeStore = create()( + persist( + (set, get) => ({ + appTypeMap: new Map(), + setAppTypeMap(appTypeMap: Map) { + set({ appTypeMap }); + }, + getAppType(appType: string) { + return get().appTypeMap?.get(appType) || ''; + } + }), + { + name: 'appType-store', + storage: { + getItem: (name) => { + const str = localStorage.getItem(name); + if (!str) return null; + const { state } = JSON.parse(str); + return { + state: { + ...state, + appTypeMap: new Map(state.appTypeMap) + } + }; + }, + setItem: (name, newValue: StorageValue) => { + // functions cannot be JSON encoded + const str = JSON.stringify({ + state: { + ...newValue.state, + appTypeMap: Array.from(newValue.state.appTypeMap.entries()) + } + }); + localStorage.setItem(name, str); + }, + removeItem: (name) => localStorage.removeItem(name) + } + } + ) +); + +export default useAppTypeStore; diff --git a/frontend/providers/costcenter/src/stores/billing.ts b/frontend/providers/costcenter/src/stores/billing.ts index eaca9da33909..d9d595685f29 100644 --- a/frontend/providers/costcenter/src/stores/billing.ts +++ b/frontend/providers/costcenter/src/stores/billing.ts @@ -1,40 +1,140 @@ +import { CYCLE } from '@/constants/valuation'; +import { Cycle } from '@/types/cycle'; +import { RegionClient } from '@/types/region'; import { formatMoney } from '@/utils/format'; +import { number } from 'echarts'; import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; type BillingState = { cpu: number; memory: number; storage: number; network: number; gpu: number; - namespace: string; - appType: string; - setAppType: (appType: string) => void; - setNamespace: (namespace: string) => void; + namespaceIdx: number; + appTypeIdx: number; + cycleIdx: number; + regionIdx: number; + appNameIdx: number; + namespaceList: [string, string][]; + appTypeList: string[]; + regionList: RegionClient[]; + appNameList: string[]; + setAppType: (appType: number) => void; + setAppName: (appName: number) => void; + setNamespace: (namespace: number) => void; + setAppNameList: (appNameList: string[]) => void; + setAppTypeList: (appTypeList: string[]) => void; + setRegionList: (regionList: RegionClient[]) => void; + setNamespaceList: (namespaceList: [string, string][]) => void; + setRegion: (region: number) => void; + setCycle: (cycle: number) => void; + getCycle: () => Cycle; + getAppType: () => string; + getAppName: () => string; + detailIsOpen: boolean; + openBillingDetail: () => void; + closeBillingDetail: () => void; + // [id, name] + getNamespace: () => [string, string] | null; + getRegion: () => RegionClient | null; updateCpu: (cpu: number) => void; updateMemory: (memory: number) => void; updateStorage: (storage: number) => void; updateNetwork: (network: number) => void; updateGpu: (gpu: number) => void; }; -const useBillingStore = create((set) => ({ - cpu: 0, - memory: 0, - storage: 0, - gpu: 0, - network: 0, - namespace: '', - appType: '', - setAppType(appType: string) { - set({ appType }); - }, - setNamespace(namespace: string) { - set({ namespace }); - }, - updateCpu: (cpu: number) => set({ cpu: formatMoney(cpu) }), - updateMemory: (memory: number) => set({ memory: formatMoney(memory) }), - updateStorage: (storage: number) => set({ storage: formatMoney(storage) }), - updateNetwork: (network: number) => set({ network: formatMoney(network) }), - updateGpu: (gpu: number) => set({ gpu: formatMoney(gpu) }) -})); - +const useBillingStore = create()( + persist( + (set, get) => ({ + cpu: 0, + memory: 0, + storage: 0, + gpu: 0, + network: 0, + namespaceIdx: 0, + appTypeIdx: 0, + cycleIdx: 0, + regionIdx: 0, + appNameIdx: 0, + namespaceList: [['', 'All Workspace']], + appTypeList: ['all_app_type'], + regionList: [], + appNameList: ['All APP'], + detailIsOpen: false, + openBillingDetail: () => set({ detailIsOpen: true }), + closeBillingDetail: () => set({ detailIsOpen: false }), + setAppNameList(appNameList: string[]) { + const { getAppName } = get(); + const appName = getAppName(); + const newAppNameIdx = appNameList.findIndex((item) => item === appName); + const appNameIdx = newAppNameIdx === -1 ? 0 : newAppNameIdx; + console.log('setAppNameList', JSON.stringify(appNameList), appNameIdx, appName); + set({ appNameList, appNameIdx }); + }, + setAppTypeList(appTypeList: string[]) { + set({ appTypeList }); + }, + setRegionList(regionList: RegionClient[]) { + const { getRegion } = get(); + const region = getRegion(); + const newRegionIdx = regionList.findIndex((item) => item.uid === region?.uid); + const regionIdx = newRegionIdx === -1 ? 0 : newRegionIdx; + set({ regionList, regionIdx }); + }, + setNamespaceList(namespaceList: [string, string][]) { + const { getNamespace } = get(); + const namespace = getNamespace(); + const newNamespaceIdx = namespaceList.findIndex((item) => item[0] === namespace?.[0]); + const namespaceIdx = newNamespaceIdx === -1 ? 0 : newNamespaceIdx; + set({ namespaceList, namespaceIdx }); + }, + setAppType(appTypeIdx: number) { + set({ appTypeIdx }); + }, + setAppName(appNameIdx: number) { + set({ appNameIdx }); + }, + setNamespace(namespaceIdx: number) { + set({ namespaceIdx }); + }, + setCycle(cycleIdx: number) { + set({ cycleIdx }); + }, + setRegion(regionIdx: number) { + set({ regionIdx }); + }, + getCycle() { + if (get().cycleIdx === -1) return CYCLE[0]; + return CYCLE[get().cycleIdx]; + }, + getAppName() { + const { appNameIdx, appNameList } = get(); + if (appNameIdx === -1 || appNameIdx === 0 || appNameList.length === 0) return ''; + return appNameList[appNameIdx]; + }, + getAppType() { + if (get().appTypeIdx === -1 || get().appTypeIdx === 0 || get().appTypeList.length === 0) + return ''; + return get().appTypeList[get().appTypeIdx]; + }, + getRegion() { + if (get().regionIdx === -1 || get().regionList.length === 0) return null; + return get().regionList[get().regionIdx]; + }, + getNamespace() { + if (get().namespaceIdx === -1 || get().namespaceList.length === 0) return null; + return get().namespaceList[get().namespaceIdx]; + }, + updateCpu: (cpu: number) => set({ cpu: formatMoney(cpu) }), + updateMemory: (memory: number) => set({ memory: formatMoney(memory) }), + updateStorage: (storage: number) => set({ storage: formatMoney(storage) }), + updateNetwork: (network: number) => set({ network: formatMoney(network) }), + updateGpu: (gpu: number) => set({ gpu: formatMoney(gpu) }) + }), + { + name: 'billing-query-store' + } + ) +); export default useBillingStore; diff --git a/frontend/providers/costcenter/src/stores/overview.ts b/frontend/providers/costcenter/src/stores/overview.ts index 89f6a4953ffa..a78eb0b26e8d 100644 --- a/frontend/providers/costcenter/src/stores/overview.ts +++ b/frontend/providers/costcenter/src/stores/overview.ts @@ -27,12 +27,11 @@ const useOverviewStore = create()( immer((set, get) => ({ rechargeSource: 0, balance: 0, - startTime: subDays(END_TIME, 2), + startTime: subDays(END_TIME, 7), endTime: END_TIME, preItems: [], items: [], setRecharge: (rechargeSource) => { - console.log('set'); set({ rechargeSource }); }, setStartTime: (time) => set({ startTime: time }), diff --git a/frontend/providers/costcenter/src/types/app.ts b/frontend/providers/costcenter/src/types/app.ts new file mode 100644 index 000000000000..5f9e78c3e06b --- /dev/null +++ b/frontend/providers/costcenter/src/types/app.ts @@ -0,0 +1,5 @@ +export type AppListItem = { + namespace: string; + appType: number; + appName: string; +}; diff --git a/frontend/providers/costcenter/src/types/billing.ts b/frontend/providers/costcenter/src/types/billing.ts index 73d75104446d..d380f6ec23af 100644 --- a/frontend/providers/costcenter/src/types/billing.ts +++ b/frontend/providers/costcenter/src/types/billing.ts @@ -36,6 +36,23 @@ export type Costs = { port: number; gpu?: number; }; +export type AppOverviewBilling = { + amount: number; + namespace: string; + regionDomain: string; + appType: number; + appName: string; +}; +export type APPBillingItem = { + app_name: string; + app_type: number; + time: string; + order_id: string; + namespace: string; + used: Record<'0' | '1' | '2' | '3' | '4' | '5', number>; + used_amount: Record<'0' | '1' | '2' | '3' | '4' | '5', number>; + amount: number; +}; export type BillingItem = { amount: number; appType: string; @@ -64,14 +81,11 @@ export type BillingData = { }; }; export type PropertiesCost = { - amount: { - cpu: number; - memory: number; - network: number; - gpu?: number; - 'services.nodeports': number; - storage: number; - }; + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; }; export type RechargeBillingItem = { ID: string; diff --git a/frontend/providers/costcenter/src/types/config.ts b/frontend/providers/costcenter/src/types/config.ts index fe68eecda819..c8e4d2bdd2d6 100644 --- a/frontend/providers/costcenter/src/types/config.ts +++ b/frontend/providers/costcenter/src/types/config.ts @@ -41,6 +41,10 @@ export type Components = { }; export type AppConfigType = { + cloud: { + regionUID: string; + domain: string; + }; costCenter: { transferEnabled: boolean; currencyType: string; @@ -87,6 +91,10 @@ export var DefaultAppConfig: AppConfigType = { } }, gpuEnabled: false + }, + cloud: { + regionUID: '', + domain: '' } }; diff --git a/frontend/providers/costcenter/src/types/cycle.ts b/frontend/providers/costcenter/src/types/cycle.ts new file mode 100644 index 000000000000..d66617270db6 --- /dev/null +++ b/frontend/providers/costcenter/src/types/cycle.ts @@ -0,0 +1,3 @@ +import { CYCLE } from '@/constants/valuation'; + +export type Cycle = (typeof CYCLE)[number]; diff --git a/frontend/providers/costcenter/src/types/region.ts b/frontend/providers/costcenter/src/types/region.ts new file mode 100644 index 000000000000..e1a298f17c34 --- /dev/null +++ b/frontend/providers/costcenter/src/types/region.ts @@ -0,0 +1,16 @@ +export type Region = { + accountSvc: string; + name: { + zh: string; + en: string; + }; + domain: string; + uid: string; +}; +export type RegionClient = { + name: { + zh: string; + en: string; + }; + uid: string; +}; diff --git a/frontend/providers/license/next.config.js b/frontend/providers/license/next.config.js index e634a41a6b49..65174947fd28 100644 --- a/frontend/providers/license/next.config.js +++ b/frontend/providers/license/next.config.js @@ -1,6 +1,6 @@ /** @type {import('next').NextConfig} */ -const { i18n } = require('./next-i18next.config') -const path = require('path') +const { i18n } = require('./next-i18next.config'); +const path = require('path'); const nextConfig = { i18n, output: 'standalone', @@ -13,15 +13,15 @@ const nextConfig = { issuer: /\.[jt]sx?$/, use: ['@svgr/webpack'] } - ]) - config.plugins = [...config.plugins] - return config + ]); + config.plugins = [...config.plugins]; + return config; }, transpilePackages: ['@sealos/ui'], experimental: { // this includes files from the monorepo base two directories up outputFileTracingRoot: path.join(__dirname, '../../') } -} +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/frontend/providers/objectstorage/.vscode/settings.json b/frontend/providers/objectstorage/.vscode/settings.json new file mode 100644 index 000000000000..1f926c5bfccb --- /dev/null +++ b/frontend/providers/objectstorage/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "i18n-ally.localesPaths": [ + "public/locales" + ] +} \ No newline at end of file